From 58f288b5aa64bf50c23a358af0584af492bf64d3 Mon Sep 17 00:00:00 2001 From: Lucas Leclerc Date: Mon, 2 Sep 2024 18:52:18 +0200 Subject: [PATCH] fix(p2p/proxy_workshop): fix readme and contracts --- p2p/6.Create_a_proxy/README.md | 258 +++++++++++------- p2p/6.Create_a_proxy/SETUP.md | 2 +- p2p/6.Create_a_proxy/help/fallback.md | 16 +- .../utils/{Counter_v1.sol => CounterV1.sol} | 2 +- .../utils/{Counter_v2.sol => CounterV2.sol} | 2 +- p2p/6.Create_a_proxy/utils/ProxyV1.t.sol | 30 ++ p2p/6.Create_a_proxy/utils/ProxyV2.t.sol | 32 +++ p2p/6.Create_a_proxy/utils/ProxyV3.t.sol | 47 ++++ p2p/6.Create_a_proxy/utils/proxy.t.sol | 92 ------- 9 files changed, 280 insertions(+), 201 deletions(-) rename p2p/6.Create_a_proxy/utils/{Counter_v1.sol => CounterV1.sol} (92%) rename p2p/6.Create_a_proxy/utils/{Counter_v2.sol => CounterV2.sol} (92%) create mode 100644 p2p/6.Create_a_proxy/utils/ProxyV1.t.sol create mode 100644 p2p/6.Create_a_proxy/utils/ProxyV2.t.sol create mode 100644 p2p/6.Create_a_proxy/utils/ProxyV3.t.sol delete mode 100644 p2p/6.Create_a_proxy/utils/proxy.t.sol diff --git a/p2p/6.Create_a_proxy/README.md b/p2p/6.Create_a_proxy/README.md index c0ed287a..a794dd11 100644 --- a/p2p/6.Create_a_proxy/README.md +++ b/p2p/6.Create_a_proxy/README.md @@ -17,27 +17,28 @@ If you have already heard about smart contracts, you know that it's not possible ### Why is it useful? The use of a proxy contract, such as ERC-1967, provides several benefits, including: -- **Upgradability**๐Ÿ“ˆ: The ability to upgrade the implementation contract without changing the contract's address or user interactions. + +- **Upgradability**๐Ÿ“ˆ: The ability to upgrade the implementation contract without changing the contract's address or user interactions is very useful, especially when there is a security issue. - **Cost-Efficiency**๐Ÿ’ฐ: Upgrading a contract using a proxy contract is more cost-effective than deploying a new contract. - **Security**๐Ÿ”’: Proxy contracts can be used to implement security features, such as access control and permission management. +- **Storage Separation**๐Ÿ—„๏ธ: The proxy is the storage, so when we change the implementation, all data remains in the proxy. ### What technology is used to do this? -**Solidity** is a **programming language** specifically **designed for developing smart contracts on the Ethereum blockchain**. It enables developers to **create self-executing, autonomous, and verifiable contracts** that define rules and interactions within a **decentralized environment**โ›“๏ธโ€๐Ÿ’ฅ. Solidity is based on **JavaScript-like syntax** and offers features such as state management, control structures, events, and calls to other contracts, enabling the **creation of complex and secure solutions** on the Ethereum blockchain. If you've never worked with Solidity before, take a look at the [Solidity Essentials](./Solidity.md) to understand the basics. You can also consult the [official documentation](https://docs.soliditylang.org/en/v0.8.21/). +**Solidity** is a **programming language** specifically **designed for developing smart contracts on the Ethereum blockchain**. It enables developers to **create self-executing, autonomous, and verifiable contracts** that define rules and interactions within a **decentralized environment**โ›“๏ธโ€๐Ÿ’ฅ. Solidity is based on **JavaScript-like syntax** and offers features such as state management, control structures, events, and calls to other contracts, enabling the **creation of complex and secure solutions** on the Ethereum blockchain. If you've never worked with Solidity before, take a look at the [Solidity Essentials](./Solidity.md) to understand the basics. You can also consult the [official documentation](https://docs.soliditylang.org/en/latest/). ## Step 0 - Setup ๐Ÿ’ป Please refer to the [SETUP.md](./SETUP.md) file. Now, let's create the needed files. + - ๐Ÿ“‚ First, delete the `script` folder as we will not need it, as well as the `Counter.sol` and `Counter.t.sol` files. -- ๐Ÿ“„ Create a folder named `proxy` in the `src` folder, and then a `proxy_v1.sol` file. -- ๐Ÿ“„ Create a folder named `implementations` in the `src` folder and add `Counter_V1.sol` and `Counter_V2.sol` files in it. - - The `Counter_V1.sol` file will contain the first version of the counter contract. - - The `Counter_V2.sol` file will contain the second version of the counter contract. - - Feel free to look at the content of these files to understand the differences between the two versions. - - The proxy will allow us to change the implementation easily by switching from Counter_V1 to Counter_V2 without requiring the user to change the address of the contract they call. -- ๐Ÿ—‘๏ธ Delete the `counter.t.sol` file in the `test` folder and replace it with the `proxy.t.sol` file in the `utils` folder. - - This file contains the tests for both the proxy contract and the counter contract. +- ๐Ÿ“„ Create a folder named `proxys` in the `src` folder, and then a `ProxyV1.sol` file. +- ๐Ÿ“„ Create a folder named `implementations` in the `src` folder and add `CounterV1.sol` and `CounterV2.sol` files which are in the `utils` folder. + - The `CounterV1.sol` file will contain the first version of the counter contract. + - The `CounterV2.sol` file will contain the second version of the counter contract. + - Feel free to look at the content of these files to understand the differences between the two versions. + - The proxy will allow us to change the implementation easily by switching from Counter_V1 to Counter_V2 without requiring the user to change the address of the contract they call. ## Step 1 - Let's start creating our proxy ๐Ÿ @@ -45,31 +46,70 @@ Now, let's create the needed files. In this step, you will implement the delegate call mechanism, which is the basis of the proxy. The delegate call mechanism allows a contract to delegate a call to another contract, which will execute the function in its context. This mechanism is essential for the implementation of a proxy contract. +**Differences between `delegatecall` and `call` in Solidity:** +- Storage Context: + - `delegatecall`: Executes the code of the called contract in the context of the calling contract. This means it uses the storage of the calling contract. + - `call`: Executes the code of the called contract in its own context, using its own storage. +- `msg.sender`: + - `delegatecall`: The msg.sender remains the same as the original caller. + - `call`: The msg.sender is the address of the calling contract. +- Use Case: + - `delegatecall`: Typically used for libraries or proxy contracts where you want to execute code in the context of the calling contract. + - `call`: Used for sending Ether or calling functions on other contracts where the context of the called contract is important. + +Example: +```solidity +contract A { + uint public n; + function setN(uint _n) public { + n = _n; + } +} + +contract B { + uint public n; + function setN(address _contract, uint _n) public { + _contract.call(abi.encodeWithSignature("setN(uint256)", _n)); + } + function delegateSetN(address _contract, uint _n) public { + _contract.delegatecall(abi.encodeWithSignature("setN(uint256)", _n)); + } +} +``` +Here, if you call `delegateSetN` from contract B, the `n` variable of contract A will be updated. If you call `setN`, the `n` variable of contract B will be updated. + ### ๐Ÿ“Œ **Tasks**: -- Create a contract `PROXY_V1` in the `proxy_v1.sol` file. -- Add the following public variables in this order in the `PROXY_V1` contract: - - `implem`, which is an address. - - This variable will store the address of the implementation contract. -- Add the contract constructor in the `PROXY_V1` contract. - - In it, initialize the `implementation` variable with the address of the implementation contract. -- Add the `implementation` function in the `PROXY_V1` contract. - - This function will return the address of the implementation contract. -- Add the `fallback` function in the `PROXY_V1` contract. - - This function will use the delegate call mechanism to delegate the call to the implementation contract. - - You need to use inline assembly at one time in this function. +- Create a contract `ProxyV1` in the `ProxyV1.sol` file. +- Add the following public variables in this order in the `ProxyV1` contract: + - `implem`, which is an address. + - This variable will store the address of the implementation contract. +- Add the contract constructor in the `ProxyV1` contract. + - In it, initialize the `implementation` variable with the address of the implementation contract. +- Add the `receive` function which is external payable. + - This function will be used to receive Ether sent to the contract. + - You can leave it empty for now, we just need to have it in the contract. +- Add the `implementation` function in the `ProxyV1` contract. + - This function will return the address of the implementation contract. +- Add the `fallback` function in the `ProxyV1` contract. + - This function will use the delegate call mechanism to delegate the call to the implementation contract. + - firstly, do a [deleguate call](https://www.rareskills.io/post/delegatecall) to the implementation contract and save the returned values with the `success` variable which is a boolean and `returnData` which is bytes. + - This line calls the `delegatecall` function on the implementation contract with the `msg.data` that was sent to the proxy. The `delegatecall` function executes the code of the implementation contract in the context of the proxy contract. The `success` variable will be `true` if the `delegatecall` was successful, and the `returnData` variable will contain the return data from the `delegatecall`. + - now you need to check if `success` is true + - if so, use [inline assembly](https://docs.soliditylang.org/en/latest/assembly.html) to return the `returnData` value. You need to use assembly because in pure solidity you can't return data not defined in the function by `returns(uint256)` for example. + - if not, `returndata` become the error message so return it with [inline assembly](https://docs.soliditylang.org/en/latest/assembly.html) if it's not empty. Otherwise return a generic message. > โš ๏ธ Don't forget the header of the file. > ๐Ÿ™‹โ€โ™‚๏ธ If you're really stuck on the fallback function, you can find help [here](./help/fallback.md), but try to do it by yourself first. ### ๐Ÿ“š **Documentation**: -- [Constructor ๐Ÿ› ๏ธ](https://docs.soliditylang.org/en/v0.8.21/contracts.html#constructor) +- [Variable visibility](https://docs.soliditylang.org/en/latest/contracts.html#state-variable-visibility) +- [Constructor ๐Ÿ› ๏ธ](https://docs.soliditylang.org/en/latest/contracts.html#constructor) +- [Function visibility ๐Ÿ‘€](https://docs.soliditylang.org/en/latest/contracts.html#function-visibility) +- [Fallback function ๐Ÿ“](https://docs.soliditylang.org/en/latest/contracts.html#fallback-function) +- [Delegate call ๐Ÿ“ฒ](https://www.rareskills.io/post/delegatecall) - [Inline Assembly ๐Ÿ“„](https://docs.soliditylang.org/en/latest/assembly.html) -- [Variable visibility](https://docs.soliditylang.org/en/v0.8.21/contracts.html#state-variable-visibility) -- [Delegate call ๐Ÿ“ฒ](https://docs.soliditylang.org/en/v0.8.21/introduction-to-smart-contracts.html#delegatecall-and-libraries) -- [Function visibility ๐Ÿ‘€](https://docs.soliditylang.org/en/v0.8.21/contracts.html#function-visibility) -- [Fallback function ๐Ÿ“](https://docs.soliditylang.org/en/v0.8.21/contracts.html#fallback-function) ## Step 2 - Let's test if it works ๐Ÿงช @@ -79,24 +119,26 @@ In this step, you will test the proxy contract you have just implemented. Testin ### ๐Ÿ“Œ **Tasks**: -In the `proxy.t.sol` file in the `utils` folder, you will find the tests for the proxy contract. +Add the `ProxyV1.t.sol` file, from the `utils`folder, in the `test`folder. This is the tests for the proxy contract. You will have to run these tests to verify that the proxy contract works correctly using the command : ```bash -forge test -vvvv +forge test ``` -- `-vvvv` adds more details. -But first, comment out all the functions starting with `test...` except the first ones (`testProxy_v1` and `testCounter`) and all variables unused. +- `-vvvv` add it for more details. -Now, run the tests. Does it work? +Run the tests. Does it work? Normally, only the counter test works. Why? -It's normal. In this version of the proxy, the delegate call uses the function logic from the `Counter_V1` contract but doesn't use its variables, so the delegate call tries to access a `count` variable that doesn't exist in the proxy contract. +It's normal. In this version of the proxy, the delegate call uses the function logic from the `CounterV1` contract but doesn't use its variables, so the delegate call tries to access a `count` variable that doesn't exist in the proxy contract. So, add the `count` public variable, which is a `uint256`, before the `implem` variable in the proxy contract and initialize it to 0 in the constructor. Now, if you run the tests, all should work. +As you can see, a delegate call uses the proxy's memory and not the memory of the contract you're calling. So when the function you're calling uses a variable, it tries to find it in the memory of the proxy. If your contract has a single variable in memory, that variable will correspond to the first storage slot in the proxy's memory. But if the slot doesn't correspond to the variable used by the function, it won't work. +That's why you add the `count` variable first because it's the first variable in the `counterV1` contract. + ### ๐Ÿ“š **Documentation**: - [Foundry](https://book.getfoundry.sh/) @@ -106,94 +148,106 @@ Now, if you run the tests, all should work. ### ๐Ÿ“‘ **Description**: -Having a functional proxy is great, but we need to write all the variables of the implementation function to make it work. It's not very convenient. So, let's upgrade our proxy to make it more flexible. +Having a functional proxy is great, but we need to write all the variables of the implementation function to make it work. It's not very convenient. So, let's upgrade our proxy to make it more flexible. -In this step, you will add a `setImplementation` function to the proxy contract and we will explore storage slots. This function will allow you to change the implementation contract address dynamically. +In this version, there is no variable in memory, so you don't need to have the same variables in the same order as the implementation contract. +To store the implementation address, we will use a constant (which is not stored in memory) to store the slot address to save the implementation address. +This slot is defined in the ERC-1967 rules to be "eip1967.proxy.implementation". + +In this step, you will add a `setImplementation` function to the proxy contract and we will explore storage slots. This function will allow you to change the implementation contract address dynamically to be upgraded. ### ๐Ÿ“Œ **Tasks**: -- ๐Ÿ“„ Create a new file `proxy_v2.sol` in the `proxy` folder. -- Create a contract `PROXY_V2` in the `proxy_v2.sol` file. - - This contract will be an upgrade of the `PROXY_V1` contract. - - Copy only the fallback function from the `PROXY_V1` contract to the `PROXY_V2` contract. +- ๐Ÿ“„ Create a new file `ProxyV2.sol` in the `proxy` folder. +- Create a contract `ProxyV2` in the `ProxyV2.sol` file. + - This contract will be an upgrade of the `ProxyV1` contract. + - Copy only the fallback function from the `ProxyV1` contract to the `ProxyV2` contract. - Add the following line at the beginning of the contract: - ```solidity - // Define the storage slot for the implementation address - bytes32 private constant IMPLEMENTATION_SLOT = keccak256("proxy.implementation.address"); - ``` - - This line defines a storage slot for the implementation address. A storage slot is a unique identifier used to store data in the contract's storage. The `keccak256` function generates a unique identifier based on the string `"proxy.implementation.address"`. - - ๐ŸฅŠ Since there are no variables in the proxy contract (as the implementation address is stored in the contract's storage), we don't need to declare the variables from the implementation contract, thus avoiding conflicts between the variables of the proxy and the implementation. + ```solidity + // Define the storage slot for the implementation address + bytes32 private constant IMPLEMENTATION_SLOT = bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1); + ``` + - This line defines a storage slot for the implementation address. A storage slot is a unique identifier used to store data in the contract's storage. The `keccak256` function generates a unique identifier based on the string `"eip1967.proxy.implementation"`. + - The slot address is stored in a const which are written in the contract bytecode so their's no variable. + - ๐ŸฅŠ Since there are no variables in the proxy contract (as the implementation address is stored in the contract's storage), we don't need to declare the variables from the implementation contract, thus avoiding conflicts between the variables of the proxy and the implementation. - Add the following functions and use inline assembly code to modify or access the storage slot: - - `setImplementation(address newImplementation)`, which is a public function. - - This function will allow you to change the implementation contract address dynamically. - - It will use the `IMPLEMENTATION_SLOT` storage slot to store the new implementation address. - - `getImplementation()`, which is a public function. - - This function will return the address of the implementation contract stored in the `IMPLEMENTATION_SLOT` storage slot. + - `_setImplementation(address newImplementation)`, which is an internal function. + - This function will allow you to change the implementation contract address dynamically. + - It will use the `IMPLEMENTATION_SLOT` storage slot to store the new implementation address. + - `setImplementation(address newImplementation)`, which is a virtual public function (you need to put virtual for the next step). + - This function will call the `_setImplementation` function to set the implementation address. + - `getImplementation()`, which is a public function. + - This function will return the address of the implementation contract stored in the `IMPLEMENTATION_SLOT` storage slot. - Add the contract constructor and set the implementation address using the `setImplementation` function. - Modify the fallback function to use the `getImplementation` function to retrieve the implementation address. ### ๐Ÿ‘จโ€๐Ÿซ **Advice/Comments**: -- Use `if` or `require` statements to handle [error cases](https://docs.soliditylang.org/en/v0.8.21/contracts.html#errors-and-the-revert-statement). +- Use `if` or `require` statements to handle [error cases](https://docs.soliditylang.org/en/latest/contracts.html#errors-and-the-revert-statement). - Use [inline Assembly](https://docs.soliditylang.org/en/latest/assembly.html) to access the storage slot. ### โœ”๏ธ **Validation**: -Test the result by uncommenting the `testProxy_v2` function and the variables used by it in the `proxy.t.sol` file. +Add the `ProxyV2.t.sol` file, from the `utils` folder, in the `test` folder. This file contains the tests for the `ProxyV2` contract. + - In this test, we will change the implementation address and check if the fallback function works correctly. - As you can see in the logs, the count value doesn't reset to `0` when we change the implementation address. This is normal because the count value is stored in the proxy's storage, not in the implementation contract. ### ๐Ÿ“š **Documentation**: -- [Storage slots ๐Ÿ’พ](https://docs.soliditylang.org/en/v0.8.21/internals/layout_in_storage.html#layout-in-storage) +- [Storage slots ๐Ÿ’พ](https://docs.soliditylang.org/en/latest/internals/layout_in_storage.html#layout-in-storage) - [Inline Assembly โ›“๏ธ](https://docs.soliditylang.org/en/latest/assembly.html) ## Step 4 - Let's upgrade it again ๐Ÿ“ˆ ### ๐Ÿ“‘ **Description**: -Great, you now have a fully functional proxy. But we can improve it further. As it stands, anyone can call the `setImplementation` function and change the implementation contract address, which is not secure. Let's add access control to this function and a version management mechanism. +Great, you now have a fully functional proxy. But we can improve it further. As it stands, anyone can call the `setImplementation` function and change the implementation contract address, which is not secure. Let's add access control to this function by using a `modifier`. + +A modifier is a function-like construct used to run code before the function attached and revert before the called function is run if certain conditions are not met. It's mainly used to check the caller or the parameter values. ### ๐Ÿ“Œ **Tasks**: -- ๐Ÿ“„ First, create a new file `proxy_ownable_upgradable.sol` in the `proxy` folder. -- Create a contract `PROXY_OWNABLE_UPGRADABLE` in the `proxy_ownable_upgradable.sol` file. - - ๐Ÿ“ˆ This contract will be an upgrade of the `PROXY_V2` contract. - - To achieve this, the contract will inherit from the `PROXY_V2` contract. +- ๐Ÿ“„ First, create a new file `ProxyOwnableUpgradable` in the `proxy` folder. +- Create a contract `ProxyOwnableUpgradable` in the `ProxyOwnableUpgradable.sol` file. + - ๐Ÿ“ˆ This contract will be an upgrade of the `ProxyV2` contract. + - To achieve this, the contract will inherit from the `ProxyV2` contract. - ๐Ÿ’พ We need to store the `owner` address and the `version` which is a string. As learned in the previous step, we use storage slots to store these variables. - - Add public functions to get the owner and version values from the storage. - - Add internal functions to set the owner and version values in the storage. - - These functions must be called by other functions in the contract. + - The slot convention for the owner is `bytes32(uint256(keccak256("eip1967.proxy.owner")) - 1)`. + - Add public functions to get the owner and version values from the storage. + - Add internal functions to set the owner and version values in the storage. + - These functions must be called by other functions in the contract. - ๐Ÿ”Ž Now, we need events to log changes in the implementation address and ownership: - - `event Upgraded(string version, address indexed implementation);` - - `event ProxyOwnershipTransferred(address previousOwner, address newOwner);` - - Events are useful for debugging and keeping track of changes in the contract. + - `event Upgraded(string version, address indexed implementation);` + - `event ProxyOwnershipTransferred(address previousOwner, address newOwner);` + - Events are useful for debugging and keeping track of changes in the contract. + - Events are declared at the beginning of the contract. - Implement the following functions: - - `transferProxyOwnership(address newOwner)`, which is a public function. - - This function will allow the current owner to transfer ownership of the proxy contract to a new owner. - - This function will emit the `ProxyOwnershipTransferred` event and set the new owner in the storage. - - `upgradeTo(string memory newVersion, address newImplementation)`, which is a public function. - - This function will allow the owner to upgrade the implementation contract address and set a new version. - - This function will emit the `Upgraded` event and set the new implementation address and version in the storage. + - `transferProxyOwnership(address newOwner)`, which is a public function. + - This function will allow the current owner to transfer ownership of the proxy contract to a new owner. + - This function will emit the `ProxyOwnershipTransferred` event and set the new owner in the storage. + - `upgradeTo(string memory newVersion, address newImplementation)`, which is a public function. + - This function will allow the owner to upgrade the implementation contract address and set a new version. + - This function will emit the `Upgraded` event and set the new implementation address and version in the storage. - ๐Ÿ”จ Create a modifier `onlyOwner` to restrict access to certain functions to the owner of the contract. - - This modifier will use the `msg.sender` variable to check if the function caller is the owner of the contract. - - Add the `onlyOwner` modifier to the `transferProxyOwnership` and `upgradeTo` functions. + - This modifier will use the `msg.sender` variable to check if the function caller is the owner of the contract. + - Add the `onlyOwner` modifier to the `transferProxyOwnership` and `upgradeTo` functions. - ๐Ÿ› ๏ธ Add a constructor to set the contract owner. - - This constructor will use the `msg.sender` variable to set the contract owner. - - This constructor will also accept parameters to set the implementation address and the contract version. -- Finally, modify the fallback function to use the `getImplementation` function to retrieve the implementation address. + - This constructor will use the `msg.sender` variable to set the contract owner. + - This constructor will also accept parameters to set the implementation address and the contract version. +- ๐Ÿ“ Override the `setImplementation` function to be usable by the owner only with the `onlyOwner` modifier. ### ๐Ÿ“š **Documentation**: -- [Inheritance ๐Ÿ‘ถ](https://docs.soliditylang.org/en/v0.8.21/contracts.html#inheritance) -- [Events ๐ŸŽญ](https://docs.soliditylang.org/en/v0.8.21/contracts.html#events) -- [Access control โ›”๏ธ](https://docs.soliditylang.org/en/v0.8.21/contracts.html#access-control) -- [Modifiers ๐Ÿ”จ](https://docs.soliditylang.org/en/v0.8.21/contracts.html#modifiers) -- [Require โœ…](https://docs.soliditylang.org/en/v0.8.21/control-structures.html#require) +- [Inheritance ๐Ÿ‘ถ](https://docs.soliditylang.org/en/latest/contracts.html#inheritance) +- [Events ๐ŸŽญ](https://docs.soliditylang.org/en/latest/contracts.html#events) +- [Access control โ›”๏ธ](https://docs.soliditylang.org/en/latest/contracts.html#access-control) +- [Modifiers ๐Ÿ”จ](https://docs.soliditylang.org/en/latest/contracts.html#modifiers) +- [Require โœ…](https://docs.soliditylang.org/en/latest/control-structures.html#require) ### โœ”๏ธ **Validation**: -Uncomment all the functions and the variables in the `proxy.t.sol` file and run the tests with the command `forge test -vvvv`. All the tests should pass. +Add the `ProxyV3.t.sol` file, from the `utils` folder, in the `test` folder. This file contains the tests for the `ProxyOwnableUpgradable` contract. ## Step 5 - Let's deploy it ๐Ÿš€ @@ -207,12 +261,12 @@ In this step, you will deploy your ERC-1967 contract on the [Ethereum Sepolia Te - ๐Ÿ“ก Download [Metamask](https://metamask.io) and create an account if you don't already have one. It is the most popular Ethereum wallet and allows you to interact with the Ethereum blockchain. - Get your RPCURL on [Alchemy](https://dashboard.alchemy.com/apps). - - Sign in. - - Click on `Create new app` and enter the project name. - - Go to network, select `Ethereum`, change `mainnet` to `Sepolia`, and copy the URL. + - Sign in. + - Click on `Create new app` and enter the project name. + - Go to network, select `Ethereum`, change `mainnet` to `Sepolia`, and copy the URL. - Add the Sepolia Testnet network to Metamask. [Here's](https://moralis.io/how-to-add-the-sepolia-network-to-metamask-full-guide/) a guide on how to do it. - Go to the [Sepolia faucet](https://www.alchemy.com/faucets/ethereum-sepolia), enter your wallet address, and send yourself some ETH. -> โš ๏ธ You need some ETH on the mainnet to receive test tokens. If you don't have any, contact the workshop manager. + > โš ๏ธ You need some ETH on the mainnet to receive test tokens. If you don't have any, contact the workshop manager. - Copy the `.env` file from the `utils` folder to your project and fill in the variables except for the last one. #### Deployment of the ERC-1967 @@ -220,25 +274,29 @@ In this step, you will deploy your ERC-1967 contract on the [Ethereum Sepolia Te The setup is now complete. Let's move on to the interesting part: deploying our ERC-1967. We will use [foundry](https://book.getfoundry.sh/), specifically the [forge create](https://book.getfoundry.sh/forge/deploying) command to deploy the contract and the [cast](https://book.getfoundry.sh/cast/) command to interact with it. - Load the environment variables: + ```bash source .env ``` - Deploy your implementation contract: + ```bash -forge create src/implementation/Counter_v1.sol:COUNTER_V1 --private-key "$PRIVATE_KEY" --rpc-url $RPC_URL --legacy +forge create src/implementations/CounterV1.sol:CounterV1 --private-key "$PRIVATE_KEY" --rpc-url $RPC_URL ``` - Copy the address from `Deployed to` and paste it into the `.env` file under the `CONTRACT_V1` variable. - Load the environment variables again: + ```bash source .env ``` - Deploy your proxy contract: + ```bash -forge create src/proxy/proxy_ownable_upgradable.sol:PROXY_OWNABLE_UPGRADABLE --private-key "$PRIVATE_KEY" --rpc-url $RPC_URL --constructor-args "v1" "$CONTRACT_V1" --legacy +forge create src/proxys/ProxyOwnableUpgradable:ProxyOwnableUpgradable --private-key "$PRIVATE_KEY" --rpc-url $RPC_URL --constructor-args "v1" "$CONTRACT_V1" ``` - Copy the address from `Deployed to` and paste it into the `.env` file under the `PROXY` variable. @@ -246,34 +304,40 @@ forge create src/proxy/proxy_ownable_upgradable.sol:PROXY_OWNABLE_UPGRADABLE --p - Load the environment variables again. - Verify that the contract has been deployed correctly: + ```bash cast call $PROXY "total()" --rpc-url $RPC_URL ``` -> ๐Ÿ’ก This should display 0. + +> ๐Ÿ’ก This should display a lot of 0.
Try some interactions: - Add to the counter: + ```bash -cast call $PROXY "add()" --private-key $PRIVATE_KEY --rpc-url $RPC_URL +cast send $PROXY "add()" --private-key $PRIVATE_KEY --rpc-url $RPC_URL ``` - Change the implementation address: - - Deploy the new implementation contract `Counter_V2`. - - Copy the address from `Deployed to` and paste it into the `.env` file under the `CONTRACT_V2` variable. - - Load the environment variables again. - - Upgrade the implementation address: + - Deploy the new implementation contract `CounterV2`. + - Copy the address from `Deployed to` and paste it into the `.env` file under the `CONTRACT_V2` variable. + - Load the environment variables again. + - Upgrade the implementation address: + ```bash -cast call $PROXY "upgradeTo(address)" $CONTRACT_V2 --rpc-url $RPC_URL +cast send $PROXY "upgradeTo(address)" $CONTRACT_V2 --private-key $PRIVATE_KEY --rpc-url $RPC_URL ``` - Try the new implementation: + ```bash -cast call $PROXY "add(uint256)" 4 --rpc-url $RPC_URL +cast send $PROXY "add(uint256)" 4 --private-key $PRIVATE_KEY --rpc-url $RPC_URL cast call $PROXY "total()" --rpc-url $RPC_URL ``` + > ๐Ÿ’ก This should display 5 if you had already called `add()` once with the previous implementation. ### ๐Ÿ“š **Documentation**: @@ -287,21 +351,23 @@ cast call $PROXY "total()" --rpc-url $RPC_URL ## Conclusion ๐Ÿ -Congratulations! You've created your own proxy. During this workshop, you learned about ERC-1967 and built your own implementation. You can find a well-known implementation [here](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/Proxy.sol). Feel free to compare it with your implementation and consider adding security features to your contract. +Congratulations! You've created your own proxy. During this workshop, you learned about ERC-1967 and built your own implementation. You can find a well-known implementation [here](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/transparent/TransparentUpgradeableProxy.sol). Feel free to compare it with your implementation and consider adding security features to your contract. Thank you for completing this workshop! If you have any questions, don't hesitate to contact the PoC Team. ## To go further ๐Ÿ”ผ You have discovered what an ERC-1967 is, but there are still many other concepts to explore. Here are some examples: + - [ERC-20](https://eips.ethereum.org/EIPS/eip-20) **Token** with this PoC [workshop](https://github.com/PoCInnovation/Workshops/tree/master/p2p/5.Create_an_ERC-20) - [ERC-721](https://eips.ethereum.org/EIPS/eip-721) **NFT** - [ERC-1155](https://eips.ethereum.org/EIPS/eip-1155) **Multi Token** +- [UUPS Proxy](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/utils/UUPSUpgradeable.sol) ## Authors ๐Ÿ‘‹ | [
Lucas LECLERC](https://github.com/Intermarch3) | -| :-----------------------------------------------------------------------------------------------------------------: | +| :--------------------------------------------------------------------------------------------------------------------: |

Organization


diff --git a/p2p/6.Create_a_proxy/SETUP.md b/p2p/6.Create_a_proxy/SETUP.md index aafe9bf5..7cc6aa0b 100644 --- a/p2p/6.Create_a_proxy/SETUP.md +++ b/p2p/6.Create_a_proxy/SETUP.md @@ -67,7 +67,7 @@ Also, I recommand you to use the extension formatter. It will format your code o { "editor.formatOnSave": true, "[solidity]": { - "editor.defaultFormatter": "JuanBlanco.solidity" + "editor.defaultFormatter": "NomicFoundation.hardhat-solidity" } } ``` diff --git a/p2p/6.Create_a_proxy/help/fallback.md b/p2p/6.Create_a_proxy/help/fallback.md index c0f321d1..fb355ce4 100644 --- a/p2p/6.Create_a_proxy/help/fallback.md +++ b/p2p/6.Create_a_proxy/help/fallback.md @@ -1,10 +1,10 @@ -# ๐Ÿ™‹โ€โ™‚๏ธ Help to create the fallback function for the proxy +## Help to create the fallback function for the proxy The fallback function is a function that is called when a contract is called with a method that is not implemented. This function is called with the method name and the arguments that were passed to the proxy. ```solidity fallback() external payable { - (bool success, bytes memory returnData) = implementation.delegatecall(msg.data); + (bool success, bytes memory returnData) = implem.delegatecall(msg.data); if (!success) { if (returnData.length > 0) { assembly { @@ -21,10 +21,10 @@ fallback() external payable { } ``` -## โ›“๏ธโ€๐Ÿ’ฅ Let's break down the fallback function line by line: +### Let's break down the fallback function line by line: -1. `(bool success, bytes memory returnData) = implementation.delegatecall(msg.data);` - - This line calls the `delegatecall` function on the `implementation` contract with the `msg.data` that was sent to the proxy. The `delegatecall` function executes the code of the `implementation` contract in the context of the proxy contract. The `success` variable will be `true` if the `delegatecall` was successful, and the `returnData` variable will contain the return data from the `delegatecall`. +1. `(bool success, bytes memory returnData) = implemn.delegatecall(msg.data);` + - This line calls the `delegatecall` function on the implementation contract with the `msg.data` that was sent to the proxy. The `delegatecall` function executes the code of the implementation contract in the context of the proxy contract. The `success` variable will be `true` if the `delegatecall` was successful, and the `returnData` variable will contain the return data from the `delegatecall`. 2. `if (!success) {` - This line checks if the `delegatecall` was successful. If it was not successful, the code inside the `if` block will be executed. @@ -38,10 +38,6 @@ fallback() external payable { 4. `assembly { return(add(returnData, 32), mload(returnData)) }` - This line returns the `returnData` if the `delegatecall` was successful. The `returnData` contains the return value of the function that was called in the `implementation` contract. -๐Ÿ‘Œ Perfect now you have a fallback function that will call the `implementation` contract with the method name and arguments that were passed to the proxy. - -If the `delegatecall` is successful, it will return the return value of the function that was called in the `implementation` contract. If the `delegatecall` fails, it will revert with the revert reason. +Perfect now you have a fallback function that will call the `implementation` contract with the method name and arguments that were passed to the proxy. If the `delegatecall` is successful, it will return the return value of the function that was called in the `implementation` contract. If the `delegatecall` fails, it will revert with the revert reason. > โš ๏ธ It's the main logic of the proxy so if you have any questions feel free to ask to the PoC Team. - -โช Back to the [Workshop](../README.md). diff --git a/p2p/6.Create_a_proxy/utils/Counter_v1.sol b/p2p/6.Create_a_proxy/utils/CounterV1.sol similarity index 92% rename from p2p/6.Create_a_proxy/utils/Counter_v1.sol rename to p2p/6.Create_a_proxy/utils/CounterV1.sol index 5434317e..e7aa5174 100644 --- a/p2p/6.Create_a_proxy/utils/Counter_v1.sol +++ b/p2p/6.Create_a_proxy/utils/CounterV1.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.25; -contract COUNTER_V1 { +contract CounterV1 { uint256 public count; constructor() { diff --git a/p2p/6.Create_a_proxy/utils/Counter_v2.sol b/p2p/6.Create_a_proxy/utils/CounterV2.sol similarity index 92% rename from p2p/6.Create_a_proxy/utils/Counter_v2.sol rename to p2p/6.Create_a_proxy/utils/CounterV2.sol index 305ace60..d0283c1b 100644 --- a/p2p/6.Create_a_proxy/utils/Counter_v2.sol +++ b/p2p/6.Create_a_proxy/utils/CounterV2.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.25; -contract COUNTER_V2 { +contract CounterV2 { uint256 public _count; constructor() { diff --git a/p2p/6.Create_a_proxy/utils/ProxyV1.t.sol b/p2p/6.Create_a_proxy/utils/ProxyV1.t.sol new file mode 100644 index 00000000..8439661e --- /dev/null +++ b/p2p/6.Create_a_proxy/utils/ProxyV1.t.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import "forge-std/Test.sol"; +import "../src/proxys/ProxyV1.sol"; +import "../src/implementations/CounterV1.sol"; + +contract ProxyTest is Test { + ProxyV1 public proxy_v1; + CounterV1 public counter; + + function setUp() public { + counter = new CounterV1(); + proxy_v1 = new ProxyV1(address(counter)); + } + + function testCounter() public { + assertEq(counter.total(), 0, "Counter should be 0"); + counter.add(); + assertEq(counter.total(), 1, "Counter should be 1"); + } + + function testProxy_v1() public { + uint256 total = CounterV1(address(proxy_v1)).total(); + assertEq(total, 0, "total should be 0"); + CounterV1(address(proxy_v1)).add(); + total = CounterV1(address(proxy_v1)).total(); + assertEq(total, 1, "total should be 1"); + } +} \ No newline at end of file diff --git a/p2p/6.Create_a_proxy/utils/ProxyV2.t.sol b/p2p/6.Create_a_proxy/utils/ProxyV2.t.sol new file mode 100644 index 00000000..010ffb11 --- /dev/null +++ b/p2p/6.Create_a_proxy/utils/ProxyV2.t.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import "forge-std/Test.sol"; +import "../src/proxys/ProxyV2.sol"; +import "../src/implementations/CounterV1.sol"; +import "../src/implementations/CounterV2.sol"; + +contract ProxyTest is Test { + ProxyV2 public proxy_v2; + CounterV1 public counter; + CounterV2 public counter_v2; + + function setUp() public { + counter = new CounterV1(); + counter_v2 = new CounterV2(); + proxy_v2 = new ProxyV2(address(counter)); + } + + function testProxy_v2() public { + uint256 total = CounterV1(address(proxy_v2)).total(); + assertEq(total, 0, "total should be 0"); + CounterV1(address(proxy_v2)).add(); + total = CounterV1(address(proxy_v2)).total(); + assertEq(total, 1, "total should be 1"); + // change implementation + proxy_v2.setImplementation(address(counter_v2)); + CounterV2(address(proxy_v2)).add(2); + total = CounterV2(address(proxy_v2)).total(); + assertEq(total, 3, "total should be 3"); + } +} \ No newline at end of file diff --git a/p2p/6.Create_a_proxy/utils/ProxyV3.t.sol b/p2p/6.Create_a_proxy/utils/ProxyV3.t.sol new file mode 100644 index 00000000..b312e8a2 --- /dev/null +++ b/p2p/6.Create_a_proxy/utils/ProxyV3.t.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import "forge-std/Test.sol"; +import "../src/proxys/ProxyV1.sol"; +import "../src/proxys/ProxyV2.sol"; +import "../src/proxys/ProxyOwnableUpgradable.sol"; +import "../src/implementations/CounterV1.sol"; +import "../src/implementations/CounterV2.sol"; + +contract ProxyTest is Test { + CounterV1 public counter; + CounterV2 public counter_v2; + ProxyOwnableUpgradable public proxy_ownable_upgradable; + address public nonOwner; + + function setUp() public { + counter = new CounterV1(); + counter_v2 = new CounterV2(); + proxy_ownable_upgradable = new ProxyOwnableUpgradable(address(counter)); + nonOwner = address(0x1234); + } + + function testProxyUpgrade() public { + CounterV1(address(proxy_ownable_upgradable)).add(); + assertEq(CounterV1(address(proxy_ownable_upgradable)).total(), 1, "should be 1"); + proxy_ownable_upgradable.upgradeTo(address(counter_v2)); + assertEq(proxy_ownable_upgradable.getImplementation(), address(counter_v2), "implementation should be CounterV2"); + assertEq(CounterV2(address(proxy_ownable_upgradable)).total(), 1, "total should be 1"); + CounterV2(address(proxy_ownable_upgradable)).add(3); + assertEq(CounterV2(address(proxy_ownable_upgradable)).total(), 4, "total should be 4"); + } + + function testUpgradeToAsNonOwner() public { + // Attempt to upgrade implementation with a non-owner account + vm.prank(nonOwner); + vm.expectRevert("Caller is not the owner"); + proxy_ownable_upgradable.upgradeTo(address(counter_v2)); + assertEq(proxy_ownable_upgradable.getImplementation(), address(counter), "Implementation should not change for non-owner"); + } + + function testTransferProxyOwnership() public { + // Transfer ownership to nonOwner + proxy_ownable_upgradable.transferProxyOwnership(nonOwner); + assertEq(proxy_ownable_upgradable.getOwner(), nonOwner, "Owner should be nonOwner"); + } +} \ No newline at end of file diff --git a/p2p/6.Create_a_proxy/utils/proxy.t.sol b/p2p/6.Create_a_proxy/utils/proxy.t.sol deleted file mode 100644 index 289217e6..00000000 --- a/p2p/6.Create_a_proxy/utils/proxy.t.sol +++ /dev/null @@ -1,92 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.25; - -import "forge-std/Test.sol"; -import "../src/proxys/proxy_v1.sol"; -import "../src/proxys/proxy_v2.sol"; -import "../src/proxys/proxy_ownable_upgradable.sol"; -import "../src/implementations/Counter_v1.sol"; -import "../src/implementations/Counter_v2.sol"; - -contract ProxyTest is Test { - PROXY_V1 public proxy_v1; - PROXY_V2 public proxy_v2; - COUNTER_V1 public counter; - COUNTER_V2 public counter_v2; - PROXY_OWNABLE_UPGRADABLE public proxy_ownable_upgradable; - address public nonOwner; - - function setUp() public { - counter = new COUNTER_V1(); - counter_v2 = new COUNTER_V2(); - proxy_v1 = new PROXY_V1(address(counter)); - proxy_v2 = new PROXY_V2(address(counter)); - proxy_ownable_upgradable = new PROXY_OWNABLE_UPGRADABLE("v1", address(counter)); - nonOwner = address(0x1234); - } - - function testCounter() public { - assertEq(counter.total(), 0, "Counter should be 0"); - counter.add(); - assertEq(counter.total(), 1, "Counter should be 1"); - } - - function testProxy_v1() public { - uint256 total = COUNTER_V1(address(proxy_v1)).total(); - assertEq(total, 0, "total should be 0"); - COUNTER_V1(address(proxy_v1)).add(); - total = COUNTER_V1(address(proxy_v1)).total(); - assertEq(total, 1, "total should be 1"); - } - - function testProxy_v2() public { - uint256 total = COUNTER_V1(address(proxy_v2)).total(); - assertEq(total, 0, "total should be 0"); - COUNTER_V1(address(proxy_v2)).add(); - total = COUNTER_V1(address(proxy_v2)).total(); - assertEq(total, 1, "total should be 1"); - // change implementation - proxy_v2.setImplementation(address(counter_v2)); - COUNTER_V2(address(proxy_v2)).add(2); - total = COUNTER_V2(address(proxy_v2)).total(); - assertEq(total, 3, "total should be 3"); - } - - function testProxyUpgrade() public { - assertEq(fixNullBytes(proxy_ownable_upgradable.getVersion()), "v1", "version should be v1"); - COUNTER_V1(address(proxy_ownable_upgradable)).add(); - assertEq(COUNTER_V1(address(proxy_ownable_upgradable)).total(), 1, "should be 1"); - proxy_ownable_upgradable.upgradeTo("v2", address(counter_v2)); - assertEq(fixNullBytes(proxy_ownable_upgradable.getVersion()), "v2", "version should be v2"); - assertEq(COUNTER_V2(address(proxy_ownable_upgradable)).total(), 1, "total should be 1"); - COUNTER_V2(address(proxy_ownable_upgradable)).add(3); - assertEq(COUNTER_V2(address(proxy_ownable_upgradable)).total(), 4, "total should be 4"); - } - - function testUpgradeToAsNonOwner() public { - // Attempt to upgrade implementation with a non-owner account - vm.prank(nonOwner); - vm.expectRevert("Caller is not the owner"); - proxy_ownable_upgradable.upgradeTo("v2", address(counter_v2)); - assertEq(proxy_ownable_upgradable.implementation(), address(counter), "Implementation should not change for non-owner"); - } - - // remove null bytes from string - function fixNullBytes(string memory str) internal pure returns (string memory) { - bytes memory strBytes = bytes(str); - uint256 trimIndex = 0; - - // Find the position of the first null byte - while (trimIndex < strBytes.length && strBytes[trimIndex] != 0) { - trimIndex++; - } - - // Create a new string with the trimmed length - bytes memory trimmedBytes = new bytes(trimIndex); - for (uint256 i = 0; i < trimIndex; i++) { - trimmedBytes[i] = strBytes[i]; - } - - return string(trimmedBytes); - } -} \ No newline at end of file