PubSub Pattern in Solidity Smart Contracts
Rocket Pool is a complex beast with many interlocking parts. After spending some time working on the shiny, new Rocket Pool 2.0, we found that we had a number of contracts which needed to know about changes in others. Until recently, we had simply called their methods directly from external code, to alert them to these changes. This works, but it results in tight coupling between contracts and can make code difficult to follow and maintain.
An example of this tight coupling is the interaction that was present between two main Rocket Pool smart contracts:
RocketMinipool.sol. These two files manage some important aspects of Rocket Pool and each needs to be aware of changes that happen in the other.
For instance, a
RocketMinipool contract changes its status several times throughout its lifetime, from the “initialised” state, to “staking” with Casper, and so on. The
RocketPool contract needs to be notified of these status changes in order to perform tasks like assigning user deposits correctly. Previously, we accomplished this by having
RocketPool.sol’s methods directly whenever its status changed, making any necessary updates to its state.
Let’s look at our naïve implementation of updating the available minipool set:
This is something we’d like to avoid, but until very recently, that was almost impossible. With the introduction of Solidity 0.5.0 just a few weeks ago, we can now leverage some design patterns to decouple these contracts. Let’s go ahead and unravel this spaghetti code!
No More Tangles
We’ll enlist the help of an old standby for decoupling code: the Publish-Subscribe (or PubSub) design pattern. Note that this differs subtly from the venerable Observer pattern, in that we’ll use a single, global Publisher contract to broadcast messages. This will help keep our code lean, as we won’t need to extend a Publisher base class for every contract which needs to communicate with others.
Here’s our initial attempt at the PubSub contracts:
All references to storage are based on our upgradeable contract design pattern — a great way to improve contract design architecture if you’re concerned with upgradeability.
Note that modifiers have been applied to these methods to prevent access from arbitrary addresses, but have been omitted for brevity. We want to ensure that only we can modify the subscriber list, and only the subscriber contract can call corresponding notify methods!
Here’s our updated
RocketPool contract code:
We’ve successfully decoupled these contracts. At deployment, we send a transaction to
Publisher.addSubscriber to subscribe the
RocketPool contract to the
"minipool.status.change" event. Then, whenever a minipool’s status changes:
- It calls
publishmethod loops through the set of all contracts subscribed to the event, and calls the
SubscriberInterface.notifymethod on each of them
RocketPool.notifyis called, and can handle the event accordingly
There’s just one problem — the
RocketPool contract needs know the minipool’s address and new status in order to correctly add it to, or remove it from, the available set. We’ll need to pass this information along with the event, but we’d have to overload a couple of methods to do that:
This is far from ideal — we don’t want to have to overload our
notify methods for every different set of parameters we’re sending with our events. This is where tools like variadics / generics help in languages like C++ and Rust, but we don’t have that technology yet for Solidity. And even if we could dynamically generate the required methods at compile-time, we’d end up with some pretty heavy contract bytecode, which might not play well with EIP-170!
If only we could pass some arbitrary, encoded data in a single parameter, and decode it at runtime instead. But there aren’t any cheap, concise encoding utilities available in Solidity, right? Wrong!
Solc v0.5.0 to the Rescue!
Among the numerous updates to Solidity 0.5.0 is a new abi.decode function, complementing the existing abi.encode one. We can now pack some arbitrary data into a byte array, pass it to another function, and easily reconstruct the data in its original form.
Let’s see what the updated PubSub code looks like:
And once again, our updated
RocketPool contract code:
Of course, the abstractions we have introduced aren’t free. However, benchmarking the gas costs of transactions which invoke the
Publisher contract shows only a 5% increase in gas usage! In our opinion, this is a small price to pay for a design pattern which helps to keep code malleable and maintainable, and ensures that concerns are properly separated within a project.
It’s easy to dismiss the idea of using abstractions when working with the EVM; you could be forgiven for thinking that some design patterns simply aren’t viable due to the heavy focus on performance and optimisation. However, we discovered that it’s worth exploring all the options before writing them off — you might be surprised what you can do!
Of course, if you decide to use similar patterns in your own projects, it’s important to carefully consider the risks associated with decoding data at runtime. Make sure that you have total control over the data that gets passed to
abi.decode, and account for all the possible calls to it with a robust suite of unit tests!
Questions and Hellos
If you have questions or want to know a bit more about us, why not swing by for a chat and say G’day! You can view our website or have a chat with us in our chat room that anyone can join. If chat rooms aren’t your thing, we’re also on Twitter!
The gists embedded in this article are simplified versions of our contract code — to see the full versions in action, check out the Rocket Pool git repo!