Why Constructors Are Not Used in Implementation Contracts
In Solidity, constructors are special functions that execute once when a contract is deployed. They are commonly used to initialize contract variables and set up critical logic. However, when working with upgradable smart contracts, particularly proxy-based upgradeable contracts, constructors are avoided in the implementation contract. Instead, initialization is handled by separate initializer functions. Here, we are going to explores in depth why constructors are not used in implementation contracts, the technical reasons behind this choice, and best practices for handling initialization in upgradable contracts.
Understanding Proxy-Based Upgradable Contracts
Smart contracts deployed on Ethereum are immutable by default, meaning once deployed, their code cannot be modified. However, upgradeability is achieved using a proxy pattern, which separates contract logic from stored data. An upgradable contract system consists of two main parts:
Proxy Contract (Permanent)
Stores the contract’s state variables (e.g., balances, ownership data).
Forwards calls to an implementation contract using
delegatecall
.Can be upgraded to point to a new implementation contract when updates are needed.
Implementation Contract (Upgradeable)
Contains contract logic but does not hold any persistent state.
Can be replaced with a new version to introduce changes.
Why Are Constructors Avoided in Implementation Contracts?
1. Constructors Execute Only Once, Not on Proxy Calls
In Solidity, a constructor is executed only once—at the time the contract is deployed. But in an upgradable system, users do not interact with the implementation contract directly. Instead, they interact with the proxy, which delegates function calls to the implementation contract.
If an implementation contract contains a constructor, it will execute when the implementation is deployed, but not when the proxy calls it. This means any initialization done in the constructor will be lost, leaving the contract uninitialized when accessed through the proxy.
Example
Imagine we deploy an implementation contract with a constructor like this:
contract Implementation {
address public owner;
constructor() {
owner = msg.sender;
}
}
When this contract is deployed, owner
is set correctly. However, when a proxy delegates calls to this implementation, the constructor does not execute, meaning the owner
variable will remain uninitialized in the proxy’s storage.
2. Storage Misalignment Due to delegatecall
The delegatecall
function is what enables proxy contracts to interact with the logic stored in the implementation contract. However, delegatecall
executes the implementation’s code in the storage context of the proxy contract.
If the constructor initializes state variables, it does so in the context of the implementation contract’s storage, which is separate from the proxy’s storage. Since users interact with the proxy, any data stored in the implementation contract is ignored.
Incorrect Use of a Constructor in Upgradable Contracts
contract Implementation {
uint256 public value;
constructor(uint256 _value) {
value = _value;
}
}
If this contract is deployed as an implementation, the constructor runs once, setting value
. However, the proxy contract will not store this value, leading to unintended behavior when users interact with it.
How to Handle Initialization Correctly
Instead of using a constructor, upgradable contracts use an initialize
function that is explicitly called after deployment. This function sets up the contract state within the proxy’s storage.
Correct Approach: Using an Initializer Function
contract Implementation {
address public owner;
uint256 public value;
bool public initialized;
function initialize(address _owner, uint256 _value) public {
require(!initialized, "Already initialized");
owner = _owner;
value = _value;
initialized = true;
}
}
With this approach, the initialize
function must be manually called after the proxy is set up, ensuring proper state initialization inside the proxy contract’s storage.
Conclusion
Constructors are unsuitable for implementation contracts in upgradable systems because they execute only once at deployment and do not run when the proxy interacts with the contract. Instead, developers should use an initialize
function to set up contract state. This approach ensures that upgradable contracts function correctly, maintain storage consistency, and remain upgradeable in the future.