Skip to content

Commit

Permalink
README, contract upgrades and script.
Browse files Browse the repository at this point in the history
  • Loading branch information
gotnoshoeson committed Jan 24, 2024
1 parent 1ce3d59 commit 15dcfc9
Show file tree
Hide file tree
Showing 10 changed files with 707 additions and 8 deletions.
110 changes: 109 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,117 @@
# 🏗 Scaffold-ETH 2 - Transparent Proxy Pattern

TL;DR - Transparent Upgradeable Proxy

Why do this? Why should you care? Short answer is upgradeable smart contracts. I know, this may be a bit taboo. Smart contracts are supposed to be immutable, isn't that the whole point? All of the proxy patterns that have been published by OpenZeppelin keep the immutable storage in tact and allow admins to add additional functionality without changing the contract address that the users interact with. For example, you have a smart contract that has a function that increments a value (++1), but now you want the contract to also have a decrement function (--1). An upgradeable contract pattern will allow you to do that and keep the smart contract's address and storage data in tact. So depending on the smart contract use, this could be an acceptable set of terms. What is this sorcery?

## Requirements

Before you begin, you need to install the following tools:

- [Node (>= v18.17)](https://nodejs.org/en/download/)
- Yarn ([v1](https://classic.yarnpkg.com/en/docs/install/) or [v2+](https://yarnpkg.com/getting-started/install))
- [Git](https://git-scm.com/downloads)

## Quickstart

To get started with Scaffold-ETH 2, follow the steps below:

1. Clone this repo & install dependencies

```
git clone https://github.com/scaffold-eth/scaffold-eth-2.git
cd scaffold-eth-2
yarn install
```

2. Run a local network in the first terminal:

```
yarn chain
```

This command starts a local Ethereum network using Hardhat. The network runs on your local machine and can be used for testing and development. You can customize the network configuration in `hardhat.config.ts`.

3. On a second terminal, deploy the test contract:

```
yarn deploy
```

This command deploys a test smart contract to the local network. The contract is located in `packages/hardhat/contracts` and can be modified to suit your needs. The `yarn deploy` command uses the deploy script located in `packages/hardhat/deploy` to deploy the contract to the network. You can also customize the deploy script.

4. On a third terminal, start your NextJS app:

```
yarn start
```

Visit your app on: `http://localhost:3000`. You can interact with your smart contract using the `Debug Contracts` page. You can tweak the app config in `packages/nextjs/scaffold.config.ts`.

<h4 align="center">
<a href="https://docs.scaffoldeth.io">Documentation</a> |
<a href="https://scaffoldeth.io">Website</a>
<a href="https://github.com/gotnoshoeson/se-clone-factory.git">Open Zeppelin Contract Docs</a>
</h4>

With that out of the way, onto the build!

## The build

** NOTE: This build is using OpenZeppelin contracts version 4.8.1. Let me know if you'd like to see a build with the latest OZ v5.0 TransparentUpgradeableProxy pattern. **

So why do this?

Contract upgradeability. Also, If you want to be able to deploy multiple instances of a smart contract and you want to reduce the cost of deployment.

We will start out with two smart contracts in this build:
-1: YourContract.sol
-2: ProxyFactory.sol

YourContract will be used as the implementation contract and ProxyFactory will be used as an on-chain way to deploy proxies of the implementation contract. All calls to the proxy contracts will be forwarded (delegatecall[delegatecall](https://solidity-by-example.org/delegatecall/)) to the implementation contract that contains the contract logic. The storage will be maintained in the proxy contract.

1. If you followed the steps previously, you can interact with YourContract and ProxyFactory on the typical Scaffold-Eth Debug page. Go ahead and choose the Factory contract and choose the createProxy method.

- Image here -

Create a few more so we can test them in our new page, Debug Proxies!

2. On the Debug Proxies page, select the contract you want to interact with by clicking on the 'Select Proxy Contract' dropdown menu. Set a new greeting and bam, Bob's your uncle!

What's that? The transaction failed? This is expected behavior as msg.sender was set as the admin (or owner) of the Proxy. The admin address can only call functions on the TransparentUpgradeableProxy functions, any other function calls will not fallback to the implementation contract if made by the admin. So let's log into the app with a different account.

To log in with a new burner wallet, simply open a new tab (you'll likely need to open a private or incognito tab in your browser to get a new burner address) and head to `http://localhost:3000/proxiesDebug`. Select a proxy contract and set a new greeting. It should work now! You can also check YourContract and see that the greeting has not been changed on this implementation contract. Cool. So we can create multiple copies of a contract and share the logic of one implementation contract for all proxy contracts. This alone is helpful because it reduces deployment costs. Onto the upgradeable part of the build.

## The contract upgrade

This may sound sacrilegious to you, afterall, smart contracts are supposed to be immutable right? There may be situations where you want to expand the functionality of your smart contract after you've already deployed it. In this case, the goal is to upgrade the contract by adding new functionality but retaining the data immutability; and this is exactly what all Proxy patterns are designed to do. For a deeper dive, check out this article from OpenZeppelin --> `https://blog.openzeppelin.com/proxy-patterns?utm_source=zos&utm_medium=blog&utm_campaign=transparent-proxy-pattern`

Every time that we created a new proxy, we did so with the Factory contract and not with our hardhat like the other contracts. For this reason we don't currently have the ABI to interact with any of the TransparentUpgradeableProxy functions or any of the functions that it inherits.

3. Let's deploy an upgraded version of YourContract and a YourTransparentUpgradeableProxy contract. In packages/hardhat/upgrade/contract, copy YourContract2.sol and YourTransparentUpgradeableProxy.sol to the packages/hardhat/contracts directory. In packages/hardhat/upgrade/deploy_script, copy 01_deploy_your_contract_upgrade.ts to the pacakges/hardhat/deploy directory. Hardhat will run the scripts found in this directory in the order of the numerical prefixes in the file names.

In the terminal, run:

```
yarn deploy
```

We didn't make any changes to YourContract or Factory so hardhat won't re-deploy those. The ABI for YourContract2 and YourTransparentUpgradeableProxy will be added to nextjs/contracts/deployedContracts to be used on the frontend. On the Debug Contracts page you should now see UI for all four contracts.




--- FOOTER ---

Now that you've learned the Transparent Upgradeable Proxy pattern, I have some bad news for you. This pattern is NOT the current recommended pattern for upgradeable proxy functionality. UUPS is now the pattern recommended by OpenZeppelin for this type of functionality. Read more about it here --> `https://docs.openzeppelin.com/contracts/4.x/api/proxy#transparent-vs-uups`.

Want to use the latest recommended proxy pattern? Stay tuned for a UUPS build coming soon...

Want to upgrade all proxies with one upgrade call? Stay tuned for an Upgradeable Beacon Proxy build coming soon...


<h4 align="center">
<a href="https://docs.scaffoldeth.io">Documentation</a> |
<a href="https://scaffoldeth.io">Website</a> |
<a href="https://blog.openzeppelin.com/the-transparent-proxy-pattern">Open Zeppelin Blog - The transparent proxy pattern</a>
</h4>

Expand Down
2 changes: 1 addition & 1 deletion packages/hardhat/contracts/Factory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ contract Factory {
}

function createProxy() public returns(address) {
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(implementation, msg.sender, "0x");
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(implementation, msg.sender, "");
proxyList.push(address(proxy));
return address(proxy);
}
Expand Down
3 changes: 2 additions & 1 deletion packages/hardhat/deploy/00_deploy_your_contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,11 @@ const deployYourContract: DeployFunction = async function (hre: HardhatRuntimeEn
log: true,
autoMine: true,
});

};

export default deployYourContract;

// Tags are useful if you have multiple deploy files and only want to run one of them.
// e.g. yarn deploy --tags YourContract
deployYourContract.tags = ["YourContract"];
deployYourContract.tags = ["YourContract", "Factory"];
123 changes: 123 additions & 0 deletions packages/hardhat/upgrade/contracts/YourContract2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
//SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;

// Useful for debugging. Remove when deploying to a live network.
import "hardhat/console.sol";

// Use openzeppelin to inherit battle-tested implementations (ERC20, ERC721, etc)
// import "@openzeppelin/contracts/access/Ownable.sol";

/**
* A smart contract that allows changing a state variable of the contract and tracking the changes
* It also allows the owner to withdraw the Ether in the contract
* @author BuidlGuidl
*/
contract YourContract2 {
// State Variables
address public immutable owner;
string public greeting = "Building Unstoppable Apps!!!";
bool public premium = false;
uint256 public totalCounter = 0;
mapping(address => uint) public userGreetingCounter;

string public farewell = "Go get 'em buidler!!!";
uint256 public totalCounterFarewell = 0;
mapping(address => uint) public userFarewellCounter;

// Events: a way to emit log statements from smart contract that can be listened to by external parties
event GreetingChange(
address indexed greetingSetter,
string newGreeting,
bool premium,
uint256 value
);

event FarewellChange(
address indexed farewellSetter,
string newFarewell,
bool premium,
uint256 value
);

// Constructor: Called once on contract deployment
// Check packages/hardhat/deploy/00_deploy_your_contract.ts
constructor(address _owner) {
owner = _owner;
}

// Modifier: used to define a set of rules that must be met before or after a function is executed
// Check the withdraw() function
modifier isOwner() {
// msg.sender: predefined variable that represents address of the account that called the current function
require(msg.sender == owner, "Not the Owner");
_;
}

/**
* Function that allows anyone to change the state variable "greeting" of the contract and increase the counters
*
* @param _newGreeting (string memory) - new greeting to save on the contract
*/
function setGreeting(string memory _newGreeting) public payable {
// Print data to the hardhat chain console. Remove when deploying to a live network.
console.log(
"Setting new greeting '%s' from %s",
_newGreeting,
msg.sender
);

// Change state variables
greeting = _newGreeting;
totalCounter += 1;
userGreetingCounter[msg.sender] += 1;

// msg.value: built-in global variable that represents the amount of ether sent with the transaction
if (msg.value > 0) {
premium = true;
} else {
premium = false;
}

// emit: keyword used to trigger an event
emit GreetingChange(msg.sender, _newGreeting, msg.value > 0, 0);
}

/**
* Function that allows the owner to withdraw all the Ether in the contract
* The function can only be called by the owner of the contract as defined by the isOwner modifier
*/
function withdraw() public isOwner {
(bool success, ) = owner.call{ value: address(this).balance }("");
require(success, "Failed to send Ether");
}

/**
* Function that allows the contract to receive ETH
*/
receive() external payable {}

function setFarewell(string memory _newFarewell) public payable {
// Print data to the hardhat chain console. Remove when deploying to a live network.
console.log(
"Setting new farwell '%s' from %s",
_newFarewell,
msg.sender
);

// Change state variables
farewell = _newFarewell;
totalCounterFarewell += 1;
userFarewellCounter[msg.sender] += 1;

// msg.value: built-in global variable that represents the amount of ether sent with the transaction
if (msg.value > 0) {
premium = true;
} else {
premium = false;
}

// emit: keyword used to trigger an event
emit FarewellChange(msg.sender, _newFarewell, msg.value > 0, 0);
}

}
16 changes: 16 additions & 0 deletions packages/hardhat/upgrade/contracts/YourProxyAdmin.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;

// Use openzeppelin to inherit battle-tested implementations (ERC20, ERC721, etc)
import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";

/**
* A smart contract so we can use the ABI in the frontend
* The TransparentUpgradeableProxy creates a ProxyAdmin on chain, we usually use hardhat to generate the ABI
* @author BuidlGuidl
*/
contract YourProxyAdmin is ProxyAdmin {

// Constructor: Called once on contract deployment
// Check packages/hardhat/deploy/01_deploy_your_contract_upgrade.ts
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { DeployFunction } from "hardhat-deploy/types";

/**
* Deploys a contract named "YourContract" using the deployer account and
* constructor arguments set to the deployer address
*
* @param hre HardhatRuntimeEnvironment object.
*/
const deployYourContract: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
/*
On localhost, the deployer account is the one that comes with Hardhat, which is already funded.
When deploying to live networks (e.g `yarn deploy --network goerli`), the deployer account
should have sufficient balance to pay for the gas fees for contract creation.
You can generate a random account with `yarn generate` which will fill DEPLOYER_PRIVATE_KEY
with a random private key in the .env file (then used on hardhat.config.ts)
You can run the `yarn account` command to check your balance in every network.
*/
const { deployer } = await hre.getNamedAccounts();
const { deploy } = hre.deployments;

await deploy("YourContract2", {
from: deployer,
// Contract constructor arguments
args: [deployer],
log: true,
// autoMine: can be passed to the deploy function to make the deployment process faster on local networks by
// automatically mining the contract deployment transaction. There is no effect on live networks.
autoMine: true,
});

await deploy("YourProxyAdmin", {
from: deployer,
// Contract constructor arguments
args: [],
log: true,
// autoMine: can be passed to the deploy function to make the deployment process faster on local networks by
// automatically mining the contract deployment transaction. There is no effect on live networks.
autoMine: true,
});
};

export default deployYourContract;

// Tags are useful if you have multiple deploy files and only want to run one of them.
// e.g. yarn deploy --tags YourContract
deployYourContract.tags = ["YourContract2", "YourProxyAdmin"];
4 changes: 2 additions & 2 deletions packages/nextjs/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ export const menuLinks: HeaderMenuLink[] = [
icon: <BugAntIcon className="h-4 w-4" />,
},
{
label: "Debug Clones",
href: "/clonesDebug",
label: "Debug Proxies",
href: "/proxiesDebug",
icon: <DocumentDuplicateIcon className="h-4 w-4" />,
},
];
Expand Down
Loading

0 comments on commit 15dcfc9

Please sign in to comment.