Skip to main content

2 posts tagged with "versioning"

View All Tags

How to manage versions in upgradeable smart contracts

· 6 min read
Abhijeet Bhagat
Enscribe Senior Engineer

Proxy and implementation contracts visual

In our recent guide on smart contract versioning, we explored how smart contracts can be versioned and suggested a few ideas on how to integrate contract versioning in your existing deployment pipelines. This post is a sequel explaining how similar ideas can be applied when you have upgradeable contracts.

In smart contract engineering, the proxy pattern is a standard for smart contract upgradability. By separating the proxy (stable user-facing abstraction) from the implementation (the upgradeable logic), developers can ship improvements without breaking contract calls on the clients because the address changed.

Whilst naming the implementation contract is awesome, what happens if you just name either the proxy contract or the implementation contract? It introduces a ‘transparency problem’ — without naming both the contracts, figuring out which contract does what and which version of the logic is currently active behind a proxy becomes a headache for users, developers and auditors.

proxy

A simple proxy and implementation contract

How can a proxy contract differentiate a transaction that is meant to upgrade the implementation address it points to from a transaction that is meant to interact with it?

TransparentUpgradeableProxy pattern ensures that only the admin can upgrade the implementation whilst for normal users, it ‘transparently’ forwards the transaction to the implementation contract, executing the logic, as if the proxy wasn’t there (hence the name).

Let’s first see how the TransparentUpgradeableProxy proxy contract from OpenZeppelin that uses the transparent upgradeable proxy pattern looks like.

We have a constructor that configures the proxy admin and the implementation address:

constructor(address _logic, address initialOwner, bytes memory _data) payable ERC1967Proxy(_logic, _data) {
_admin = address(new ProxyAdmin(initialOwner));

// Set the storage value and emit an event for ERC-1967 compatibility
ERC1967Utils.changeAdmin(_proxyAdmin());
}

The _fallback function implements differentiating an admin trying to upgrade the contract from other accounts trying to call the implementation’s logic:

function _fallback() internal virtual override {
if (msg.sender == _proxyAdmin()) {
if (msg.sig != ITransparentUpgradeableProxy.upgradeToAndCall.selector) {
revert ProxyDeniedAdminAccess();
} else {
_dispatchUpgradeToAndCall();
}
} else {
super._fallback();
}
}

A proxy admin is only allowed to upgrade the proxy’s implementation address. Any calls to execute business logic are reverted.

Now let’s take a look at our implementation contract the proxy points to.

For a contract to have a primary name set, a reverse record needs to be set with the Reverse Registrar. This can be done only by the owner of the contract. To support ownership, a contract needs to implement the Ownable interface. This is how a simple Ownable implementation contract would look:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/proxy/utils/Initializable.sol";

contract ImplementationV1 is Initializable, Ownable {

string public message;

// Runs only on the implementation instance (not on proxy storage).
constructor() Ownable(msg.sender) {
_disableInitializers(); // prevents initializing the implementation directly
}

// Runs through proxy via delegatecall; writes owner/message into proxy storage.
function initialize(address initialOwner, string calldata initialMessage) external initializer {
_transferOwnership(initialOwner);
message = initialMessage;
}

function setMessage(string calldata newMessage) external onlyOwner {
message = newMessage;
}
}

We can now deploy both these contracts and point the proxy contract to the implementation by using a number of ways:

If deploying a new proxy: pass (implementationAddress, adminAddress, initData) to proxy constructor. If upgrading an existing proxy: admin calls upgradeToAndCall(newImplementationAddress, initData) (or upgradeTo if already initialized).

The naming convention for proxy and implementation contracts

To effectively version a proxy contract and the implementation contract, we recommend a simple approach:

The proxy Identity: Assign your primary ENS name (e.g., app.mydomain.eth) directly to the proxy contract. This is the stable address that frontends and users interact with. The Logic Versions: Assign versioned subnames (e.g., v1.app.mydomain.eth, v2.app.mydomain.eth) to the individual Implementation contracts as they are deployed.

Versioning with Foundry

The following CI/CD pipeline deployment uses the Enscribe Foundry library to set names for the proxy and its implementation. In a deploy script, you simply deploy the new proxy, its implementation and tag the new version programmatically.


// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {Script} from "forge-std/Script.sol";
import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import {ImplementationV1} from "../src/ImplementationV1.sol";
import {Ens} from "enscribe/Ens.sol";

