Dark Forest

zkSNARK space warfare

The latest release of Dark Forest v0.6, Round 5, probably has the most new features since the first release of v0.6—which was over 9 months ago!

This was possible due to a refactor of our underlying smart contract code. In previous rounds, adding new features was quite complex due to hitting contract size limits. Prior to the improvements, every new feature would require adding a new contract or refactoring existing contracts into libraries, both of which added dev time and complexity to the codebase.

If you dove into the smart contract code previously, you probably noticed we had 3-4 different contracts that any game client needed to know about. Plumbing their addresses and ABIs through clients and projects have long frustrated the core team and third-party developers.

Converting to Diamonds

To solve these issues and make some other exciting features possible, I dedicated time during our development cycle of Round 5 to replace our proxy/upgrade system with the Diamond Standard, EIP-2535.

Diamonds are a proxy pattern for Solidity development that allows a single gateway contract to proxy calls and storage to any number of other contracts. This provides a single interface for anyone to use your contracts, while allowing your feature set to grow into many contracts. The Diamond Standard also allows for replacing or extending functionality after your contracts are deployed. We actually used this “upgradability” to fix some problems in Round 5.

I highly recommend reading the specification and some of the excellent blog posts about it, such as Introduction to EIP-2535 Diamonds and The Diamond Standard: A new paradigm for upgradeability. I referenced these during implementation and in my documentation for Dark Forest.

We implemented the diamond-2 reference implementation from Nick Mudge, the EIP author himself. Additionally, we found the SolidState Contracts by Nick Barry which provides shared libraries that work well with the Diamond Standard. We previously used the OpenZeppelin contracts, but they don’t support the Diamond Standard yet. In addition, we open sourced a hardhat plugin for generating a “Diamond ABI”—an ABI that contains all functions a Diamond provides. We’re also experimenting with other Diamond tooling that you can check out in the Dark Forest contracts repository. Eventually these will be turned into tools other people can install and use, but we want to be confident in them first.

Diamond storage

Since the Diamond Standard owns and provides all storage to the facets, the smart contracts that are being proxied, I needed to decide on the right storage pattern for Dark Forest. Originally, I thought that the AppStorage pattern was going to be right for us, since our old contracts used nearly the same pattern; however, it quickly became apparent that the Diamond Storage pattern gave us more flexibility. When I discovered the SolidState Contracts also used Diamond Storage, it was decided. This also allows us to organize subsets of our storage into different namespaces, for easier use by everyone. For example, we created a single getter function that returns every game constant set when initializing a Dark Forest universe. You can see in our LibStorage that we store the GameConstants struct at the storage position of keccak256("darkforest.constants.game").

In the future, different features could provide storage in their own namespace, and we could mix-and-match different contracts without needing to worry about too much, nor conflicting storage.

Benefits

Spending the time and effort to make this update to our codebase has resulted in some pretty great improvements to the Dark Forest experience.

Single address and ABI

Our Diamond contract provides a single point-of-entry into all functionality of Dark Forest, including the core game logic, whitelist contracts, admin functionality, etc.

This really streamlines interacting with the contracts because we only need to keep track of one ABI and one contract address. You might have noticed that the URL to play Round 5 is https://zkga.me/play/0x5da117b8ab8b739346f5edc166789e5afb1a7145, which is actually the contract address of the Round 5 Diamond contract.

Being able to include the contract address directly in the URL allows the official client to connect to different deployed versions of Dark Forest (as long as they provide the same functionality). This could allow for Community Rounds using the official client—or maybe a guild could deploy a proxy contract where many players could control one account and they’d all play through the official client.

Upgrade visibility

As mentioned above, we used our upgrade system a few times during Round 5. Dark Forest has been upgraded in past rounds; however, there was never as much visibility into the changes.

