Missing Gap Variable
Overview
- Severity: Low
- Confidence: Medium
- Affected Versions: All
What is the Missing Gap Variable vulnerability?
The popular collection of open-source smart contracts provided by OpenZeppelin includes a number of upgradeable contracts which can be use to evolve smart contract functionality over time by using proxies. If using these implementations, it is important to reserve storage space for future use; typically (and as recommended by the OpenZeppelin documentation), this is via an array variable conventionally named __gap
. Failure to reserve storage space may cause future upgrades to undesirably modify existing storage variables.
Further reading: OpenZeppelin Documentation: Storage Gaps
Technical example of vulnerable code
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract ExampleUpgradeableContractV1 is Initializable {
// State variables in the initial version
address owner;
mapping(address => uint256) public balances;
// Initializer function
function initialize(address _initialOwner) public initializer {
owner = _initialOwner;
}
function adjustBalance(address _toAdjust, uint256 _newBalance) public {
require(msg.sender == owner, "Only the owner may directly adjust balances.");
balances[_toAdjust] = _newBalance;
}
// Notice: No storage gap reserved for future upgrades
}
In the example above, contract ExampleUpgradeableContractV1
represents a toy version of an upgradeable OpenZeppelin contract. Note that this contract fails to reserve storage space for future upgrades. If a future version requires additional state variables to support new functionality, the existing storage layout could potentially be compromised.
Technical example of how to fix the vulnerability
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract ExampleUpgradeableContractV2 is Initializable {
address owner;
mapping(address => uint256) public balances;
uint256[50] private __gap; // Storage space reserved for future versions
// Initializer function
function initialize(address _initialOwner) public initializer {
owner = _initialOwner;
}
function adjustBalance(address _toAdjust, uint256 _newBalance) public {
require(msg.sender == owner, "Only the owner may directly adjust balances.");
balances[_toAdjust] = _newBalance;
}
// Additional functionality omitted for brevity
}
In the revised example above, contract ExampleUpgradeableContractV2
includes a storage array variable large enough to hold 50 uint256
values named __gap
per convention, reserving space for future upgraded functionality.