contract UpgradeScript is Script {
function run() public {
// Deployer EOA that signs deployment txs.
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");

// App owner (calls app functions via proxy, e.g. setMessage).
address appOwner = vm.envAddress("APP_OWNER");

// Proxy admin (upgrade authority). Keep separate from appOwner in transparent pattern.
address proxyAdmin = vm.envAddress("PROXY_ADMIN");

// Example init value for app state.
string memory initialMessage = vm.envString("INITIAL_MESSAGE");

require(appOwner != proxyAdmin, "Use separate app owner and proxy admin");

vm.startBroadcast(deployerPrivateKey);

// Deploy implementation contract (logic only).
ImplementationV1 impl = new ImplementationV1();

// Encode initializer call to run via delegatecall in proxy context.
bytes memory initData = abi.encodeCall(
OwnableAppV1.initialize,
(appOwner, initialMessage)
);

// Deploy proxy pointing to implementation, with admin + init data.
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
address(impl),
proxyAdmin,
initData
);


// Version the proxy (e.g., app.mydomain.eth)
Ens.setName(
block.chainid,
address(implementation),
string.concat("app", vm.envString("ENS_PARENT"))
);

// Version the implementation (e.g., v1.app.mydomain.eth)
Ens.setName(
block.chainid,
address(implementation),
string.concat(vm.envString("VERSION"), ".", "app", vm.envString("ENS_PARENT"))
);

vm.stopBroadcast();
}
}

Just like with standard contracts, you can run this via GitHub Actions exporting VERSION=v1 and ENS_PARENT=mydomain.eth in addition to other environment variables. The proxy and implementation are instantly versioned the moment they are deployed.

When we now go to block explorers, we can now clearly see that our user-facing app (the proxy) is named as app.mydomain.eth and the concrete implementation it points to is named as v1.app.mydomain.eth.

Here’s an example of how this same proxy contract named as cooldapp.abhi.eth looks like on Blockscout:

proxy

And here’s how the implementation contract named as v1.cooldapp.abhi.eth looks like:

implementation

Should you want to update the implementation, you can simply deploy and name it as v2.app.mydomain.eth and upgrade the proxy to point to this new version.

For other approaches to versioning regular smart contracts you can refer to our recent guide on smart contract versioning.

Why versioning matters for upgradeable contracts

Adopting onchain versioning for upgradeable contracts solves the transparency problem. We are moving from a world where users are forced to blindly trust an app upgrade to one where every implementation logic is distinctly named, human-readable and easy to verify on block explorers.

Ready to bring transparency to your upgrades? Check out the Enscribe Foundry Library, Hardhat Plugin, and TypeScript SDK to start versioning your proxies today.

Happy versioning! 🚀

How to perform onchain versioning of smart contracts

· 5 min read
Abhijeet Bhagat
Enscribe Senior Engineer

In software engineering, versioning is a core tenet of release management. However, when smart contracts are deployed to Ethereum networks, the deployment artifacts live as 42-character hexadecimal strings, with no onchain versioning information being captured about them.

They may be versioned in the GitHub repositories they were deployed on, or documented in docs, but there is no decentralised location where release information is captured, which seems ironic given they live on permissionless networks.

We want to see this change. It is now possible to easily deploy smart contracts with onchain versioning, and in this post we’re going to outline how you can do it.

We will demonstrate how you can bridge the gap between CI/CD pipelines, deployment scripts and onchain identity by programmatically assigning ENS names during deployment using open source tools.

Most developers use Foundry and Hardhat - two very popular options for smart contracts management. We provide examples showing usages of both these tools.

We’ll use Github Actions as an example but the steps/configs described should be fairly straightforward and similar with other CI/CD tools.

Naming with Foundry

We can use the Enscribe Foundry library that is written in Solidity to version your contracts. This is an example script that demonstrates how you can set a name to a contract using an environment variable:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {Script, console} from "forge-std/Script.sol";
import {Counter} from "../src/Counter.sol";
import {Ens} from "enscribe/Ens.sol";

contract MyContractScript is Script {
function run() public {
vm.startBroadcast();

counter = new Counter();
Ens.setName(block.chainid, address(counter),
string.concat(vm.envString("VERSION"), ".", vm.envString("ENS_PARENT")));

vm.stopBroadcast();
}
}

Assume we already have a top level domain name registered like mydomain.eth. You can now run this script directly from the command line to deploy & set a version for your contract:

$ export VERSION=v1 ENS_PARENT=app.mydomain.eth
$ forge script script/Counter.s.sol:CounterScript --rpc-url <BASE RPC URL> --chain-id 8453 --broadcast --private-key <PRIVATE KEY>

