Introduction
In traditional Solidity contracts, a constructor is a special function that runs once during deployment to initialize state variables. However, when it comes to upgradeable smart contracts—where logic is separated from state via a proxy—using constructors in the implementation (logic) contracts becomes problematic. Instead, developers rely on initializer functions. In this article, we explore how creation and deployment code work in Solidity and why constructors are not used in implementation contracts, with practical code examples.
How Constructors Work in Solidity
Creation Code vs. Runtime Code:
When a contract is deployed, the EVM executes the creation code—which includes the constructor. Once execution finishes, the constructor’s code is discarded, and only the runtime code (the public interface and functions) is stored on-chain.Initialization at Deployment:
In a non-upgradeable contract, the constructor sets up initial state and is run in the context of the freshly deployed contract’s storage.
Example: Non-upgradeable Contract Using a Constructor
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract NonUpgradeable {
uint public value;
address public owner;
// Constructor is executed during deployment.
constructor(uint _value) {
value = _value;
owner = msg.sender;
}
function setValue(uint _value) public {
value = _value;
}
}
In the above contract, the constructor initializes the state variables when the contract is deployed. This pattern works perfectly when the contract is deployed directly.
The Upgradeable Contract Paradigm
Upgradeable contracts separate the logic contract (or implementation contract) from the proxy contract that holds the state:
Proxy Contract:
This contract stores state variables and delegates calls (usingdelegatecall
) to the implementation contract.Implementation Contract:
Contains the business logic. However, when a proxy is used, the implementation’s constructor is not called in the proxy’s context. Instead, the proxy must be separately initialized.
Why Constructors Fall Short in Implementation Contracts
No Proxy Storage Initialization:
In an upgradeable setup, the proxy holds the state. A constructor in the implementation contract runs only during its own deployment. As a result, any state initialized in the implementation contract’s constructor is not applied to the proxy’s storage.Delegatecall Context:
The proxy delegates calls to the implementation contract usingdelegatecall
, which executes the implementation’s functions in the context of the proxy’s storage. Constructors, however, run only during deployment and are not part of the runtime code executed via delegatecall.Upgradeability Requirements:
Upgradeable contracts need a mechanism to initialize (or reinitialize) state after the proxy is deployed. Constructors can’t be re-run, so developers use initializer functions (often protected by aninitializer
modifier) that can be explicitly called on the proxy.Avoiding Immutable Settings:
Using a constructor in an implementation contract might inadvertently set immutable state values that cannot be changed later. Initializer functions are more flexible for setting up state in the proxy.
Code Samples: Constructor vs. Initializer
Implementation Contract with a Constructor (Problematic for Upgradeability):
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract ImplementationWithConstructor {
uint public value;
address public owner;
// Constructor initializes state during its own deployment,
// but this does NOT affect the proxy's storage when used with delegatecall.
constructor(uint _value) {
value = _value;
owner = msg.sender;
}
function setValue(uint _value) public {
value = _value;
}
}
When used as the logic contract behind a proxy, the constructor of ImplementationWithConstructor
runs only when the implementation is deployed. The proxy, however, never runs this constructor, leaving its state uninitialized.
Upgradeable Implementation Contract Using an Initializer Function:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// Import OpenZeppelin's Initializable contract.
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract UpgradeableImplementation is Initializable {
uint public value;
address public owner;
// Instead of a constructor, we use an initializer function.
// The `initializer` modifier ensures this function can be called only once.
function initialize(uint _value) public initializer {
value = _value;
owner = msg.sender;
}
function setValue(uint _value) public {
value = _value;
}
}
In this upgradeable pattern:
The
initialize
function takes the place of the constructor.The proxy, once deployed, will delegate calls to the implementation contract.
After setting up the proxy, the
initialize
function is called on the proxy, ensuring the proxy’s storage is correctly initialized.
Best Practices for Upgradeable Contracts
Use Initializer Functions:
Replace constructors with functions (commonly namedinitialize
) and protect them with an initializer modifier (such as the one provided by OpenZeppelin) to ensure they are executed only once.Avoid Immutable Variables in Implementation Contracts:
Immutable variables are set during construction and inlined into the bytecode. They do not work correctly in a proxy pattern where the state is held by the proxy.Keep Logic and State Separate:
Ensure that the implementation contract focuses solely on business logic. All state variables that need initialization should be set via the proxy’s initializer.
Conclusion
Constructors are a fundamental part of traditional Solidity contracts, but their one-time execution during deployment makes them unsuitable for implementation contracts in upgradeable architectures. Since the proxy pattern requires state to be managed separately in the proxy, initializer functions provide a flexible and safer alternative. By understanding the distinction between creation code and runtime code—and how they interact with delegatecall—we can appreciate why constructors are deliberately omitted from implementation contracts in modern, upgradeable smart contract designs.
References:
Solidity Docs
(Solidity documentation on creation and deployment code)