Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(p2p/proxy_workshop): add v1 of proxy workshop #96

Merged
merged 9 commits into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
328 changes: 328 additions & 0 deletions p2p/6.Create_a_proxy/README.md

Large diffs are not rendered by default.

77 changes: 77 additions & 0 deletions p2p/6.Create_a_proxy/SETUP.md
Original file line number Diff line number Diff line change
@@ -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"
Intermarch3 marked this conversation as resolved.
Show resolved Hide resolved
}
}
```

## Back to the workshop

[Jump !](./README.md)
88 changes: 88 additions & 0 deletions p2p/6.Create_a_proxy/Solidity.md
Original file line number Diff line number Diff line change
@@ -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)
47 changes: 47 additions & 0 deletions p2p/6.Create_a_proxy/help/fallback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# 🙋‍♂️ 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.

⏪ Back to the [Workshop](../README.md).
11 changes: 11 additions & 0 deletions p2p/6.Create_a_proxy/utils/.env.sample
Original file line number Diff line number Diff line change
@@ -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=
18 changes: 18 additions & 0 deletions p2p/6.Create_a_proxy/utils/Counter_v1.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
18 changes: 18 additions & 0 deletions p2p/6.Create_a_proxy/utils/Counter_v2.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
92 changes: 92 additions & 0 deletions p2p/6.Create_a_proxy/utils/proxy.t.sol
Original file line number Diff line number Diff line change
@@ -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");
Intermarch3 marked this conversation as resolved.
Show resolved Hide resolved
}

Intermarch3 marked this conversation as resolved.
Show resolved Hide resolved
// 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);
}
}
Loading