I’m trying to understand something about upgradable contracts that I can’t quite wrap my head around. I found an upgradable contract using a transparent proxy pattern, where the implementation contract uses immutable variables set in the constructor.
From what I understand, after an upgrade, the proxy wouldn't be able to read the new implementation’s immutable variables if they have changed. However, I’m not sure which part of the process would be causing the issue.
Here’s a minimal example
pragma solidity ^0.8.25;
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {ERC1967Utils} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol";
contract CustomProxy is ERC1967Proxy {
address private immutable _admin;
error ErrorProxy();
constructor(address logic, bytes memory data) ERC1967Proxy(logic, data) {
_admin = msg.sender;
ERC1967Utils.changeAdmin(_proxyAdmin());
}
function upgradeToAndCall(address newImplementation, bytes calldata data) external {
if (msg.sender != _proxyAdmin()) {
revert ErrorProxy();
}
ERC1967Utils.upgradeToAndCall(newImplementation, data);
}
function _proxyAdmin() internal view returns (address) {
return _admin;
}
}
Implementation 1 :
pragma solidity ^0.8.25;
abstract contract StorageModuleV1 {
uint256 public immutable VAR_ONE;
uint256 public immutable VAR_TWO;
constructor(uint256 varOne, uint256 varTwo) {
VAR_ONE = varOne;
VAR_TWO = varTwo;
}
}
contract CustomLogicV1 is StorageModuleV1 {
constructor(uint256 varOne, uint256 varTwo)
StorageModuleV1(varOne, varTwo)
{}
function getVars() external view returns (uint256, uint256) {
return (VAR_ONE, VAR_TWO);
}
}
Implementation 2:
pragma solidity ^0.8.25;
abstract contract StorageModuleV2 {
uint256 public immutable NEW_VAR_ONE;
uint256 public immutable NEW_VAR_TWO;
constructor(uint256 newVarOne, uint256 newVarTwo) {
NEW_VAR_ONE = newVarOne;
NEW_VAR_TWO = newVarTwo;
}
}
contract CustomLogicV2 is StorageModuleV2 {
constructor(uint256 newVarOne, uint256 newVarTwo)
StorageModuleV2(newVarOne, newVarTwo)
{}
function getNewVars() external view returns (uint256, uint256) {
return (NEW_VAR_ONE, NEW_VAR_TWO);
}
}
From what I understand, the proxy wouldn’t be able to read the new implementation’s immutable variables if they are changed after an upgrade. But I don't know why..
- Either the proxy has access to the bytecode and immutable variables of
CustomLogic
version 1, but notCustomLogic
version 2, for some reason? - Or the constructor of
CustomLogic
version 2 cannot be called for some reason, meaning the immutable variables in the new implementation are never set during the upgrade? - Or, the proxy somehow stores the immutable variables from
CustomLogic
version 1 in its state, making it impossible to change them after the upgrade?
I’m not sure which one it is, or if it’s something else entirely. Could anyone clarify how the proxy interacts with the immutable variables during an upgrade and why the new implementation’s immutable variables might be inaccessible?