"Advanced" Smart Contracting I

(Note: this article is intended to give readers a superficial understanding of things, with pointers to references that let you go deep into the sub-topics.)

So you’ve been writing smart contracts for a while now, and you’ve gone beyond the basic counter and PFP NFT stuff, venturing into DeFi or GameFi or MusicFi or WhateverFi with contracts spanning over the 24KB, and you’ve deployed things going beyond the first iteration, and you’re now building new features on top and suddenly you realize you wanna keep the same addresses and gotta break down the code cause it’s too big to fit into a single file and you’re wondering how did you get into this mess.

Welcome to Advanced Smart Contracting I. There will be a II, eventually. Maybe a III, possibly. More than that, idk.

Smart contracts are immutable

right? Right? RIGHT? Welcome to the “it depends” world, AKA The Real World, where things are never quite black and white.

When a contract is deployed, it goes and lives forever in some address in the blockchain, e.g. 0x…123, with some code and storage.Yes, that address is now forever associated with a piece of code that is immutable. But the data will most certainly change, unless you’re programming a no-so-smart brick contract.

Ok, but what am I trying to say? Well, the data storage of a smart contract can change. And there’s this thing called a fallback function, and this thing called delegatecall, and you mix these things you end up with something called the Proxy Pattern.

And what can you do with this Proxy Pattern? How about telling your function to point to a different smart contract to execute?

Yes, we now have a way for a smart contract to change the code that runs. Bye bye immutability, so long suckers!

Upgrades

What about this upgradeability thing? Our PFP NFT v1 has the images hosted on some Photobucket, and our users have complained about it because they don’t like it, so now we wanna put the images on-chain. But setImageURI isn’t enough, we gotta change the implementation - we gotta upgrade the contract.

Of course, there are different ways of doing this. The basic approach is to deploy the new version, migrate the data over to the new contract with the images on-chain - but we won’t be able to keep the same address (and that’s acceptable in most cases).

What if we want to keep the original contract address? Then we need to be a bit more sophisticated, and that’s where the Proxy Pattern can help us - the proxy (which is the original address) can point to a new implementation. But understand that depending on the complexity of the changes, you might be able to upgrade without a proxy.

But of course, we gotta be aware that these techniques exist before we deploy things (plus there are a few different ways to build proxies).

Here’s a good article about the specific topic of Upgrades.

Proxies

A proxy contract is simply a smart contract that sits in front of other smart contracts, and routes the call to an implementation contract.

Here’s a very basic but concrete example - it shows how we can “change” the implementation of the contract. In reality, the code of the smart contract isn’t changing, it’s the data that changes, but in practice this causes different code to execute!

Example of a Basic Proxy
Example of a Basic Proxy

The above example is exposing an interface (the sayGM function) in the ProxyContract , which itself keeps track of an interface (IGMImpl) to call.

But the EVM makes it possible to bypass that, and have an ultra-generic proxy that can hold any function!!!

fallback + delegatecall

The fallback function is a special function in a smart contract, and

the fallback function is executed when a function call does not match functions specified in a contract.

i.e. if someone calls your contract with a function that’s not explicitly defined in the contract, then fallback() is executed.

The delegatecall is

an opcode that allows a contract to call another contract, while the actual code execution happens in the context of the calling contract

The previous example actually shows a delegatecall, but it’s not explicitly mentioned; When the ProxyContract executes impl.x(), it’s actually doing a delegatecall to the implementation contract (GM1Contract or BDContract or GM2Contract).

So by joining these two things, you can write a Proxy Contract that is ultra-generic, because it does not need to know what interface to expose!

Making the proxy pattern work requires writing a custom fallback function that specifies how the proxy contract should handle function calls it does not support. In this case the proxy's fallback function is programmed to initiate a delegatecall and reroute the user's request to the current logic contract implementation.

Ok, I’ve told you about smart contract immutability, upgrades, proxies, fallback and delegatecall. Now what?

(PS: what I didn’t talk about is how the ProxyContract is the one to hold the state AKA the data, which means changing implementations must be done carefully, because it can screw up the data because it’s based on the memory layout.)

(PSS: I also didn’t talk about this thing called selector clash that happens when the ProxyContract and the ImplementationContract have the same function and delegatecall now isn’t sure which function to execute and this can cause headaches.)

The Many Roads to Rome

Or from Rome?

Ok, so everyone’s using pretty much the same Proxy Contract (or something very similar), since it’s ultra-generic and actually solves all the requirements, which is kinda crazy cause this never happens.

So when do things start to differ?

Well, it’s when you start going into how to organize things.

Kinda Transparent
Like, if you want things to be upgradeable, that means you want to be able to change e.g. the address of the implementation contract - in the previous example, that would be calling setImpl(newImpl). So now your Proxy Contract isn’t just the fallback function, and starts to have other functions, and most likely these should be guarded by some control access like the onlyOwner modifier (or something more sophisticated), and you might even have to tweak your fallback to check for the admin because you want to avoid selector clashes, and now you end up implementing what’s called the Transparent Proxy Pattern.

With this pattern, the code to upgrade is inside the ProxyContract itself, like in the previous example.

UUPS not UPS
What if you don’t want to keep the code to upgrade in the ProxyContract? Well, you can put it inside the ImplementationContract, and that’s what the Universal Upgradeable Proxy Standard is all about.

Following the example, that means that IGMImpl should have a setImpl function declared, and GM1Contract, BDContract, GM2Contract would have to implement it.

Lighthouse would be a better name
You heard that All problems in computer science can be solved by another level of indirection (and that’s actually what a Proxy is!), so you became one of those modularity freaks who decides to factor out the address of the ImplementationContract and move it to its own smart contract called ImplRetriever with a getImpl(), and have the ProxyContract do implRetriever.getImpl().x() because that’s so much better.

Well done, you’re building a Beacon Proxy. This is a useful pattern when you have multiple proxies sharing the same implementation (why would you do that? well…), and it’ll make it easier for you to change the pointer to the implementation.

Diamonds are Forever
Do you wanna get extra fancy? Just get a Diamond, and you’ll be able to split your implementation logic into multiple implementation contracts, and do things like partial upgrades, and have a “safe” buffer for large contracts.

It really works, but you should read the ERC to get a full grasp of things.

Routers in Proxyland
This great talk about proxies goes in-depth into the technical issues with proxies, and how to solve them, and also comes up with a variation of the Diamond Standard, which they call Router Proxies.

Final Thoughts

There’s a strong current of thought that tells you to avoid proxies and upgrades at all costs, and that smart contracts should be purely immutable. And I don’t disagree with it, since it makes things so much easier to reason about, and it makes change explicit and directly visible, which is one of the pillars of blockchain.

But YMMV.

Subscribe to omnifient
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.