Free 30-min Web3 Consultation
Book Consultation
Smart Contract Security Audits
View Audit Services
Custom DeFi Protocol Development
Explore DeFi
Full-Stack Web3 dApp Development
View App Services
Free 30-min Web3 Consultation
Book Consultation
Smart Contract Security Audits
View Audit Services
Custom DeFi Protocol Development
Explore DeFi
Full-Stack Web3 dApp Development
View App Services
Free 30-min Web3 Consultation
Book Consultation
Smart Contract Security Audits
View Audit Services
Custom DeFi Protocol Development
Explore DeFi
Full-Stack Web3 dApp Development
View App Services
Free 30-min Web3 Consultation
Book Consultation
Smart Contract Security Audits
View Audit Services
Custom DeFi Protocol Development
Explore DeFi
Full-Stack Web3 dApp Development
View App Services
LABS
Guides

Launching a Governance-Controlled Upgrade Framework

A developer tutorial for implementing a secure, on-chain governance system to manage smart contract upgrades, covering proposal flow, voting mechanisms, and timelock integration.
Chainscore © 2026
introduction
TUTORIAL

Launching a Governance-Controlled Upgrade Framework

A guide to implementing a secure, decentralized upgrade mechanism for smart contracts using on-chain governance.

Governance-controlled upgrades are a critical design pattern for decentralized applications (dApps) that require long-term evolution. Unlike immutable contracts or admin-controlled upgrades, this framework delegates upgrade authority to a community of token holders or delegates via a governance contract. This ensures that changes to the protocol's core logic are transparent, debated, and executed only after achieving a predefined consensus threshold, such as a majority vote. It is the standard for protocols like Compound and Uniswap, balancing adaptability with decentralization.

The architecture typically involves three core components: the logic contract containing the business rules, a proxy contract that users interact with and which delegates calls to the logic, and a governance contract (e.g., OpenZeppelin Governor) that holds the upgrade authority. The proxy's storage is persistent, while its logic is replaceable. When a governance proposal passes, it executes a transaction that updates the proxy's reference to a new logic contract address. This pattern, often using the EIP-1967 standard, allows for seamless user experience and preserved state during upgrades.

Implementing this starts with your logic contract using upgradeable patterns. With OpenZeppelin's Upgrades Plugins, you deploy an initial BoxV1 contract. A simplified deploy script using Hardhat and the @openzeppelin/hardhat-upgrades plugin looks like this:

javascript
const BoxV1 = await ethers.getContractFactory("Box");
const box = await upgrades.deployProxy(BoxV1, [42], { initializer: 'initialize' });
await box.deployed();
console.log("Box Proxy deployed to:", box.address);

This deploys a proxy, an admin contract (initially held by the deployer), and your BoxV1 implementation.

Next, you must transfer the proxy's admin rights to a governance contract. First, deploy a governance module like OpenZeppelin's Governor. After deployment, you use the plugin to schedule the transfer of admin powers:

javascript
const governor = await ethers.getContractAt("Governor", governorAddress);
const proxyAdmin = await upgrades.admin.getInstance();
const transferCalldata = proxyAdmin.interface.encodeFunctionData("changeAdmin", [governorAddress]);
// Propose this calldata to the governance contract for a vote

Once the proposal passes and is executed, the governance contract becomes the sole entity capable of authorizing upgrades, completing the decentralization of control.

Security considerations are paramount. Always use transparent proxies (EIP-1967) or UUPS proxies (EIP-1822) from audited libraries to avoid storage collisions. Thoroughly test upgrades on a testnet using a forked mainnet state. The governance process itself must have secure parameters: a sufficient voting delay and period, a high quorum, and a timelock contract between the vote and execution. This timelock gives users a window to exit if they disagree with an upgrade. Never leave upgrade authority in an Externally Owned Account (EOA) in production.

This framework future-proofs your protocol. When BoxV2 is ready, a governance proposal is created to upgrade the proxy. The proposal includes the calldata to execute the upgrade via the proxy admin. After successful voting and timelock, the upgrade is executed. All user funds and data in the proxy storage remain intact, but the logic is updated. This process, while more complex than admin upgrades, is essential for building trustless, community-owned infrastructure that can adapt to new innovations and security standards over time.

