There are two common reasons for wanting smart-contract-upgradability — new features and security (bugs). Upgradeable contracts are more scalable and secure since they can be updated to keep up with state-of-the-art, community-wide practices and standards.
Since Firefly is a customizable and extensible protocol, it must provide the flexibility to change pieces of core business logic through the decentralized governance process. That's why OpenZeppelin's thoroughly audited, open-source upgradability framework allows for cutting-edge, upgradable smart contracts with a high level of security. In this article, Firefly's Insurance Fund and Governance contracts will serve as examples of how upgradable contracts work under the hood.Figure I: A smart contract before and after an upgrade.
By design, smart contracts are immutable. This powerful feature prevents someone with malicious intent from changing a trusted contract. However, it can also be an obstacle hindering the ability to add features and bolster security. On the other hand, upgradability requires an admin address to be trusted with the responsibility of modifying contracts correctly. This adds a central point of failure and goes against the trustless nature of decentralized applications. To make upgradability possible while keeping the Firefly protocol trustless, the ownership of the contracts will be transferred to Firefly's decentralized Governance
Equipped with this understanding, let's now dive into the main topic: how contracts can be made upgradeable. Upgradable contracts are achieved through a proxy pattern, which requires two separate contracts for one upgradable one. As illustrated below, a "proxy" contract acts as an interface between the outer world (users) and an "implementation" contract that stores the core business logic.Figure II: User interacts with a proxy contract, which delegates function calls to the upgraded contract.
A user interacts with the proxy contract, which diverts function calls it receives to the implementation contract as shown in Figure II.
The proxy also has a few important publicly exposed functions
setAdmin/updateAdmin (same function with
two naming conventions) that allow users to change the admin of
the proxy. The admin can be an address belonging to a single
user, a multi-sig wallet, or another contract. Only the current
admin of the proxy can transfer adminship so these functions
generally have the
modifier applied to them.
The admin is also the only address that may invoke the
setImplementation function on the proxy contact
that updates the address of the implementation contract stored
within the proxy. Once updated, the proxy contract routes all
function calls to the new contract as seen in Figure III. This
design allows users to continue using the same point of access
(the proxy contract) while also having access to the new
Another important and difficult process required to perform a successful contract upgrade is the migration of the current implementation contract's state to the new contract's state. If the new contract does not have the same state, data, and parameters as the old one, it could result in a large loss of data or at worse crash the entire protocol. This is analogous to database migrations when updating backend servers for an application. If an application is moved from AWS to Azure and the database is not also migrated, the application will lose all previous data such as client preferences, balance, etc.
Writing code for proxy contracts and migrations is not a trivial task; it requires dedicated time and effort to ensure an upgrade can be pushed successfully when needed while retaining its previous state. This is where OpenZeppelin's Upgrades Plugin shines - it provides a framework to build, test, deploy and upgrade contracts in a secure and orderly fashion. The Upgrades Plugin implements the aforementioned proxy design and logic, allowing developers to instead focus on updating the business logic of the implementation contract. It does so by enforcing users to follow these framework guidelines. A few of the main ones are:
- An upgradeable contract must derive from base upgradable contracts (where needed)
- An upgradeable contract must implement its own public initializer that may be invoked only once - when deploying the first implementation contract
- An upgradeable contract's newer implementation can not change the order of storage variables
These guidelines and many others outlined by the framework are followed to make Firefly's Insurance Fund and Governance contracts upgradeable. Apart from the DETToken contract, not upgradeable due to important design considerations, all other contracts can be easily upgraded via a governance proposal. These contracts include:
- InsuranceFund: Allows users to stake USDC and earn rewards in DET
- TokenVesting: Allows users to create vesting contracts and aids in distributing tokens vested over a period of time
- Governance: Allows users to create proposals to upgrade the protocol
- TimeLock: Allows governance to execute the actions proposed in a proposal
Figure V shows the ownership/adminship graph; the
TimeLock Contract is the admin of all proxies including
its own. When a proposal is initiated and accepted through
voting on Governance, the TimeLock Contract is
called upon by the Governance to execute the actions specified
in the proposal. The self adminship of TimeLock is a
special case that was thoroughly researched, verified, and
tested before implementation.
It exists to serve the case when an upgrade proposal to
upgrade the TimeLock Contract is passed on the
Governance. In such a scenario, the TimeLock Contract itself will have to
upgrade its implementation address by calling the
upgradeTo function of its proxy since it is under
Before a proposal can be presented to governance, the proposer must deploy the new implementation on the network like this:
// The proposer must first prepare and deloy the new implementation of the contract // in order to upgrade // token vesting contract deployed on local/testnet/mainnet const tokenVesting:TokenVesting; const proxyAddress:string = tokenVesting.address // returns the address of proxy // gets the new implementation for TokenVesting const factory = await hardhat.ethers.getContractFactory(`TokenVestingV2`) // openzeppelin upgrade function first checks if the new implementation // adheres to upgrade plugin rules and deploys on the network and returns the address const newImplAddress = await hardhat.upgrades.prepareUpgrade(proxyAddress, factory)
Once the new implementation is deployed, a proposal can be proposed in Governance to update the proxy contract of TokenVesting having the following actions:
const values = 0; // address of the proxy const targets = tokenVesting.address; // signature of the function const signatures = "upgradeTo(address)"; // argument to upgradeTo is the address of new implementation const callData = encodeParameters(["address"], [newImplAddress]); // governance contract deployed on local/testnet/mainnet const governance:Governance; // proposes an upgrade for TokenVesting await governance.propose( targets, values, signatures, callDatas, "Upgrading TokenVesting to V2" ); // once the proposal is successfully approved by majority of DET holders, // it can be executed via governance, which internally calls TimeLock to execute // each function call in proposal await governance.execute(<proposalId>)
Once the proposal has accumulated enough votes, the
Governance executes the proposal. Governance uses
TimeLock to execute the
on TokenVesting Contract proxy and updates the
implementation to the new address.