James Bachini

Solidity Proxy Contract Tutorial With Example Code

Proxy Contracts Tutorial

This Solidity proxy contract tutorial delves into the concept of upgradeable proxy contracts in Solidity, specifically utilizing OpenZeppelin’s proxy contract template. We’ll start with an understanding of how these contracts work, the compromise of immutable decentralization vs upgradeability and then dive into practical code examples.

  1. Proxy Contract Video Tutorial
  2. Upgradeable Proxy Contracts
  3. Immutability vs Upgradeability
  4. Proxy Contract Example
  5. 4 Proxy Security Considerations

Proxy Contract Video Tutorial

In this video I provide an overview of how proxy contracts work and demonstrate deploying a UUPS contract and then upgrading the implimentation.

James On YouTube

Upgradeable Proxy Contracts

A proxy contract is a design pattern in Ethereum smart contracts, allowing for the modification of contract code without changing the contract address or the state. This is crucial for fixing bugs, updating logic, or adding new features after deployment.

How Proxy Contracts Work

An upgradeable proxy contract involves two main components: the proxy contract itself and the implementation contract.

  1. Proxy Contract: Acts as a entry point for users, holding the state and delegating calls to the implementation contract.
  2. Implementation Contract: Contains the actual logic of the application.
Upgradeable proxy smart contracts

The DelegateCall

The magic happens with the delegatecall functionality, which allows the proxy contract to execute code from the implementation contract in its own context. This means the state is preserved, even when the implementation contract is upgraded.

There are two main types of upgradeable proxies widely used today. They are UUPS and Transparent Proxies, both of which have support in OpenZeppelin libraries.

They differ primarily in their upgrade mechanisms and design pattern. Transparent proxies handle upgrades and admin functions within the proxy itself, leading to a more complex and costly deployment. In contrast, UUPS proxies integrate the upgrade logic into the implementation contract, making the proxy itself simpler and less expensive to deploy. Additionally, UUPS proxies can potentially have their upgradeability removed, offering a path to later immutability.

While Transparent proxies maintain a clear separation between upgrade logic and business logic, UUPS proxies require careful implementation, including specific security mechanisms, to ensure safe upgrades and proper access control. This integrated approach in UUPS proxies is generally more efficient but demands a higher level of vigilance in design to maintain security and functionality.


Immutability vs Upgradeability

Smart contracts are by design immutable, once a contract is deployed, its code cannot be altered. This permanence:

  • Ensures Trust Users can interact with contracts knowing the rules won’t change unexpectedly
  • Prevents Tampering Makes it virtually impossible for bad actors to alter the contract for malicious purposes
  • Preserves History All interactions with the contract are permanently recorded on the blockchain

Using proxy patterns, developers can work around this to create upgradeable logic post deployment by directing calls to a new versions or implementations. This flexibility allows:

  • Bug Fixes and Improvements Developers can patch vulnerabilities and enhance functionality over time
  • Adaptation to Changing Needs Contracts can evolve to meet new requirements, market conditions and the fast evolving DeFi landscape

Just because we can create upgradeable contracts doesn’t necessarily mean we should. By introducing a proxy we add a layer of centralization around the development team. The code is no long immutable and decentralized, it loses some of the core value which attracted us to blockchain development in the first place.

There are workarounds such as time delayed upgrades which negates some of the security benefits in return for giving users an amount of time to withdraw funds or make decisions before the new code gets implemented.

Poxy contracts are useful in cases where decentralization is not a priority but their use should be restricted to situations where they are absolutely necessary


Transparent Proxy Contract Example

We have a simple hello world smart contract here:

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

contract HelloWorldV1 {
    string public text = "hello world";

    function setText(string memory _text) public {
        text = _text;
    }
} 

We can copy and paste this into a new contract on remix and deploy it locally or to a testnet to get a contract address. In my case this HelloWorldV1 contract is deployed to 0x32Df3A7f5Bc1834368Fa642Bdbc7c686C245d05a

We are going to be creating a transparent proxy pattern and an admin contract using OpenZeppelin libraries.

This is a factory type contract which deploys two further contracts:

  • ProxyAdmin – is the contract to manage ownership and upgrades
  • TransparentUpgradeableProxy – is the contract address you’ll give to users to interact with your contract

Full code for this is also available in the Solidity Snippets Github repo.

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

import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";

contract HelloWorldProxyDeployer {
	address public admin;
	address public proxy;
	constructor(address _implementation) {
		ProxyAdmin adminInstance = new ProxyAdmin(msg.sender);
		admin = address(adminInstance);
		TransparentUpgradeableProxy proxyInstance = new TransparentUpgradeableProxy(_implementation, admin, "");
		proxy = address(proxyInstance);
	}
}

