right, my mistake; really what i meant is they don’t get selectors (or at least aren’t matched against when doing an external call). another tricky case is external view
functions. it seems that these do have selectors, and are routed through the same delegatecall
mechanism that mutating external
calls are routed through. this is the only way that read-only functions could be directed to the proxy and work (even though the code isn’t there).
a final question—huge thanks for your patience.
can you clarify the meaning of:
This function does not return to its internal call site, it will return directly to the external caller.
(see EIP-1967 for all refs.)
by “internal call site”, do you mean essentially back to the body of _fallback()
(the only place where _delegate()
is called)? if so, then i don’t see the significance of this, since _delegate()
is the final operation called within _fallback()
, and _fallback()
is moreover itself the final operation in both fallback()
and receive()
, the only places where it’s called. so it seems to me to amount to the same thing (at least functionally speaking) whether _delegate()
returns to its internal call site or not.
i guess mechanically, the reason it doesn’t return to its internal call site stems from the semantics of the return
Yul instruction. i was aware that revert
returns to the next-outermore call
er, but i guess it’s not surprising that return
also does this.
so i guess ultimately this is a point about solidity memory management. i take it it’s safe to overwrite the memory location 0
as long as you’re not returning to the internal call site (?). if so, why is this roughly?
what is the downside—besides possibly very slightly higher gas—of implementing _delegate()
this way (differences marked)?
function _delegate(address implementation) internal virtual {
assembly {
let location := mload(0x40) // <--- notice this
calldatacopy(location, 0, calldatasize()) // <--- and this
let result := delegatecall(gas(), implementation, location, calldatasize(), location, 0) // <--- etc
returndatacopy(location, 0, returndatasize())
switch result
case 0 {
revert(location, returndatasize())
}
default {
return(location, returndatasize())
}
}
}
in fact, it seems that the following also works:
function _delegate(address implementation) internal virtual {
(bool success, bytes memory data) = implementation.delegatecall(msg.data);
assembly {
let size := mload(data)
switch success
case 0 {
revert(add(data, 0x20), size)
}
default {
return(add(data, 0x20), size)
}
}
}
perhaps it’s a matter of taste, but it seems arguably more aesthetically appealing. it may be possible to get rid of the inline assembly altogether; i’m not sure.
edit: i can confirm that “my” way leads to slightly larger (~234 bytes) bytecode. not sure exactly about the gas.
Rationale is that memory is cleared across EVM CALLs, so if _delegate is always returning from the current CALL frame, then memory will be cleared anyway, so it’s safe to overwrite.
Yup, my guess is that this implementation would also work.