We are proposing an EIP to standardise how proxies store the address of the logic contract they delegate to. Given that the delegating proxy contract pattern has become widespread, we believe there is value, especially for off-chain tooling and explorers, in having a standard storage layout for proxy-specific information. In particular, a storage layout based on the unstructured storage pattern.
We are pushing for a standard storage layout and not a standard interface since proxies are designed to act transparently for a user, and introducing proxy-specific functions can lead to attacks.
This EIP is designed to be a generalization for other delegating proxy standards that use the unstructured storage pattern, such as EIP-1822.
Delegating proxy contracts are widely used for both upgradeability and gas savings. These proxies rely on a logic contract (also known as implementation contract or master copy) that is called using delegatecall. This allows proxies to keep a persistent state (storage and balance) while the code is delegated to the logic contract.
To avoid clashes in storage usage between the proxy and logic contract, the address of the logic contract is typically saved in a specific storage slot guaranteed to be never allocated by a compiler. This EIP proposes a set of standard slots to store proxy information. This allows clients like block explorers to properly extract and show this information to end users, and logic contracts to optionally act upon it.
If you have keccak256('eip1967.proxy.implementation') the preimage is known, it is 'eip1967.proxy.implementation'. If you subtract one you get a random number with unknown preimage because keccak256 is a cryptographic hash.
beacon is simply a beacon with implementation() (and other things) while ProxyObject accepts a beacon in the constructor, stores it in the beacon storage slot, and declares a fallback function that reads that storage slot, calls implementation() then delegate-calls to it.
This would always revert due to function call to a non-contract account when doing beacon.implementation(), even though manually doing that method or even manually calling ProxyObject worked fine. Somehow ProxyObject being delegate-called by MainContract produced this weird error.
After a while, I figured out that since ProxyObject is delegate-called and not called regularly (or using STATICCALL), it is using the storage of MainContract which doesn’t contain the (right) beacon storage slot.
I “solved” this by storing the beacon address in an immutable field and defaulting to that if the storage slot is empty. Obviously not the best solution, but I can’t think of any other solution besides either:
Storing the ProxyObject's address in an immutable field during construction, and making it query itself for the beacon implementation. This would mean that ProxyObject requires a regular (non-fallback) function for this. Perhaps it could use some magic parameter and check msg.sender to detect when it’s a “request from itself” versus a regular proxy call it should delegate, but quite complex and counter-intuitive. Quite a fundamental (and complex) change though.
Simply not use EIP-1967 as it seems that it never anticipated the ProxyObject (the proxy contract with the beacon storage slot) to be delegate-called.
Is my reasoning in all this wrong, or did the EIP actually overlook this issue? I don’t see anything about this restriction (“proxy scripts that use beacon storage slots can’t be delegate-called”) in the EIP. I If that’s the case, perhaps adding a warning about this might not be a bad idea?
It is possible to use EIP-1967 in the scenario you describe. The storage variable just needs to be set in the “top level” contract, in your case MainContract. This is the case whenever there is more than 1 proxy layer, i.e. multiple delegatecalls in series.
The way the EIP should be interpreted is that the storage slots it specifies are only relevant in a call context where those slots are active. In a delegatecall context, the storage of the intermediate proxy is always ignored, so the EIP is not “in effect”.
Still, a warning wouldn’t be a bad idea though. After all, I was of the impression that whether a contract is a “real” contract or a proxy pointing to another contract shouldn’t matter. Calling the proxy and non-proxy the same way should have the same results (assuming same state/address/etc), and the same for delegate calls. It’s quite counter-intuitive that delegate-calling a proxy suddenly makes the proxy act as a “different” proxy (e.g. affected by your own beacon storage slot).
This issue should also happen for proxies that work by storing a logical implementation address instead of a beacon.
Also interesting, although not strictly about this EIP: delegate-calling a diamond (EIP-2535) would also suffer from this (it’d look for selectors in the caller’s DiamondStorage instead of the diamond’s). The reason why I’m mentioning this is because although it’s a bit too cumbersome for the “simple” proxies in this EIP (1967), it’s not unthinkable to deploy proxies (with or without beacons) pointing at a diamond, where upgrading the diamond would also “upgrade” all proxies pointing at it.
EDIT: Mentioned the diamond issue on the discussion for EIP-2535. I imagine that any progress/remarks both here and there affect both EIPs.
hi @spalladino and others, thanks for this incredible work. i have a few questions.
is the standard flow that the (initial) implementation contract needs to be deployed first, and then the proxy deployed?
isn’t it the case that someone could (if they wanted) invoke the implementation contract directly—and that if they did this, then there would be a “ghost” parallel state kept by the implementation contract, alongside that kept by the proxy contract?
Yep, but the point is which contract is the “legitimate” one. Using that argument, you could deploy a copy of a popular contract and start invoking it, but no one will pay attention to it.
There is one caveat to this: if the implementation contract has an instruction that can alter its code, then interacting with the implementation can lead to issues. The only opcode that can do this is SELFDESTRUCT, or doing a delegatecall to another contract with a SELFDESTRUCT operation.
great! many thanks for your responses. i hope you’ll humor me on another very basic question (which is more about Solidity overall than EIP-1967).
if i am understanding things correctly, in this pattern, it becomes difficult to use the usual visibility control of functions on the implementation contract. for example, it seems that an internal, but state-changing, function, on the implementation contract, could be invoked via this mechanism: after all, the “technical” entrypoint of the EOA call is the proxy’s fallback() external payable (which is of course external). but by the time we reach this function, an attacker (say) could submit calldata corresponding to an internal function of the implementation contract. he won’t be blocked, since he has already “gained entry” to the proxy contract, via the fallback, and delegatecall treats the (internal) functions of the implementation contract as internal functions on the proxy contract.
is this actually accurate, or am i mistaken? if so, are there any easy ways to deal with this? thanks again.
edit: looks like this was a misunderstanding on my part, apologies. by definition of delegatecall, it will take the calldata supplied to the proxy, look for an external function on the implementation matching the selector, and then invoke that external function using the supplied calldata. internal functions don’t even get selectors in the compiled implementation contract (rather, they’re “inlined”), so it doesn’t actually make sense to call one through delegatecall. so for all intents and purposes, the visibility works “as expected”.