DEV Community

Cover image for Deep Dive in Transparent Proxy Code
Sebastien Levy
Sebastien Levy

Posted on

Deep Dive in Transparent Proxy Code

Intro

In a previous article I made a presentation on Transparent Upgradeable Proxies in Solidity; what they are, why they are useful and key concepts about how they work.

I let readers to dive in at their leisure in the code in this repository should they wish to know more.

I had some feedback though telling me it could be valuable to expose this proxy implementation code alongside precise explanations about how it works.

So here we are, 100% proxy, 100% focus.

Context

Let's keep something that is working by taking some code from The Nifty repository :

Study of TransparentUpgradeableProxy.sol

This is the actual transparent proxy implementation.
In this section I'll make a parallel between concepts I exposed in this article and the actual code.

Storage Slots

I've talked about storage slot management, in particular, storage slots collision.
Remember, a proxy call effectively calls an underlying implementation contract function but all state management is done within the proxy. This is the basis of how delegatecall works.

You see, this proxy implementation has also some states to manage:

  • the address of the current administrator of the proxy
  • the address of the current implementation contract to call functions of.

If those states were declared as they are in the implementation contract (or any normal contract), they would collide together and things would get very hot, very soon.

But wait... there are no state member within this proxy contract.

The only thing you have are:

  • weird constants whose values are provided (for gas optimizations)
  • a modifier and functions using those weird constants through a weird type named ProxyStorage

Weird constants

You can see they are result of hashing a string. In fact, the result of this hashing will give us the slot number where to store state of this proxy.

Therefore, ADMIN_SLOT will be the slot where we'll store the address of the proxy admin and IMPLEMENTATION_SLOT, the slot where we'll store the current underlying implenetation contract.

The library

Let's take a look at the ProxyStorage.sol file.

It defines a solidity library. A library can be seen as a base contract whose exposed function are delegatecalled somehow, allowing those called function to modify the state of the caller contract (libraries in solidity).

This library only expose one function which gives us a reference on a stored struct object.

Keep in mind the stored struct object lies in the caller contracts in our case: TransparentUpgradeableProxy.

This function is quite low level as it uses inlined assembly directive.
This is to override the default compiler behavior.
By default, the compiler store states of the contract at the first slot available starting at 0 and going forward (at leat for value types).
With this function, we can choose the slot (the location) of the TransparentUpgrageableProxy state variables thus, avoiding collisions with underlying implementation contract whose layout is deduced by the compiler default behavior I told just before.

assembly ("memory-safe") { // memory-safe indeed, we just change a slot number
      r.slot := slot // define the slot number of the returned struct object
}
Enter fullscreen mode Exit fullscreen mode

Therefore, in the transparent proxy implementation a function such as

function getAdmin_() private view returns (address) {
    // returns the value (address) stored in the ADMIN_SLOT slot number
    return ProxyStorage.getAddressSlot(ADMIN_SLOT).value;
}
Enter fullscreen mode Exit fullscreen mode

ensures we do not collide with any underlying implementation contract state.

Call delegation

This key concept of transparent proxy deserves a bit of explanation.
To put simply, the proxy effectively use of delegatecall but it is fine tuned for several reasons. Let's explore them:

  1. Gas efficiency
    • Any, I say any non-admin function calls to an underlying implementation contract pass through the proxy. Gas saving is paramount. This is why assembly construct is used here as we'll see
  2. call delegation through fallback function
    • The fallback function is a particular beast. It does not return any value. But proxy delegated calls must work with functions returning values. Once more, assembly to the rescue!
  3. standardization
    • In fact, this implementation of delegated call is the same as the one in the OpenZeppelin repository. Understand that it's a super standard way to implement transparent proxy. I did not invent anything new here.

Here's the code:

function fallback_(address implementationContract) private {
    // we'll see why in the next section about transparency
    require(msg.sender != getAdmin_(), InvalidAdminCall());

    assembly {
      // Copy msg.data.
      // gas saving (no bytes allocation)
      // no harm due to scratch pad over-writing because there is no solidity code after this inline assembly block
      calldatacopy(0x00, 0x00, calldatasize())

      // this is the actual delegatecall
      // Call the implementation.
      // offset 0 and size unknown
      let result := delegatecall(gas(), implementationContract, 0x00, calldatasize(), 0x00, 0x00)

      // copy return data starting from offset 0 using return data size we know at this point
      returndatacopy(0x00, 0x00, returndatasize())

      // either returns or revert, using return data size
      switch result
      case 0 { revert(0x00, returndatasize()) }
      default { return(0x00, returndatasize()) }
    }
}
Enter fullscreen mode Exit fullscreen mode

Transparency

In this project, the proxy must behave differently depending who's using it. Put simply:

  • If the sender is the proxy admin, call proxy function without forwarding to the underlying implementation. Calling an unexisting function leads to revert.
  • If the sender is not the proxy admin, delegate function call to the underlying implementation even if this function exists in the proxy.

In the code, this is represented by the onlyAdmin modifier used in proxy getter functions (admin and implementation):

modifier onlyAdmin() {
    if (getAdmin_() == msg.sender) {
      _; // call of the proxy function
    } else {
      // delegatecall to underlying implementation      
      fallback_(ProxyStorage.getAddressSlot(IMPLEMENTATION_SLOT).value);
    }
}
Enter fullscreen mode Exit fullscreen mode

Some experts reader might say writing modifier like this is an anti-pattern. I understand. Specify if-else construct might obscure the intent of such modifier this is true. But in my case, I find it acceptable and it removes duplication; proxy are truly very specific and this code is not intended to change a lot.

Conclusion

I hope this deep tech dive into transparent proxy code is clear.
I'd like to remind you though this implementation is functional, it is largely imperfect and can be enhanced a lot. Always prefer stardardized solution such as Open Zeppelin for your proxy needs.

Thanks

  • @christopherkade for it's banner generator
  • Open Zeppelin hard work that inspired me
  • Ethereum ecosystem and community for the hard work they have been providing.

Top comments (0)