Tutorial 37: initializer and onlyInitializing

In smart contracts, especially in upgradeable contracts, the initialization process is used to set up the contract's state and configure its settings. A special function, often called initialize(), is typically used instead of the constructor because constructors do not work well with proxy contracts (commonly used for upgradeable patterns).

An initialization vulnerability occurs when the initialization function is incorrectly implemented, leading to the contract being locked out of initialization or reinitialization after upgrades. This type of vulnerability is particularly concerning in upgradeable contracts because it can prevent new functionality from being properly configured, potentially breaking the contract.

In this context, initialization should be carefully managed using modifiers like initializer and onlyInitializing to ensure that contracts can be upgraded safely without accidentally blocking further updates.


Understanding the EIP-712 Meta-Transaction Initialization Vulnerability

The EIP-712 standard is commonly used to sign messages and verify the authenticity of transactions, enabling meta-transactions. A meta-transaction allows users to submit a transaction without needing to pay for gas fees themselves. Instead, a third party (the relayer) pays the gas fees on behalf of the user. To ensure the security of meta-transactions, proper initialization of the EIP-712 domain is critical.

In a real-world vulnerability example related to the EIP-712 Meta-Transaction implementation, the contract's initialization process was incorrectly handled. The contract used the initializer modifier in its initialization function, which prevented reinitialization after upgrades. This could result in the contract being permanently locked out from setting up the EIP-712 domain again during contract upgrades, potentially leading to failure in the meta-transaction mechanism.

Problematic Code:

function initializeEIP712(string memory _name, string memory _version)
    public
    initializer
{
    name = _name;
    version = _version;

    __EIP712_init(_name, _version);
}

In this example, the initializer modifier was used, which restricts the function from being called more than once. In the case of upgradeable contracts, each upgrade might require reinitialization to set up the new state. The use of initializer in this context prevents that, causing reinitialization to fail and making the contract inoperable after upgrades.


Consequences of Incorrect Initialization

  1. Contract Lockout: Using the wrong initialization function can prevent upgrades from being properly configured. If the initialization process fails, new contract logic may not function as expected, leading to partial or full contract lockout.

  2. Reentrancy Issues: Incorrect use of initialization can lead to reentrancy attacks or the contract perceiving itself as being in a state of initialization when it is not, causing security issues in future contract executions.

  3. Upgrade Failures: If the contract cannot be reinitialized after an upgrade, new versions of the contract may not be able to configure crucial state variables, resulting in unexpected behavior.


Example Scenario: Incorrect Initialization in EIP-712 Meta-Transactions

Let’s go through a hypothetical scenario where the incorrect implementation of the initialize function leads to issues in the upgradeable contract.

  1. Initial Deployment: The EIP-712 Meta-Transaction contract is deployed with the initializer modifier. During the initial deployment, the contract is correctly initialized with the name and version for EIP-712 signing.

  2. Contract Upgrade: Six months later, the contract is upgraded to introduce a new feature, and a new version of the EIP-712 domain is required. However, since the initializer modifier was used, the upgrade process attempts to reinitialize the contract but fails. The contract reverts because the initializeEIP712 function can only be called once.

  3. System Failure: Due to the failed reinitialization, the meta-transaction feature stops working as the new domain data is never set. Users can no longer sign transactions, and the entire upgrade is deemed a failure.


Mitigation Strategies

To prevent initialization vulnerabilities, developers must ensure that initialization functions are implemented correctly in upgradeable contracts. Below are the steps to mitigate the risk:

1. Use onlyInitializing Instead of initializer for Upgradeable Contracts

In upgradeable contracts, use the onlyInitializing modifier rather than initializer when creating functions that need to be called during upgrades. This allows the function to be executed during upgrades, without restricting it to a single use.

The onlyInitializing modifier ensures that the function can be called whenever the contract is being initialized or reinitialized during an upgrade, preventing the contract from locking out future upgrades.

2. Use the Correct Modifier for Initialization

  • Ensure that you use initializer only for functions that should be called once (during the initial deployment).

  • For functions that need to be called on upgrades, always use onlyInitializing to allow multiple initializations during the contract's lifecycle.

3. Make Use of Proxy Patterns

  • In upgradeable contracts, use proxy patterns to ensure that contracts can be safely reinitialized after each upgrade. Proxy patterns allow the logic contract to be upgraded while maintaining the state, ensuring that initialization functions can be used properly.

4. Leverage OpenZeppelin Contracts

  • Use trusted libraries like OpenZeppelin to handle common contract patterns. OpenZeppelin provides well-audited and secure implementations of initialization logic for upgradeable contracts, including EIP-712 support.

Last updated