prerequisites
PREREQUISITES AND SETUP

Launching a Governance-Controlled Upgrade Framework

This guide details the technical prerequisites and initial setup required to deploy a secure, on-chain governance framework for managing smart contract upgrades.

Before writing any code, you must define the core components of your upgrade system. A standard governance-controlled upgrade framework typically consists of three main contracts: the implementation contract containing the business logic, a proxy contract (like a Transparent or UUPS proxy) that delegates calls to the implementation, and a Timelock Controller that acts as the proxy's admin. The Timelock enforces a mandatory delay on upgrade proposals, giving token holders time to react. You will also need a governance token contract (e.g., an ERC20Votes token) and a governor contract (like OpenZeppelin's Governor) to manage proposal creation and voting.

Your development environment must be configured to handle upgradeable contracts. This requires specific compiler settings and security plugins. Install the necessary libraries using npm or yarn: @openzeppelin/contracts-upgradeable, @openzeppelin/hardhat-upgrades, and hardhat. In your hardhat.config.js, import the upgrade plugin and configure your network connections for both testing (e.g., Hardhat Network) and eventual deployment to a live chain like Ethereum Mainnet or an L2. Ensure your Solidity compiler version is compatible with the OpenZeppelin upgradeable contracts library, typically 0.8.x or higher.

Writing upgradeable contracts differs from standard Solidity. You cannot use constructors or initialize state variables in their declarations. Instead, you must define an initialize function, which acts as the constructor for the proxy pattern. This function should include the initializer modifier from OpenZeppelin to prevent re-initialization. For example, your implementation contract would start with: function initialize(address initialAdmin) public initializer { __Ownable_init(); _transferOwnership(initialAdmin); }. All parent contracts must use their _init functions, and you must avoid leaving gaps in your storage layout between upgrades.

