Hi,
I'm reading this part about the Proxy forward.
A very important thing to note is that the code makes use of the EVM’s
delegatecall
opcode which executes the callee’s code in the context of the caller’s state. That is, the logic contract controls the proxy’s state and the logic contract’s state is meaningless. Thus, the proxy doesn’t only forward transactions to and from the logic contract, but also represents the pair’s state. The state is in the proxy and the logic is in the particular implementation that the proxy points to.
Based on my understanding of the upgrade mechanism and the assembly code listed in the page, wanna confirm my below thinking is correct.
When are the memory slots of variables declared impl contract allocated on proxy contract?
Only when we call some initialize functions, it will allocate corresponding memory slot for those variables in proxy contract.
Example impl contract
contract MyToken is
Initializable,
ERC20Upgradeable,
AccessControlUpgradeable,
UUPSUpgradeable
{
bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");
int128 public x;
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() initializer {}
function initialize() public initializer {
x = 100;
__ERC20_init("MyToken", "MTK");
__AccessControl_init();
__UUPSUpgradeable_init();
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(UPGRADER_ROLE, msg.sender);
}
function getX() public view returns (int128) {
return x;
}
function _authorizeUpgrade(address newImplementation)
internal
override
onlyRole(UPGRADER_ROLE)
{}
}
e.g. the x = 100
behavior.
What if we didn't initialize a field at all and then trying to get its value?
Example impl contract:
contract MyToken is
Initializable,
ERC20Upgradeable,
AccessControlUpgradeable,
UUPSUpgradeable
{
bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");
int128 public x;
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() initializer {}
function initialize() public initializer {
// didn't initialize
// x = 100;
__ERC20_init("MyToken", "MTK");
__AccessControl_init();
__UUPSUpgradeable_init();
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(UPGRADER_ROLE, msg.sender);
}
function getX() public view returns (int128) {
return x;
}
function _authorizeUpgrade(address newImplementation)
internal
override
onlyRole(UPGRADER_ROLE)
{}
}
Let's say we didnt initalize the x
anywhere. The proxy will not declared the memory slot in its realm right? And if we call getX()
, what will happen?
From my experiment, it will return 0
which is the default value of this type.
- How I understand the return value here? Is it meaningful. Like we when I call
getX()
, the compiled assembly will check if there's a corresponding slot forx
, if not, declare new slot on the way. - I think above question is more like how should I understand the
delegatecall
works. Like, whenever a delegatecall happens, the whole impl's context (memory slots etc.) will be copied into proxy's domain?
Sorry for many details asking and thanks in advance.