You can also run this script from Github Actions like so:

- name: Version Contract
env:
VERSION: v1
ENS_PARENT: app.mydomain.eth
RPC_URL: ${{ secrets.RPC_URL }}
PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}

# Execute forge script command to set the name
run: forge script script/Counter.s.sol:CounterScript --rpc-url "$RPC_URL" --chain-id 8453 --broadcast --private-key "$PRIVATE_KEY"

You can name multiple contracts in the same script as well:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {Script, console} from "forge-std/Script.sol";
import {Counter} from "../src/Counter.sol";
import {HelloWorld} from "../src/HelloWorld.sol";
import {MyAwesomeApp} from "../src/MyAwesomeApp.sol";
import {Ens} from "enscribe/Ens.sol";

contract DeployScript is Script {
function run() public {
vm.startBroadcast();

counter = new Counter();
hello = new HelloWorld();
app = new MyAwesomeApp();

Ens.setName(block.chainid, address(counter), string.concat(vm.envString("VERSION"), ".counter.", vm.envString("ENS_PARENT"));
Ens.setName(block.chainid, address(hello), string.concat(vm.envString("VERSION"), ".hello.", vm.envString("ENS_PARENT"));
Ens.setName(block.chainid, address(app), string.concat(vm.envString("VERSION"), ".app.", vm.envString("ENS_PARENT"));

vm.stopBroadcast();
}
}
- name: Version Contracts
env:
VERSION: v1
ENS_PARENT: mydomain.eth
RPC_URL: ${{ secrets.RPC_URL }}
PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}

# Execute forge script command to set the names
run: forge script script/DeployScript.s.sol:DeployScript --rpc-url "$RPC_URL" --chain-id 8453 --broadcast --private-key "$PRIVATE_KEY"

Naming with Hardhat

If Hardhat, instead, is what you use for contracts management, then you can version using Hardhat and the Hardhat Enscribe plugin.

We can now set an environment variable in your CI/CD pipeline like $CONTRACT_NAME=v1.app.mydomain.eth and add a Hardhat Enscribe plugin command to set this name to the contract address to the pipeline:

$ npx hardhat enscribe name $CONTRACT_NAME --contract <contract address>

For e.g., this is an example Github Actions workflow file where you can set this variable and set the name of a contract:

- name: Version Contract
env:
CONTRACT_NAME: v1.app.mydomain.eth
CONTRACT_ADDRESS: ${{ env.DEPLOYED_ADDRESS }}
RPC_URL: ${{ secrets.RPC_URL }}
PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}

# Execute Hardhat plugin command to set the name
run: npx hardhat enscribe name "$CONTRACT_NAME" --contract "$CONTRACT_ADDRESS" --network mainnet

Your should add the Hardhat Enscribe plugin to your dev dependencies in the package.json file before:

{
"devDependencies": {
"hardhat": "^3.0.0",
"@enscribe/hardhat-enscribe": "^0.1.5",
...
}
}

When you trigger a build and release cycle in the pipeline, your contract is now automatically versioned.

We can use a Git based commit hash as a part of the contract version if it is preferred over semantic versioning by extracting it programmatically and exposing it as an environment variable:

- name: Extract and set Git Hash
shell: bash
run: |
# 1. Get the short hash (7 chars)
SHORT_SHA=$(git rev-parse --short HEAD)

# 2. Save it to the Global Environment for this job
echo "GIT_HASH=$SHORT_SHA" >> $GITHUB_ENV

- name: Run Deployment
run: npx hardhat enscribe name "$CONTRACT_NAME" --contract "$CONTRACT_ADDRESS" --network mainnet
env:
CONTRACT_NAME: ${{ env.GIT_HASH }}.app.mydomain.eth
CONTRACT_ADDRESS: ${{ env.DEPLOYED_ADDRESS }}
RPC_URL: ${{ secrets.RPC_URL }}
PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}

Why Versioning Matters

By adopting onchain versioning, we aren't just making our own lives easier; we are establishing long-overdue transparency for releases. We are moving from a world where users are forced to trust hexadecimal strings to one where every deployment has a human-readable, verifiable name. The tools to make this happen are now here and ready to integrate into your pipeline.

Ready to upgrade your workflow? Check out the Foundry Library, Hardhat Plugin and TypeScript SDK or these examples to start versioning your contracts.

Happy Versioning! 🚀