You will need a comprehensive testing strategy. Use a forked test environment (e.g., Hardhat's hardhat_reset to fork Mainnet) to simulate real conditions. Write tests that verify: the initial deployment and linking of the proxy, implementation, and Timelock; the full governance flow from proposal creation through voting, queueing in the Timelock, and final execution; and that storage layout is preserved during an upgrade. Tools like @openzeppelin/upgrades-core can help validate upgrades. Testing the security invariants, like ensuring only the Timelock can execute an upgrade, is critical.

For deployment, script the process using Hardhat scripts. A typical deployment sequence is: 1. Deploy the implementation contract (logic). 2. Deploy the proxy contract, pointing it to the implementation. 3. Deploy the Timelock Controller with a minimum delay (e.g., 2 days). 4. Transfer ownership of the proxy admin role to the Timelock address. 5. Deploy the governance token and governor, configuring the governor to use the Timelock as its executor. Always verify your contracts on block explorers like Etherscan after deployment, using the hardhat-etherscan plugin for the proxy, implementation, and all admin contracts.

key-concepts
ARCHITECTURE

Core Components of the Framework

A governance-controlled upgrade framework requires specific smart contract patterns and security modules. This section details the essential components for a secure and decentralized upgrade path.

01

Proxy Contracts

The proxy pattern separates a contract's logic from its storage. A proxy contract holds the state and delegates function calls to a logic contract. This allows the logic to be upgraded without migrating data or disrupting user interactions. Common implementations include:

  • Transparent Proxy Pattern: Uses an admin to manage upgrades, preventing function selector clashes.
  • UUPS (EIP-1822): Upgrade logic is built into the logic contract itself, making proxies cheaper to deploy.
  • Beacon Proxy: A single beacon contract stores the current logic address for many proxies, enabling mass upgrades.
02

Timelock Controller

A Timelock is a mandatory delay between a governance vote's approval and its execution. This critical security component prevents malicious or rushed upgrades by enforcing a mandatory waiting period (e.g., 48-72 hours). During this window, users can:

  • Review the exact calldata of the pending upgrade.
  • Exit the protocol if they disagree with the changes.
  • It acts as a final safeguard, ensuring upgrades are transparent and giving the community time to react.
03

Governance Module

This is the on-chain voting mechanism that authorizes upgrades. Proposals to change the proxy's logic address must pass through here. Key design choices include:

  • Token-based Voting (e.g., Compound Governor): Voting power is proportional to governance token holdings.
  • Multisig Execution: A Gnosis Safe can act as the executor, requiring multiple signatures to proceed after a vote.
  • Proposal Thresholds: Minimum token requirements to submit a proposal prevent spam.
  • Quorum & Voting Period: Defines the minimum participation and time needed for a valid vote.
04

Upgrade Function & Initializer

The mechanism that performs the actual upgrade. In the logic contract, a function like upgradeTo(address newImplementation) is protected by access control (usually the Timelock). Because constructors cannot be used with proxies, you must use an initializer function marked with initializer to set up the contract's initial state. This function should only be callable once to prevent re-initialization attacks. Libraries like OpenZeppelin's Initializable provide the modifiers for this pattern.

06

Rollback & Emergency Procedures

A robust framework plans for failure. This includes:

  • Pausing Mechanism: An emergency pause function in the logic contract, controlled by a trusted entity or governance, can halt system operations if a bug is discovered post-upgrade.
  • Rollback Preparedness: Maintain the previous logic contract's code and deployment artifacts. A rollback is simply a new governance proposal to upgrade the proxy back to the old, verified address.
  • Multisig Guardian: Some protocols deploy a proxy admin multisig with unilateral power to pause or execute an emergency upgrade if governance is compromised, creating a last-resort safety layer.
step-1-governance-token
FOUNDATION

Step 1: Deploying the Governance Token

The governance token is the cornerstone of your upgrade framework, granting voting power to stakeholders. This step covers the design, deployment, and initial distribution of the token using a standard like OpenZeppelin's ERC20Votes.

Governance tokens confer voting rights, typically weighted by token balance, to make collective decisions about a protocol's future. For an upgrade framework, these decisions include authorizing upgrades to core smart contracts, adjusting system parameters, or allocating treasury funds. Using a battle-tested standard like ERC20Votes is critical, as it provides built-in vote delegation and a snapshot mechanism that prevents voting power manipulation by locking balances at a specific block number. This ensures each vote reflects a user's committed, long-term stake in the ecosystem.

Deployment involves writing and compiling a token contract that extends the OpenZeppelin library. A minimal implementation imports ERC20Votes.sol and Ownable.sol. The constructor mints an initial supply to a designated wallet (e.g., a multi-sig for the founding team or a community treasury). It's essential to decide on the total supply and initial distribution upfront, as these are immutable properties post-deployment. For transparency, many projects publicly verify the contract source code on block explorers like Etherscan immediately after deployment.

After deployment, you must establish the initial token distribution. Common methods include a fair launch via a liquidity pool, an airdrop to early community members, or allocations for investors and core contributors. The distribution model directly impacts decentralization and security; concentrating too many tokens in few hands risks centralization. Tools like Snapshot can be used for off-chain signaling votes during this bootstrap phase before the on-chain governance system is fully activated, allowing the community to participate in early decisions like setting up the first liquidity pool.

step-2-timelock-executor
IMPLEMENTING SECURE DELAYS

Step 2: Setting Up the Timelock Controller

The Timelock Controller is a smart contract that enforces a mandatory delay between a governance proposal's approval and its execution, creating a critical safety window.

A Timelock Controller acts as an intermediary executor for your protocol's upgradeable contracts. Instead of a governor contract calling a function directly, it schedules the call through the Timelock. This contract holds the authority (via the TIMELOCK_ADMIN_ROLE) and will execute the call automatically after a predefined minimum delay has passed. This delay is the core security mechanism, providing time for users to review the pending action and potentially exit the system if they disagree with it.

You can deploy a Timelock using OpenZeppelin's contracts. A standard setup involves specifying the minimum delay (e.g., 2 days for a mainnet protocol) and the addresses of the proposers and executors. Typically, your Governor contract will be the sole PROPOSER_ROLE, and the EXECUTOR_ROLE is granted to a zero address to allow anyone to execute after the delay. The TIMELOCK_ADMIN_ROLE should be granted to a multisig or the deploying EOA initially, and then ideally renounced or transferred to the Governor for full decentralization.

Here is a basic example of deploying a Timelock Controller using Foundry and OpenZeppelin Contracts v5:

solidity
import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol";

contract DeployTimelock {
    function deployTimelock(address initialAdmin, uint256 minDelay) public returns (TimelockController) {
        address[] memory proposers = new address[](1);
        address[] memory executors = new address[](1);
        proposers[0] = address(governor); // Governor address set elsewhere
        executors[0] = address(0); // Public execution

        TimelockController timelock = new TimelockController(
            minDelay,
            proposers,
            executors,
            initialAdmin
        );
        return timelock;
    }
}

After deployment, you must configure your Governor contract to use this Timelock as its executor.

Configuring the Governor involves setting the Timelock address in the Governor's constructor or via initialization. For OpenZeppelin's Governor contracts, this is typically the timelockAddress parameter. Once linked, all proposals that pass a vote will result in an id (a hash of the operation details). The Governor calls timelock.schedule(...) with this operation, which queues it for execution after the delay. Users can monitor pending operations using the timelock's getTimestamp(id) view function.

The security model relies on the integrity of the delay. Key considerations include: setting a delay long enough for community response (e.g., 24-72 hours), ensuring the Timelock's admin role is securely managed, and verifying that all critical protocol functions are owned by the Timelock address. Never grant the PROPOSER_ROLE or EXECUTOR_ROLE to an externally owned account (EOA) without additional safeguards, as this bypasses governance.

Finally, test the entire flow in a forked mainnet environment or on a testnet. Simulate a full governance cycle: propose, vote, wait for the delay, and execute via the Timelock. Verify that operations cannot be executed before the delay expires and that only the Governor can schedule them. This step transforms your governance system from theoretical to operational, embedding a non-bypassable safety check into your protocol's upgrade mechanism.

step-3-governor-contract
IMPLEMENTING THE CORE LOGIC

Step 3: Building the Governor Contract

This step deploys the on-chain governance contract that will manage proposals, voting, and the execution of upgrades to your protocol.

The Governor contract is the central on-chain component of your upgrade framework. It is responsible for the entire proposal lifecycle: creation, voting, and execution. For this guide, we will use OpenZeppelin's Governor contracts, specifically the Governor base and the GovernorTimelockControl module. This setup integrates a Timelock contract, which introduces a mandatory delay between a proposal's approval and its execution, providing a critical security buffer for users.

Start by writing and deploying your custom Governor contract. You will extend Governor and GovernorTimelockControl, configuring key parameters like votingDelay (blocks before voting starts), votingPeriod (blocks voting is active), and quorumFraction (percentage of total supply needed to pass). A typical setup for an Ethereum mainnet fork might use a votingDelay of 1 block, a votingPeriod of 45818 blocks (~7 days), and a quorumFraction of 4. The constructor must initialize the associated TimelockController address.

Here is a basic implementation example in Solidity:

solidity
import "@openzeppelin/contracts/governance/Governor.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol";

contract MyGovernor is Governor, GovernorTimelockControl {
    constructor(IVotes _token, TimelockController _timelock)
        Governor("MyGovernor")
        GovernorTimelockControl(_timelock)
    {
        // Set governance parameters
    }
    // Override required functions: votingDelay(), votingPeriod(), quorum(), etc.
}

After writing the contract, compile it and deploy it to your network, passing the address of your governance token and the deployed TimelockController as constructor arguments.

Once deployed, you must grant the Governor contract specific roles within the Timelock. The Governor needs the PROPOSER_ROLE to schedule operations and the CANCELLER_ROLE to cancel them. Use the Timelock's grantRole function, called by the address holding the DEFAULT_ADMIN_ROLE (which should be a secure multisig). Crucially, revoke the DEFAULT_ADMIN_ROLE from any EOA deployer addresses to decentralize control. After this setup, the Timelock will only execute actions that have been proposed and approved through the Governor's process.

Finally, verify the integration. Create a test proposal that queues a dummy function call in the Timelock. The flow should be: 1) Proposal is created on the Governor, 2) Token holders vote, 3) If the vote succeeds, the action is automatically queued in the Timelock, and 4) After the timelock delay, the action can be executed. This end-to-end test confirms that your governance-controlled upgrade framework is operational and secure, with all power flowing from token holders through the Governor contract.

