A more gas-efficient upgradeable proxy by not using storage

Hey there! After working on compiling the state of smart contract upgrades, I wanted to try out a variation on a pattern, to see if I could get a more gas-efficient proxy. Here I’m focusing on less gas overhead per call, but the end result seems also cheap in terms of deployment, since the proxy itself ended up being pretty thin.

The pattern I wanted to build on was the beacon proxy built by 0age, in which proxies don’t keep the implementation address, but an immutable reference to a beacon contract. The beacon, in turn, keeps track of the current implementation in storage. Whenever the proxy needs to delegate a call, it CALLs the beacon to fetch the current implementation. Whenever the admin needs to upgrade, they just upgrade the beacon, effectively upgrading multiple proxies in a single tx.

Given that the goal was to reduce gas cost, I looked into removing the most expensive operation: loading the implementation address from storage with an SLOAD. Now, the only alternative to storage for saving persistent data in an Ethereum contract is code - and it can actually be more gas efficient after the Istanbul gas repricing, as @Agusx1211 points out in this article.

The solution I ended up with is storing the implementation address in the beacon in code, and loading it from the proxy directly via EXTCODECOPY, effectively requiring no SLOADs during a CALL. Beacons are deployed using CREATE2 and dynamic runtime code, so to perform an upgrade they can be selfdestructed and recreated. To prevent the downtime associated with destroying and recreating the beacon, each proxy has two beacon: a main one, and a fallback that is used only when the first one is destroyed.

I ran this implementation versus other popular upgradeable proxy patterns, and it outperforms all of them in terms of gas overhead per call. It also has the nice property of beacons that multiple proxies can be upgraded with a single tx.

Proxy Gas cost Gas overhead
OpenZeppelin Transparent 29815 2770
Dharma Beacon 29752 2707
EIP-1882 UUPS 28679 1634
Storageless Beacon 28629 1584

The code for the solution can be found in the repo below. Keep in mind that this is just a proof of concept, and should not be used in production. I’d love to hear your thoughts about it!

4 Likes

Interesting, I’d like to see the result of your gas overhead test with a diamond.

1 Like

Very interesting take on the beacon pattern and that’s a very nice gas reduction. Tbh it’s a pitty that solc doesn’t support the inmutable keyword inside assembly blocks and that you had to play around it.

Personally I like the original beacon implementation because of the readability aspect, which suffered an important reduction, as it should be since you are trying to gas golf it to the max, especially in the Proxy implementation and the proxy creation, the later because of having to play around solc.

The only part that I do not fully understand is choosing to maintain a second beacon, I understand the CREATE2 redeployment caveat but unless this “backup beacon” maintains a very thin implementation (or even an scape hatch like the Dharma wallet) then, as I see things, it would mean that every beacon upgrade is actually followed by a backup beacon upgrade (?)

But, on the other hand if you were to maintain a second beacon with a scape hatch implementation then you would be running into a trust issue if this second beacon is also destructible and upgradable… (I might be overthinking this a little bit haha)

btw, I think it would be great if you did an article on how to keep the init code of a contract static to achieve deployment/re-deployment on the same address using CREATE2 maybe explaining different approaches like using inmutable storage + calling a “controller contract” or approaches like Metamorphic contracts. I know you just did an article about upgrades but I think these specific techniques didn’t get a deep explanation.

All in all, great stuff.

1 Like

What’s the motivation behind extcodecopying from the middle of a contract instead of just using a call? My guess is the gas savings aren’t huge since extcodecopy also has a base cost of 700, and extcodecopy(main, 0, 127, 20) strikes me as extremely fragile. The offset to the that address in a compiled solidity contract is going to depend on everything – solc minor version, optimizer enabled or not, optimizer runs, order of variables, etc.

1 Like

I’d expect it to as expensive as UUPS, since it requires at least an SLOAD. Possibly slightly higher as the code is not as straightforward. I’m not sure how many SLOADs are required per call on a diamond, or if it depends on which of the three diamond versions is used.

Chris commented today suggesting just “copying” it in the method before using it. It would greatly simplify the implementation.

The code for the backup beacon is exactly the same as the one for the main beacon. And yes, every upgrade requires two txs: one for creating the backup and destroying the main one, and another for re-creating the main one with the new address and destroying the backup.

Good idea, thanks! I feel there must be something out there already, but if not, I’ll be happy to write something about it.

They aren’t huge, but given this code gets run on every single call, I wanted to optimize as much as possible. Gas savings are mostly related to the fact that, even though both have the same base cost, the copy doesn’t require executing any code at all. And yes, this is super fragile without some tooling that verifies it. Hence the warning of not using in production!

2 Likes