From b0a7d0448245dba0fb751cf60894618ec0ab7273 Mon Sep 17 00:00:00 2001 From: Lucas Leclerc Date: Tue, 6 Aug 2024 13:53:33 +0200 Subject: [PATCH 1/9] feat(p2p/proxy_workshop): add v1 of proxy workshop --- p2p/6.Create_a_proxy/README.md | 318 ++++++++++++++++++++++++++ p2p/6.Create_a_proxy/SETUP.md | 77 +++++++ p2p/6.Create_a_proxy/Solidity.md | 88 +++++++ p2p/6.Create_a_proxy/help/fallback.md | 43 ++++ 4 files changed, 526 insertions(+) create mode 100644 p2p/6.Create_a_proxy/README.md create mode 100644 p2p/6.Create_a_proxy/SETUP.md create mode 100644 p2p/6.Create_a_proxy/Solidity.md create mode 100644 p2p/6.Create_a_proxy/help/fallback.md diff --git a/p2p/6.Create_a_proxy/README.md b/p2p/6.Create_a_proxy/README.md new file mode 100644 index 00000000..2b88c88d --- /dev/null +++ b/p2p/6.Create_a_proxy/README.md @@ -0,0 +1,318 @@ +# Workshop - Create a proxy (ERC-1967) + +โœ” What is a proxy + +โœ” Make your own implementation of an ERC-1967 + +โœ” Deploy an ERC-1967 on Ethereum Sepolia Testnet + +## Introduction in a few lines + +If you have already heard about smart contracts, you know that it's not possible to update a contract once it's deployed. But what if you want to update a contract without changing its address? That's where the proxy comes in. + +### What is a proxy (ERC-1967)? + +ERC-1967 refers to the Ethereum Request for Comment 1967, which defines a proxy contract in the Ethereum blockchain ecosystem. A proxy contract acts as an intermediary between a user and the actual implementation contract. + +### 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. +- **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. + +### 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/). + +## 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. + +## Step 1 - Let's start creating our proxy + +### ๐Ÿ“‘ **Description**: + +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. + +### ๐Ÿ“Œ **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. + +> โš ๏ธ 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) +- [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 + +### ๐Ÿ“‘ **Description**: + +In this step, you will test the proxy contract you have just implemented. Testing the proxy contract is essential to ensure that the delegate call mechanism works correctly and that the proxy can interact with the implementation contract as expected. + +### ๐Ÿ“Œ **Tasks**: + +In the `proxy.t.sol` file in the `utils` folder, you will find the tests for the proxy contract. You will have to run these tests to verify that the proxy contract works correctly using the command `forge test -vvvv` (`-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. + +Now, 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. +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. + +### ๐Ÿ“š **Documentation**: + +- [Foundry](https://book.getfoundry.sh/) +- [Foundry tests](https://book.getfoundry.sh/forge/fuzz-testing) + +## Step 3 - Let's upgrade it + +### ๐Ÿ“‘ **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. +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. + +### ๐Ÿ“Œ **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. +- 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. +- 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. +- 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 [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. +- 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) +- [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. + +### ๐Ÿ“Œ **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. +- 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. +- 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. +- 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. +- 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. +- 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. + +### ๐Ÿ“š **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) + +### โœ”๏ธ **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. + +## Step 5 - Let's deploy it + +### ๐Ÿ“‘ **Description**: + +In this step, you will deploy your ERC-1967 contract on the [Ethereum Sepolia Testnet](https://www.alchemy.com/overviews/sepolia-testnet). Testing our contract on a testnet allows us to deploy and test our application on the Ethereum network without spending real money, as we use test tokens. Be careful; this step requires several tools. The first part of the tasks involves installing these tools, while the second part focuses on deploying the ERC-1967. + +### ๐Ÿ“Œ **Tasks**: + +#### Installation of the Tools + +- 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. +- 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. +- 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 + +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 +``` + +- 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 +``` + +- Copy the address from `Deployed to` and paste it into the `.env` file under the `PROXY` variable. + +- 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. + +
+ +Try some interactions: + +- Add to the counter: +```bash +cast call $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: +```bash +cast call $PROXY "upgradeTo(address)" $CONTRACT_V2 --rpc-url $RPC_URL +``` + +- Try the new implementation: +```bash +cast call $PROXY "add(uint256)" 4 --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**: + +- [Ethereum Sepolia Testnet](https://www.alchemy.com/overviews/sepolia-testnet) +- [Metamask](https://metamask.io) +- [Add Sepolia Testnet to Metamask](https://moralis.io/how-to-add-the-sepolia-network-to-metamask-full-guide/) +- [Sepolia faucet](https://www.alchemy.com/faucets/ethereum-sepolia) +- [Foundry deploy](https://book.getfoundry.sh/forge/deploying) +- [Cast command](https://book.getfoundry.sh/cast/) + +## 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. + +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** + +## Authors + +| [
Lucas LECLERC](https://github.com/Intermarch3) | +| :-----------------------------------------------------------------------------------------------------------------: | + +

Organization

+
+

+ + LinkedIn logo + + + Instagram logo + + + Twitter logo + + + Discord logo + +

+

+ + Website logo + +

+ +> ๐Ÿš€ Don't hesitate to follow us on our different platforms, and give a star ๐ŸŒŸ to PoC's repositories. diff --git a/p2p/6.Create_a_proxy/SETUP.md b/p2p/6.Create_a_proxy/SETUP.md new file mode 100644 index 00000000..4e59604f --- /dev/null +++ b/p2p/6.Create_a_proxy/SETUP.md @@ -0,0 +1,77 @@ +# Setup - Foundry & VSCode extension + +[Foundry](https://book.getfoundry.sh/) is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust. We will need it throughout the workshop. + +## Download foundry + +- Open your terminal and type + +```bash +curl -L https://foundry.paradigm.xyz | bash +``` + +This will download foundryup. + +- Then, you can download foundry by running `foundryup` +- If everything went fine you should be able to use `forge`, `anvil`, `chisel` and `cast`. +- If you are on macos you will need to install `libusb` with + +```bash +brew install libusb +``` + +After the installation, run the following command to ensure it has been properly installed on your computer: + +```bash +forge --version +``` + +It should print your current version. + +If you have some troubles during the installation, you can refer to the [official documentation](https://book.getfoundry.sh/getting-started/installation). + +## Create a foundry project + +Once everything is done, you can create a new project using + +```bash +forge init new_project +cd new_project +``` + +This should create a new directory with a brand new foundry project + +If you already have a repository, you might need to add + +```bash +--no-commit +``` + +The first thing you wants to do is set the solidity version of this project in the `foundry.toml` file wich is the configuration file of foundry. + +You can do this by adding in the "[profile.default]" section: + +```toml +solc_version = "0.8.25" +``` + +## VSCode Integration + +I recommand you to install [solidity vscode extension](https://marketplace.visualstudio.com/items?itemName=JuanBlanco.solidity), it is an extension that simplifies development in Solidity. + +Also, I recommand you to use the extension formatter. It will format your code on save, which allows you to have a clean codebase. To do so: + +- Create a `.vscode/settings.json` file with this content + +```json +{ + "editor.formatOnSave": true, + "[solidity]": { + "editor.defaultFormatter": "JuanBlanco.solidity" + } +} +``` + +## Back to the workshop + +[Jump !](./README.md) \ No newline at end of file diff --git a/p2p/6.Create_a_proxy/Solidity.md b/p2p/6.Create_a_proxy/Solidity.md new file mode 100644 index 00000000..f7849de9 --- /dev/null +++ b/p2p/6.Create_a_proxy/Solidity.md @@ -0,0 +1,88 @@ +# Solidity Essentials to create a proxy + +## Introduction + +**Solidity** is a **programming language** specifically **designed for developing smart contracts on the Ethereum blockchain**. Understanding Solidity is crucial for creating **proxys**. Here's a summary of the key Solidity concepts you'll need to know. + +## Contracts + +- **Contracts**: The building blocks of Ethereum smart contracts. + - [Solidity by Example - Hello World](https://solidity-by-example.org/hello-world/) + +## Data Types and State Variables + +- **Data Types**: Essential types include `uint`, `address`, `string`, and `bool`. + - [Solidity by Example - Data Types](https://solidity-by-example.org/primitives/) +- **State Variables**: Hold data across function calls, stored on the blockchain. + - [Solidity by Example - Variables](https://solidity-by-example.org/variables/) + +## Visibility Modifiers + +- **public**: Can be accessed internally and externally. +- **internal**: Accessible only within the contract and derived contracts. +- **external**: Can be called from outside the contract. +- **private**: Accessible only within the contract. + - [Solidity by Example - Visibility](https://solidity-by-example.org/visibility/) + +## Data Locations + +- **Storage**: Persistent data stored on the blockchain. +- **Memory**: Temporary data stored during function execution. +- **Stack**: Local variables stored in function execution context. + - [Solidity by Example - Data Locations](https://solidity-by-example.org/data-locations/) + +## Functions + +- **Functions**: Code blocks within contracts that execute specific tasks. + - [Solidity by Example - Functions](https://solidity-by-example.org/function/) + +## Constructor + +- The constructor initializes contract variables when the contract is deployed. + - [Solidity by Example - Constructor](https://solidity-by-example.org/constructor/) + +## Interface + +- **Interface**: Abstract contracts that define functions but don't provide implementations. + - [Solidity by Example - Interface](https://solidity-by-example.org/interface/) + +## Modifiers + - **Modifiers**: Reusable code blocks that can change the behavior of functions. + - [Solidity by Example - Modifiers](https://solidity-by-example.org/function-modifier/) + +## Inheritance + +- **Inheritance**: Contracts can inherit properties and methods from other contracts. + - [Solidity by Example - Inheritance](https://solidity-by-example.org/inheritance/) + +## Error Handling + +- Use `require` and `revert` to handle error cases and provide useful feedback. + - [Solidity by Example - Error Handling](https://solidity-by-example.org/error/) + +## Events + +- **Events**: Enable logging of important contract actions for external consumption. + - [Solidity by Example - Events](https://solidity-by-example.org/events/) + +## DelegateCall + +- **DelegateCall**: Allows a contract to execute code from another contract. + - [Solidity by Example - DelegateCall](https://solidity-by-example.org/delegatecall/) + +## Units + +- **Units**: Ether and Wei are the primary units of Ethereum. 1 ether = 10^18 wei. + - [Solidity by Example - Units](https://solidity-by-example.org/ether-units/) + +## Solidity by Example + +- To find more examples of practical Solidity implementations, explore [Solidity by Example](https://solidity-by-example.org/). + +## Conclusion + +Mastering these Solidity essentials will empower you to create your own proxy with confidence and understanding. Dive into the documentation, experiment with code, and explore real-world examples to solidify your knowledge. + +## Back to the workshop + +[Jump !](./README.md) \ No newline at end of file diff --git a/p2p/6.Create_a_proxy/help/fallback.md b/p2p/6.Create_a_proxy/help/fallback.md new file mode 100644 index 00000000..7a1aad96 --- /dev/null +++ b/p2p/6.Create_a_proxy/help/fallback.md @@ -0,0 +1,43 @@ +## 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); + if (!success) { + if (returnData.length > 0) { + assembly { + let returndata_size := mload(returnData) + revert(add(32, returnData), returndata_size) + } + } else { + revert("Delegatecall failed without reason"); + } + } + assembly { + return(add(returnData, 32), mload(returnData)) + } +} +``` + +### 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`. + +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. + +3. `if (returnData.length > 0) {` + - This line checks if the `returnData` has a length greater than 0. If it does, it means that the `delegatecall` failed with a revert reason. + - The `assembly` block extracts the revert reason from the `returnData` and reverts with that reason. + - If the `returnData` is empty, it means that the `delegatecall` failed without a revert reason, and the fallback function reverts with a generic message. + - We use `assembly` to handle the revert reason because the `delegatecall` does not automatically propagate the revert reason. + +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. + +> โš ๏ธ It's the main logic of the proxy so if you have any questions feel free to ask to the PoC Team. From d3b2bf473ef019f2fdeb8e838e0add80a445c442 Mon Sep 17 00:00:00 2001 From: Lucas Leclerc Date: Tue, 6 Aug 2024 13:58:12 +0200 Subject: [PATCH 2/9] style(p2p/proxy_workshop): fix readme style errors --- p2p/6.Create_a_proxy/SETUP.md | 2 +- p2p/6.Create_a_proxy/Solidity.md | 2 +- p2p/6.Create_a_proxy/help/fallback.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/p2p/6.Create_a_proxy/SETUP.md b/p2p/6.Create_a_proxy/SETUP.md index 4e59604f..358abf03 100644 --- a/p2p/6.Create_a_proxy/SETUP.md +++ b/p2p/6.Create_a_proxy/SETUP.md @@ -74,4 +74,4 @@ Also, I recommand you to use the extension formatter. It will format your code o ## Back to the workshop -[Jump !](./README.md) \ No newline at end of file +[Jump !](./README.md) diff --git a/p2p/6.Create_a_proxy/Solidity.md b/p2p/6.Create_a_proxy/Solidity.md index f7849de9..571baddf 100644 --- a/p2p/6.Create_a_proxy/Solidity.md +++ b/p2p/6.Create_a_proxy/Solidity.md @@ -85,4 +85,4 @@ Mastering these Solidity essentials will empower you to create your own proxy wi ## Back to the workshop -[Jump !](./README.md) \ No newline at end of file +[Jump !](./README.md) diff --git a/p2p/6.Create_a_proxy/help/fallback.md b/p2p/6.Create_a_proxy/help/fallback.md index 7a1aad96..fbb2c1bd 100644 --- a/p2p/6.Create_a_proxy/help/fallback.md +++ b/p2p/6.Create_a_proxy/help/fallback.md @@ -1,4 +1,4 @@ -## 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. From 395b0a267b845fbf7093ceb73390a1b99aeb9d38 Mon Sep 17 00:00:00 2001 From: Lucas Leclerc Date: Tue, 6 Aug 2024 13:59:43 +0200 Subject: [PATCH 3/9] style(p2p/proxy_workshop): fix readme style errors again --- p2p/6.Create_a_proxy/help/fallback.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/p2p/6.Create_a_proxy/help/fallback.md b/p2p/6.Create_a_proxy/help/fallback.md index fbb2c1bd..783e26b6 100644 --- a/p2p/6.Create_a_proxy/help/fallback.md +++ b/p2p/6.Create_a_proxy/help/fallback.md @@ -21,7 +21,7 @@ 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`. From a464513154d657ba286a4266f3d9004878f85e0b Mon Sep 17 00:00:00 2001 From: Lucas Leclerc Date: Tue, 6 Aug 2024 14:30:03 +0200 Subject: [PATCH 4/9] feat(p2p/proxy_workshop): add utils --- p2p/6.Create_a_proxy/utils/Counter_v1.sol | 18 +++++ p2p/6.Create_a_proxy/utils/Counter_v2.sol | 18 +++++ p2p/6.Create_a_proxy/utils/proxy.t.sol | 92 +++++++++++++++++++++++ 3 files changed, 128 insertions(+) create mode 100644 p2p/6.Create_a_proxy/utils/Counter_v1.sol create mode 100644 p2p/6.Create_a_proxy/utils/Counter_v2.sol create mode 100644 p2p/6.Create_a_proxy/utils/proxy.t.sol diff --git a/p2p/6.Create_a_proxy/utils/Counter_v1.sol b/p2p/6.Create_a_proxy/utils/Counter_v1.sol new file mode 100644 index 00000000..5434317e --- /dev/null +++ b/p2p/6.Create_a_proxy/utils/Counter_v1.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +contract COUNTER_V1 { + uint256 public count; + + constructor() { + count = 0; + } + + function add() public { + count += 1; + } + + function total() public view returns (uint256) { + return count; + } +} \ No newline at end of file diff --git a/p2p/6.Create_a_proxy/utils/Counter_v2.sol b/p2p/6.Create_a_proxy/utils/Counter_v2.sol new file mode 100644 index 00000000..305ace60 --- /dev/null +++ b/p2p/6.Create_a_proxy/utils/Counter_v2.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +contract COUNTER_V2 { + uint256 public _count; + + constructor() { + _count = 0; + } + + function add(uint8 nb) public { + _count += nb; + } + + function total() public view returns (uint256) { + return _count; + } +} \ 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 new file mode 100644 index 00000000..289217e6 --- /dev/null +++ b/p2p/6.Create_a_proxy/utils/proxy.t.sol @@ -0,0 +1,92 @@ +// 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 From 63ec01e240f0e2a6159b84cf22868fe8c9f3627c Mon Sep 17 00:00:00 2001 From: Lucas Leclerc Date: Tue, 6 Aug 2024 14:34:51 +0200 Subject: [PATCH 5/9] feat(p2p/proxy_workshop): add .env example --- p2p/6.Create_a_proxy/utils/.env.sample | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 p2p/6.Create_a_proxy/utils/.env.sample diff --git a/p2p/6.Create_a_proxy/utils/.env.sample b/p2p/6.Create_a_proxy/utils/.env.sample new file mode 100644 index 00000000..eb246e5c --- /dev/null +++ b/p2p/6.Create_a_proxy/utils/.env.sample @@ -0,0 +1,11 @@ +# This is the private key of your account, you can get it in metamask +PRIVATE_KEY= +# This is the address of your account, the one associated with your private key +ADMIN_ADDRESS= +# Alchemy node url for ethereum sepolia network +RPC_URL= +# The address of your deployed ERC-1967 contract +PROXY= +# The address of the counter contract +CONTRACT_V1= +CONTRACT_V2= \ No newline at end of file From 9e97f9eaa2dd883fe6495a3c89942b9013f0b01b Mon Sep 17 00:00:00 2001 From: Lucas Leclerc Date: Wed, 7 Aug 2024 13:43:40 +0200 Subject: [PATCH 6/9] feat(p2p/proxy_workshop): add emoji --- p2p/6.Create_a_proxy/README.md | 100 ++++++++++++++------------ p2p/6.Create_a_proxy/SETUP.md | 8 +-- p2p/6.Create_a_proxy/help/fallback.md | 11 ++- 3 files changed, 67 insertions(+), 52 deletions(-) diff --git a/p2p/6.Create_a_proxy/README.md b/p2p/6.Create_a_proxy/README.md index 2b88c88d..c0ed287a 100644 --- a/p2p/6.Create_a_proxy/README.md +++ b/p2p/6.Create_a_proxy/README.md @@ -1,10 +1,10 @@ # Workshop - Create a proxy (ERC-1967) -โœ” What is a proxy +โœ” ๐Ÿ“– What is a proxy -โœ” Make your own implementation of an ERC-1967 +โœ” ๐Ÿ› ๏ธ Make your own implementation of an ERC-1967 -โœ” Deploy an ERC-1967 on Ethereum Sepolia Testnet +โœ” ๐Ÿš€ Deploy an ERC-1967 on Ethereum Sepolia Testnet ## Introduction in a few lines @@ -12,34 +12,34 @@ If you have already heard about smart contracts, you know that it's not possible ### What is a proxy (ERC-1967)? -ERC-1967 refers to the Ethereum Request for Comment 1967, which defines a proxy contract in the Ethereum blockchain ecosystem. A proxy contract acts as an intermediary between a user and the actual implementation contract. +๐Ÿค“ Hum Actually, ERC-1967 refers to the Ethereum Request for Comment 1967, which defines a proxy contract in the Ethereum blockchain ecosystem. A proxy contract acts as an intermediary between a user and the actual implementation contract. ### 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. -- **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. +- **Upgradability**๐Ÿ“ˆ: The ability to upgrade the implementation contract without changing the contract's address or user interactions. +- **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. ### 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/v0.8.21/). -## Step 0 - Setup +## 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. +- ๐Ÿ“‚ 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. +- ๐Ÿ—‘๏ธ 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. -## Step 1 - Let's start creating our proxy +## Step 1 - Let's start creating our proxy ๐Ÿ ### ๐Ÿ“‘ **Description**: @@ -60,18 +60,18 @@ In this step, you will implement the delegate call mechanism, which is the basis - You need to use inline assembly at one time in this function. > โš ๏ธ 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. +> ๐Ÿ™‹โ€โ™‚๏ธ 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) -- [Inline Assembly](https://docs.soliditylang.org/en/latest/assembly.html) +- [Constructor ๐Ÿ› ๏ธ](https://docs.soliditylang.org/en/v0.8.21/contracts.html#constructor) +- [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) +- [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 +## Step 2 - Let's test if it works ๐Ÿงช ### ๐Ÿ“‘ **Description**: @@ -79,12 +79,21 @@ 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. You will have to run these tests to verify that the proxy contract works correctly using the command `forge test -vvvv` (`-vvvv` adds more details). +In the `proxy.t.sol` file in the `utils` folder, you will find 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 +``` +- `-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. Now, 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. + 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. @@ -93,16 +102,17 @@ Now, if you run the tests, all should work. - [Foundry](https://book.getfoundry.sh/) - [Foundry tests](https://book.getfoundry.sh/forge/fuzz-testing) -## Step 3 - Let's upgrade it +## Step 3 - Let's upgrade it ๐Ÿ“ˆ ### ๐Ÿ“‘ **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. + 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. ### ๐Ÿ“Œ **Tasks**: -- Create a new file `proxy_v2.sol` in the `proxy` folder. +- ๐Ÿ“„ 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. @@ -112,7 +122,7 @@ In this step, you will add a `setImplementation` function to the proxy contract 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. + - ๐ŸฅŠ 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. @@ -131,14 +141,14 @@ In this step, you will add a `setImplementation` function to the proxy contract Test the result by uncommenting the `testProxy_v2` function and the variables used by it in the `proxy.t.sol` file. - 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. +- 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) -- [Inline Assembly](https://docs.soliditylang.org/en/latest/assembly.html) +- [Storage slots ๐Ÿ’พ](https://docs.soliditylang.org/en/v0.8.21/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 +## Step 4 - Let's upgrade it again ๐Ÿ“ˆ ### ๐Ÿ“‘ **Description**: @@ -146,15 +156,15 @@ Great, you now have a fully functional proxy. But we can improve it further. As ### ๐Ÿ“Œ **Tasks**: -- First, create a new file `proxy_ownable_upgradable.sol` in the `proxy` folder. +- ๐Ÿ“„ 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. + - ๐Ÿ“ˆ This contract will be an upgrade of the `PROXY_V2` contract. - To achieve this, the contract will inherit from the `PROXY_V2` 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. +- ๐Ÿ’พ 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. -- Now, we need events to log changes in the implementation address and ownership: +- ๐Ÿ”Ž 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. @@ -165,27 +175,27 @@ Great, you now have a fully functional proxy. But we can improve it further. As - `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. +- ๐Ÿ”จ 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. -- Add a constructor to set the contract owner. +- ๐Ÿ› ๏ธ 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. ### ๐Ÿ“š **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/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) ### โœ”๏ธ **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. -## Step 5 - Let's deploy it +## Step 5 - Let's deploy it ๐Ÿš€ ### ๐Ÿ“‘ **Description**: @@ -195,7 +205,7 @@ In this step, you will deploy your ERC-1967 contract on the [Ethereum Sepolia Te #### Installation of the Tools -- 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. +- ๐Ÿ“ก 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. @@ -275,20 +285,20 @@ cast call $PROXY "total()" --rpc-url $RPC_URL - [Foundry deploy](https://book.getfoundry.sh/forge/deploying) - [Cast command](https://book.getfoundry.sh/cast/) -## Conclusion +## 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. Thank you for completing this workshop! If you have any questions, don't hesitate to contact the PoC Team. -## To go further +## 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** -## Authors +## Authors ๐Ÿ‘‹ | [
Lucas LECLERC](https://github.com/Intermarch3) | | :-----------------------------------------------------------------------------------------------------------------: | diff --git a/p2p/6.Create_a_proxy/SETUP.md b/p2p/6.Create_a_proxy/SETUP.md index 358abf03..aafe9bf5 100644 --- a/p2p/6.Create_a_proxy/SETUP.md +++ b/p2p/6.Create_a_proxy/SETUP.md @@ -1,8 +1,8 @@ -# Setup - Foundry & VSCode extension +# ๐Ÿ’ป Setup - Foundry & VSCode extension [Foundry](https://book.getfoundry.sh/) is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust. We will need it throughout the workshop. -## Download foundry +## ๐Ÿ“ก Download foundry - Open your terminal and type @@ -39,7 +39,7 @@ forge init new_project cd new_project ``` -This should create a new directory with a brand new foundry project +๐Ÿ“‚ This should create a new directory with a brand new foundry project If you already have a repository, you might need to add @@ -55,7 +55,7 @@ You can do this by adding in the "[profile.default]" section: solc_version = "0.8.25" ``` -## VSCode Integration +## ๐Ÿงฉ VSCode Integration I recommand you to install [solidity vscode extension](https://marketplace.visualstudio.com/items?itemName=JuanBlanco.solidity), it is an extension that simplifies development in Solidity. diff --git a/p2p/6.Create_a_proxy/help/fallback.md b/p2p/6.Create_a_proxy/help/fallback.md index 783e26b6..bc926ad7 100644 --- a/p2p/6.Create_a_proxy/help/fallback.md +++ b/p2p/6.Create_a_proxy/help/fallback.md @@ -1,4 +1,4 @@ -# 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. @@ -21,7 +21,7 @@ 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`. @@ -38,6 +38,11 @@ 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). +``` From ea0e944517472f6ecfaccd97e7aaf6316d120a73 Mon Sep 17 00:00:00 2001 From: Lucas Leclerc Date: Wed, 7 Aug 2024 13:46:05 +0200 Subject: [PATCH 7/9] style(p2p/proxy_workshop): fix style --- p2p/6.Create_a_proxy/help/fallback.md | 1 - 1 file changed, 1 deletion(-) diff --git a/p2p/6.Create_a_proxy/help/fallback.md b/p2p/6.Create_a_proxy/help/fallback.md index bc926ad7..c0f321d1 100644 --- a/p2p/6.Create_a_proxy/help/fallback.md +++ b/p2p/6.Create_a_proxy/help/fallback.md @@ -45,4 +45,3 @@ If the `delegatecall` is successful, it will return the return value of the func > โš ๏ธ 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). -``` From 58f288b5aa64bf50c23a358af0584af492bf64d3 Mon Sep 17 00:00:00 2001 From: Lucas Leclerc Date: Mon, 2 Sep 2024 18:52:18 +0200 Subject: [PATCH 8/9] 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 From 8d89e02edbe1ae19999c22a1722bfbb69eb55caa Mon Sep 17 00:00:00 2001 From: Lucas Leclerc Date: Mon, 2 Sep 2024 21:37:37 +0200 Subject: [PATCH 9/9] fix(p2p/proxy_workshop): fix readme markdown --- p2p/6.Create_a_proxy/help/fallback.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/p2p/6.Create_a_proxy/help/fallback.md b/p2p/6.Create_a_proxy/help/fallback.md index fb355ce4..d0c79b3c 100644 --- a/p2p/6.Create_a_proxy/help/fallback.md +++ b/p2p/6.Create_a_proxy/help/fallback.md @@ -1,4 +1,4 @@ -## 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. @@ -21,7 +21,7 @@ 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) = 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`.