step-4-upgradeable-target
IMPLEMENTATION

Step 4: Integrating the Upgradeable Contract

Deploy and configure the governance-controlled upgrade framework for your smart contract system.

With the proxy contract and upgrade logic defined, the next step is to integrate them into a functional system. This involves deploying the contracts in the correct order and establishing the governance mechanism that will control future upgrades. The standard sequence is to first deploy the ImplementationV1 contract, then the TransparentUpgradeableProxy, and finally the ProxyAdmin contract, which will be the owner of the proxy. The ProxyAdmin is crucial as it holds the administrative rights to perform upgrades, and its ownership should be transferred to your chosen governance contract, such as a DAO's Timelock or a multisig wallet.

The deployment script must carefully manage contract addresses and constructor arguments. For a TransparentUpgradeableProxy, the constructor requires three parameters: the address of the initial logic contract (_logic), the address of the initial admin (_admin), and any initialization data (_data). The initialization data is a call to the initialize function on your implementation contract, which sets up the initial state. It is critical that this initialize function can only be called once to prevent reinitialization attacks. Here is a simplified Hardhat deployment snippet:

javascript
const ImplementationV1 = await ethers.getContractFactory("ImplementationV1");
const implV1 = await ImplementationV1.deploy();
await implV1.deployed();

const ProxyAdmin = await ethers.getContractFactory("ProxyAdmin");
const admin = await ProxyAdmin.deploy();
await admin.deployed();

