Uses Foundry and OpenZeppelin's Foundry Upgrades library to create a demonstration of the Transparent Proxy pattern.
This example was created with version 1.3.2-stable of Foundry and v5 of OpenZeppelin Contracts.
Step 1: Install Foundry if needed (https://getfoundry.sh):
From a terminal session:
curl -L https://foundry.paradigm.xyz | bash
foundryupStep 2: Initialize the project:
forge init my-proxy-project
cd my-proxy-projectStep 3: Install OpenZeppelin as a dependency:
forge install OpenZeppelin/openzeppelin-foundry-upgrades
forge install OpenZeppelin/openzeppelin-contracts-upgradeableStep 4: Define remappings
Create a remappings.txt file in your project and add the following lines:
@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/
@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/
Step 5: Customize foundry settings for OpenZeppelin library
Open foundry.toml and change to the following:
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
ffi = true
ast = true
build_info = true
extra_output = ["storageLayout"]Step 6: Compile to verify
forge buildStep 7: Run a local node
Open a new terminal tab or window and start a local node:
anvilThis runs a local blockchain at http://127.0.0.0.1:8545 with test accounts
(use private key
0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 by default)
Open the generated src/Counter.sol file. Change it to the following.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.29;
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract Counter is Initializable {
uint256 public number;
function initialize(uint256 initialValue) public initializer {
number = initialValue;
}
function setNumber(uint256 newNumber) public {
number = newNumber;
}
function increment() public {
number++;
}
}This extends the Counter contract as Initializable and creates a one-time use initialize method that allows the Counter to be initialized with an arbitrary number when it's being deployed. Constructors cannot be used in implementation contracts when they sit behind a proxy.
Run forge build to ensure it compiles correctly.
Create script/Deploy.s.sol.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.29;
import {Script} from "forge-std/Script.sol";
import {console} from "forge-std/console.sol";
import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol";
import {Counter} from "src/Counter.sol";
contract Deploy is Script {
function run() external {
vm.startBroadcast();
address proxy = Upgrades.deployTransparentProxy(
"Counter.sol",
msg.sender,
abi.encodeCall(Counter.initialize, (0))
);
console.log("Proxy address:", proxy);
Counter counter = Counter(address(proxy));
counter.increment();
console.log("Count after: increment:", counter.number());
vm.stopBroadcast();
}
}This script utilizes the OpenZeppelin Foundry Upgrades library to deploy the
Counter.sol implementation contract, sets the admin as msg.sender, then
calls the Counter.initialize method to set its initial number. It then logs
the Proxy address and the modified count to the terminal (along with other
transaction metadata).
Compile your project by running forge clean && forge build.
To run the script:
forge script script/Deploy.s.sol --rpc-url http://127.0.0.1:8545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --broadcastIt will log out a lot of transaction metadata. Scroll up to the == Logs ==
section and observe the values as:
== Logs ==
npm warn exec The following package was not found and will be installed: @openzeppelin/upgrades-core@1.44.1
Proxy address: {the address your proxy was deployed to}
Count after: increment: 1Create src/CounterV2.sol and append a new function. Here we are adding
multBy to multiply the count by some value.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.29;
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract CounterV2 is Initializable {
uint256 public number;
function initialize(uint256 initialValue) public initializer {
number = initialValue;
}
function setNumber(uint256 newNumber) public {
number = newNumber;
}
function increment() public {
number++;
}
function multBy(uint256 value) public {
number *= value;
}
}Create script/Upgrade.s.sol.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.29;
import {Script} from "forge-std/Script.sol";
import {console} from "forge-std/console.sol";
import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol";
import {Options} from "openzeppelin-foundry-upgrades/Options.sol";
import {CounterV2} from "src/CounterV2.sol";
contract Upgrade is Script {
// Change to the Proxy address logged from Deploy.s.sol
address constant PROXY_ADDRESS = 0xYourProxyAddressHere;
function run() external {
vm.startBroadcast();
Options memory opts;
opts.referenceContract = "Counter.sol";
Upgrades.upgradeProxy(PROXY_ADDRESS, "CounterV2.sol", "", opts);
// Test
CounterV2 counter = CounterV2(PROXY_ADDRESS);
console.log("Current count:", counter.number());
counter.multBy(3);
counter.multBy(3);
console.log("Counter after multiplying by 3 twice:", counter.number());
vm.stopBroadcast();
}
}Run forge clean && forge build, then:
forge script script/Upgrade.s.sol --rpc-url http://127.0.0.1:8545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --broadcastThis upgrades the implementation, keeping the proxy address and state.