We pass in the implementation contract address as the constructor argument to the proxy contract. This has been deployed to Goerli here:

We can then verify the admin and proxy contracts using the remix Contract Verification plugin and play with them on Etherscan.

One little gotcha, because we are sending in no bytes to the _data input we need to send in the null bytecode when verifying which is 0x0000000000000000000000000000000000000000

Verifying Proxy Contracts

You can have a play around with these contracts to get a feel for how it works.

Write to proxy smart contract

Notice how the data is stored in proxy contract but it doesn’t affect the persistent storage of the original implementation contract. The storage slots are relative to the proxy and the implementation just provides functionality.

Proxy storage test
How To Upgrade A Proxy Smart Contract

In the example above we are using OpenZeppelin’s proxy admin and the transparent proxy library. To upgrade we can create a new v2 implementation, deploy it to Goerli and then upgrade the implementation contract address which is part of the ProxyAdmin contract: https://goerli.etherscan.io/address/0xb99ff8843dc36dC8219ebf1fd5EEa755eaF07A2A#writeContract

Proxy Admin Interface

Note that this can only be called by the original deployer of the contract unless ownership has been transferred.


UUPS Proxy Example

The first file we will deploy is a V1 contract which contains the business logic we need.

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

import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

contract MyContractV1 is Initializable, UUPSUpgradeable, OwnableUpgradeable {
    uint256 public value;

    function initialize(address initialOwner) public initializer {
        __Ownable_init(initialOwner);
        __UUPSUpgradeable_init();
        value = 0;
    }

    function setValue(uint256 newValue) public onlyOwner {
        value = newValue;
    }

    function getValue() public view returns (uint256) {
        return value;
    }

    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}

    function upgrade(address newImplementation) public onlyOwner {
        upgradeToAndCall(newImplementation, "");
    }
}

We can then deploy the proxy which will act as an entry point to this logic.

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

import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

contract MyContractProxy is ERC1967Proxy {
    constructor(address _logic, bytes memory _data) ERC1967Proxy(_logic, _data) {}
}

When deploying this we will just pass in 0x for the _data in the constructor as we don’t want to pass anything through.

We can then initialize the proxy by calling initialize(adminWallet) on the proxy contract itself.

Then when we want to upgrade we can modify contract v1 and call it v2, deploy the new contract and call the following function on the proxy contract.

upgrade(newV2ContractAddress)


4 Proxy Security Considerations

Here are some security considerations to be aware of when working with upgradeable proxy patterns in Solidity.

  1. Storage Collisions A common issue is storage collisions between different versions of the logic contract. If a variable is stored at the same storage slot in different contract versions, updating the contract could inadvertently overwrite critical data. Developers need to ensure that new versions of a logic contract extend previous versions without modifying the storage hierarchy.
  2. The Constructor In Solidity, constructor code is executed only once when the contract instance is deployed. However, in the context of a proxy, the constructor of the logic contract will never execute. Instead, an ‘initializer’ function is used, which should be structured to be callable only once.
  3. Function Clashes Proxies need certain functions like upgradeTo(address) for upgrading, which could clash with functions in the logic contract. This is particularly problematic if two different functions across the proxy and logic contract end up with the same 4-byte identifier.
  4. Proxy Admins In the example above we used a ProxyAdmin contract to manage upgrades. This adds an extra security layer, and attack vector. The owner address becomes critical to the entire protocol as an attacker could take full control of the contract logic. The highest security practices should be implemented by developers such multisig wallets and hardware devices.

In many ways proxy contracts provide a bridge for developers coming from traditional web dev backgrounds to incorporate upgradeability. This comes at a steep cost however and takes away some of the elegant beauty of immutable code living on a decentralized peer to peer network. Use them sparingly and only when absolutely required and there is no other option for an immutable solution.


Get The Blockchain Sector Newsletter, binge the YouTube channel and connect with me on Twitter

The Blockchain Sector newsletter goes out a few times a month when there is breaking news or interesting developments to discuss. All the content I produce is free, if you’d like to help please share this content on social media.

Thank you.

James Bachini

Disclaimer: Not a financial advisor, not financial advice. The content I create is to document my journey and for educational and entertainment purposes only. It is not under any circumstances investment advice. I am not an investment or trading professional and am learning myself while still making plenty of mistakes along the way. Any code published is experimental and not production ready to be used for financial transactions. Do your own research and do not play with funds you do not want to lose.