const TransparentUpgradeableProxy = await ethers.getContractFactory("TransparentUpgradeableProxy");
const initData = implV1.interface.encodeFunctionData("initialize", [arg1, arg2]);
const proxy = await TransparentUpgradeableProxy.deploy(implV1.address, admin.address, initData);
await proxy.deployed();

After deployment, you must verify that the proxy correctly points to the logic contract. You can call await admin.getProxyImplementation(proxy.address) to confirm. The final and most important governance step is to transfer ownership of the ProxyAdmin contract away from the deployer EOA to a secure, on-chain governance contract. For a DAO, this is typically a Timelock controller like OpenZeppelin's TimelockController. Once transferred, all upgrade proposals must pass through the DAO's voting and timelock delay process, ensuring no single party can unilaterally change the contract's logic. This completes the integration, establishing a secure, transparent, and community-controlled upgrade path for your protocol.

GOVERNANCE MODELS

Voting Mechanism Comparison

A comparison of on-chain voting mechanisms for upgrade proposals, detailing trade-offs in security, cost, and voter participation.

MechanismToken-Weighted VotingConviction VotingQuadratic Voting

Voting Power Basis

1 token = 1 vote

Tokens × Time Locked

(√Tokens) × Votes

Whale Resistance

Proposal Cost (Avg. ETH)

0.05 - 0.1

0.02 - 0.05

0.08 - 0.15

Time to Finality

3-7 days

1-4 weeks

3-7 days

Snapshot Integration

Gas Cost per Vote (Avg.)

$5-15

$2-8

$10-25

Used by

Uniswap, Compound

1Hive, Commons Stack

Gitcoin Grants

step-5-full-proposal-flow
VALIDATION

Step 5: Testing the End-to-End Flow

This final step validates the entire governance-controlled upgrade framework by simulating a real-world proposal, vote, and execution cycle.

With the core contracts deployed and the upgrade logic in place, you must now test the complete governance lifecycle. This involves creating a proposal, simulating a vote, and executing the upgrade to ensure all components interact correctly. A robust test suite for this flow is critical, as it validates the security and decentralization of your upgrade mechanism. Use a local testnet like Hardhat or Anvil to simulate the process from start to finish without spending real gas.