With some of our custom tooling, we actually review every change an upgrade will make and perform an interactive confirmation before the update is applied. Additionally, every time a Diamond is changed, the DiamondCut event is emitted, so players and other external parties can track and verify the changes we deployed.

In the future, we want to make our contracts available via louper.dev, so players can inspect and interact with the live Diamond!

Refactors & code organization

In the past, it has been really hard to refactor or organize the Dark Forest contract code because adding a new smart contract required piping a lot of information through the client. This generally resulted in massive libraries that multiple contracts would call. We still had to link the libraries to the contracts and that turned out to be time consuming and error prone.

Since all functionality is provided to the client through the Diamond, we can split logic into a completely separate contract and just add it to the Diamond. As you can see in DFMoveFacet, this allowed us to expand the move function to support the spaceship and abandon mechanics.

Isolating features

When we are building a new feature, like Capture Zones, we have to consider where it belongs. Does the code go in the core mechanics contract? Maybe it should be a standalone contract? With the Diamond Standard, we no longer need to stress about this separation. The entire logic for Capture Zones was written as DFCaptureFacet, a completely standalone contract that is added to our Diamond.

Limitations

In the grand scheme of things, EIP-2535 is still fairly new, so making this switch wasn’t without growing pains. We on the Dark Forest core team hope to be a driver in reducing any pain points!

Tooling

It is still fairly hard to find tooling for working with Diamonds. Even if tooling exists, it might not unlock the full power of Diamonds. For example, we evaluated hardhat-deploy for deploying and upgrading our Diamond, but it didn’t seem to give good visibility into what changes would happen during an upgrade. Instead, we are experimenting with some custom hardhat tasks that can be packaged in the future.

Another tool that is needed, as Nick Mudge called out on Twitter, is something to verify the order of state variables before an upgrade. If your state variable order changes, bad things can happen after an upgrade, so ensuring this is very important.

@mudgen tweet about needed storage verifier plugin

We also needed to build the hardhat-diamond-abi plugin because working with a Diamond within hardhat tasks was a pain without it.

Libraries

A lot of the libraries that were written before EIP-2535 either don’t work or are very difficult to use with the Diamond Standard. For example, inheriting from any OpenZeppelin contract adds state variables to the contract itself, instead of in Diamond Storage. I was lucky enough to stumble on the SolidState contracts because I was using some of Nick Barry’s hardhat plugins and noticed his other work. We need better visibility into libraries with explicit support for the Diamond Standard.

ERC721 & ERC20

In Dark Forest, artifacts are implemented as ERC721 tokens that you can withdraw into your wallet. When we wanted to add spaceships to the game, we also wanted them to be a token; however it soon became apparent that this was difficult with ERC721 & Diamonds. If you dive deep into our implementation, both artifacts and spaceships are implemented in the same token collection, and we just vary the implementation via a stored “artifact type”. This resulted in a very messy implementation in the contracts.

At the time, I didn’t know about ERC1155 tokens. As we were preparing for the launch of Round 5, Nick Mudge tweeted an article about ERC1155’s “Multi-Token” capabilities. We plan to investigate this token specification for the Dark Forest contracts!

@mudgen tweet about ERC1155

Remaining FUD

In discussing the Diamond Standard with people, there still seems to be a lot of FUD in the wild. I think this is driven by my above points, and one particular Trail of Bits article that has been refuted by Nick in a follow-up article. I think we can overcome this with enough building and education for Diamonds!

Onward

This post has just been the tip of the iceberg. If you want to get really in-depth, please check out our smart contracts and my supporting documentation around the Dark Forest Diamond at https://github.com/darkforest-eth/eth. Within the documentation, I also discuss some of our design decisions and link even more articles about the Diamond Standard.

We have some other revolutionary features that this enables, but you’ll have to wait a little longer. We’ll be sharing what we believe is the next evolution of Dark Forest on March 1st!

Dark Forest is hiring! We are looking for experienced full stack and solidity developers to join our team! If you like what you see, consider applying .