Audit Anomalies Archive — Issue#5
In the world of smart contracts, upgradable contracts have gained significant attention due to their flexibility and ability to adapt to changing requirements. These contracts follow the Proxy pattern, a design pattern that separates the logic and storage of a contract. The logic resides in one contract, while the storage is managed by a proxy contract, which uses delegatecall
to execute logic. However, there are important nuances to consider when building upgradable contracts, and one of them is related to contract initialization.
When it comes to initializing the logic contract in an upgradable contract, traditional constructors do not work as expected. This is where the OpenZeppelin Upgrades library comes into play. The library provides a solution to this challenge by introducing a special way to initialize contracts, protected by the initializer
modifier. Instead of relying on constructors, you can now use the initialize
function to set up your contract's initial state.
For example, OpenZeppelin offers functions like __ERC20_init()
and __Pausable_init()
in their openzeppelin-contracts-upgradeable
library. These functions allow you to initialize ERC-20 tokens and pauseable contracts seamlessly, ensuring proper contract setup without relying on constructors. A similar example can be found below.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
contract MyToken is Initializable, ERC20Upgradeable, OwnableUpgradeable {
// Initialize the contract
function initialize(string memory name, string memory symbol, uint256 initialSupply) initializer public {
__Ownable_init();
__ERC20_init(name, symbol);
_mint(msg.sender, initialSupply);
}
// Additional functions can be added here or in subsequent upgrades
}
Now, let’s consider a scenario where an upgradable contract diverges from the OpenZeppelin Upgrades library’s best practices. Instead of using the OwnableUpgradeable
library provided by OpenZeppelin, the contract uses the standard Ownable
library, setting the owner as part of the constructor. This decision can have significant consequences.
contract SafeOwner is UUPSUpgradeable, Initializable, Ownable {
bool public collectFee;
function initialize() public initializer {
collectFee = true;
}
}
In this scenario, the owner is never properly set due to the Proxy pattern. As a result, functions protected by the onlyOwner
modifier become inaccessible, potentially compromising the contract's intended functionality.
The query to the owner state variable returns address(0)
For a practical demonstration of this issue via Foundry, you can find more details in this link.
Thank For Reading.