Start by writing a test that creates a proposal to upgrade to a new implementation contract. Your test should call the propose function on the governance contract with the encoded calldata for the upgradeTo function on the ProxyAdmin. You'll need to simulate the proposal passing the required voting period and quorum. For example, in a Hardhat test, you can impersonate voter accounts and cast votes using hardhat.impersonateAccount and governanceContract.connect(voter).castVote(proposalId, support). Ensure the proposal state transitions correctly from Pending to Active to Succeeded.

After the vote succeeds, the proposal must be queued. Test the queue function, which will move the proposal to a timelock. The timelock introduces a mandatory delay (e.g., 48 hours) before execution, a crucial security feature that allows users to react to malicious upgrades. Your test should advance the blockchain's timestamp using ethers.provider.send('evm_increaseTime', [delay]) and then mine a new block to simulate the passage of time.

Finally, execute the queued proposal. The execute function will call through the timelock to the ProxyAdmin, which performs the actual upgradeTo operation. After execution, verify that the proxy's implementation address has changed by calling await upgrades.erc1967.getImplementationAddress(proxy.address). Also, write integration tests that call a function on the new implementation to confirm it behaves as expected, ensuring the upgrade did not break existing functionality.

Beyond the happy path, your end-to-end tests must cover edge cases and failure modes. Test scenarios where a proposal fails due to insufficient votes, where someone tries to execute before the timelock delay expires, or where a malicious proposal with invalid calldata is rejected. Consider using fuzz testing with tools like Foundry's forge to generate random input for proposal parameters and ensure the system remains robust under unexpected conditions.

Documenting the test results and the exact steps required for a live upgrade is the final deliverable. This documentation should include the proposal creation script, the expected voting timeline, and the final execution command. A successfully tested end-to-end flow confirms that your protocol's upgrade mechanism is secure, transparent, and fully under the control of its token holders.

GOVERNANCE UPGRADES

Security Considerations and Best Practices

Implementing a governance-controlled upgrade framework introduces unique security vectors. This guide addresses common developer questions and pitfalls when designing and deploying upgradeable contracts under decentralized control.

A timelock is a smart contract that enforces a mandatory delay between when a governance proposal is approved and when it can be executed. This is a critical security mechanism for several reasons:

  • Provides an escape hatch: It gives users time to react to a malicious or buggy upgrade. If a proposal is harmful, users can exit the protocol (e.g., withdraw funds) before the change takes effect.
  • Prevents instant tyranny: It mitigates the risk of a sudden governance attack where a malicious actor with temporary voting power could instantly push through a damaging proposal.
  • Allows for public scrutiny: The delay period allows developers, security researchers, and the community to audit the final executable code of the proposal.

Protocols like Compound and Uniswap use timelocks (e.g., 2-7 days). The timelock address should be set as the owner or admin of the upgradeable contract, ensuring all changes are subject to the delay.

UPGRADE FRAMEWORKS

Frequently Asked Questions

Common technical questions and solutions for developers implementing on-chain governance for protocol upgrades.

A governance-controlled upgrade framework is a smart contract architecture that delegates the authority to upgrade a protocol's logic to a decentralized governance token. Instead of a single admin key, a DAO or token-holder vote approves and executes upgrades. The core mechanism typically involves:

  • Proxy Pattern: User interactions point to a static proxy contract (e.g., an OpenZeppelin TransparentUpgradeableProxy) which delegates calls to a logic contract.
  • Governance Module: A separate contract (like Compound's Governor or OpenZeppelin Governor) holds the upgrade authority. It requires a successful proposal and vote to execute a transaction.
  • Upgrade Execution: Once a proposal passes, the governance contract calls the proxy's upgradeTo(address newImplementation) function, atomically switching the logic for all users.

This creates a transparent, time-delayed process for changes, moving away from centralized control.

How to Build a Governance-Controlled Upgrade Framework | ChainScore Guides