Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.Sign up
Dapp Architecture Designs
Designing dapps can take several forms. There are, for example, important considerations in terms of how they interact with other dapps (for example, name/app registries or decentralized exchanges) and how they work with their state over time. In some cases, some modularity might be desired in the event that functionality needs to be upgraded in the future without having to copy over state o re-register the dapp in registries (and so forth). Some of these designs will be detailed here. Names & designs are a WIP. Edit where you want to.
This is the most simple form. A dapp that is one contract, and keeps all its state and functions within it.
- Simple to understand & design for.
- Cheapest (no requirement to talk to other contracts).
- Immutable functionality. Once published there is no way to change behavior.
Closed-system Dapp with modularity on the edges.
This is similar to above. The app can function on its own, but chooses to engage with other contracts. It is not required, but adds additional functionality. A typical example is app/name registries. Another example is a token that chooses to use a decentralized exchange like etherex (where it is required to notify the dex in the event that tokens have been deposited in its account).
Same pros & cons as above, but usually these contracts are designed such that the owners can swap out or change various things in the contracts it interacts with. For example, changing a name in the app registry, or using a new decentralized exchange. The core contract stays immutable.
The dapps on the edges exist on their own. In some of these cases hooks are required in your dapp to interact with them. The edge dapp is not responsible and doesn't want to be responsible to keep track of what happens in other dapps. If it was conferred new capability or tokens, it needs to be notified. EtherEx is a good example of a hook. In a token dapp, the ether-ex dapp can be given tokens, and can then use them (by virtue of interacting with the token). But EtherEx doesn't know about the token dapp. The important thing about hooks is that, again, it doesn't need to exist. Your dapp can function without it... otherwise it's just a normal transaction. Another token dapp hook could be it reporting information to a token registry. For example, if its nominal supply changes, it must notify the registry so that its information is correct.
Multi-contract & Multi-state
This is most likely what most complex dapps will look like. Concerns are separated in terms of functionality. Some parts are supposed to exist longer than others. They are dependent on each other for functionality. It's more an ecosystem, with potentially different interfaces to it.
An example is a prediction market that consists of multiple oracles that reveal information & contracts that sell & manage the shares of events. Another example is a game where the game world might consist of a tile per contract scheme, and then confers rights to other contracts to change the tile according to some game rules.
- Modularity. For example, you don't have to build a future-proof token management system for selling shares in a prediction market, as it is expected to have a time limit. If new functionality is wanted, it can start with new bets.
- More expensive due to modularity.
Hub and Spoke
The Hub and Spoke pattern uses modular, stateful Spoke contracts for long-term storage and a Hub contract for access control to methods interacting with Spokes. The Spoke contracts should contain only app storage and methods for accessing and mutating that storage. Private Spoke modules should have a pointer to an
admin contract (initialized on creation) that points to a Hub contract, and stop execution when called from any other address. A hub contract contains pointers to these Spoke contracts and may include some configurable properties that require some vote or permission or other logic to set.
For upgradeability, private Spoke contracts may want to include a
chown method, callable only by the current hub, so the original Hub contract can be swapped for a new contract. For a clean upgrade, the Hub contract could include an
upgrade method that when passed the address of the new Hub, will send that to the
chown method of all its Spoke contracts, then auto-unregister itself from any registries, and finally suicide itself or set a pointer to the new contract indicating its obsolescence.
A potential use case for this pattern is to have contract factories spawn new Hub and Spoke contracts with some predictable makeup (e.g. only a limited set of configurable elements) and optionally let the spawned contracts be added to a set of registries with data structures to efficiently support operations like search and/or sorting. Therefore these registries will contain only contracts with storage meeting some expected pattern. And by using an upgrade method that unregisters the hub contract from that set of registries, those registries can be guaranteed to list only valid contract addresses at any time.
A Hub and Spokes pattern can also be supported by a core Spoke contract with upgrade control over its own Hub. For example, a token-based community has a core: it's token balances. Apps are built on top of these for the token-based community. To interact with it, it goes through an administrator contract (the stateless functions contracts). In the future, it wants to upgrade its administrative functions and thus votes to swap out the current administrator functions for a new one.
Hub as a name registry
Another example of this design paradigm is something like Republic of DOUG: a DAO management system. It has a Hub with pointers to Spokes that can do various additional functionalities. However, it is not entirely similar, since here the Hub acts more as a name registry for interop between Spokes which can be individually upgraded.
Hub as delegator only.
In this design, the hub is only responsible for keeping track of permissions. Contracts are given permission to manipulate state. Any other state (besides mapping of permissions) is kept in other contracts and are referable. So, in other words, you will have multiple state contracts, and multiple function contracts. For example, Function 1 could have permission to edit state of contract 1, 3, & 5, while Function 2 has permission to edit state of 1, 2, 4, 7. Permissions can be revoked. This paradigm means that new functions can be built, and old functions be turned off. This also means any state required in the future can be "tacked" onto the delegator hub. A master function contract can optionally be added that determines how permissions are lent/revoked (e.g., through owner or votes for example).
Hub and Spoke Pros and Cons:
- Extreme modularity. The Spoke contracts should never have to change [besides the pointer to its admin contract].
- The Hub contract can unilaterally own the storage of its private Spokes, and can swap out all of its access control and functionality if desired.
- Although it won't work in all cases, Hubs could support upgrading Spoke contracts as well. If a hub wants to upgrade one of its spokes, it could upgrade itself to a new hub that includes logic for switching between spoke_i_v1 and spoke_i_v2. It would likely need to support this logic in the UI as well.
- Upgrading hub contracts is do-or-die. If there's a bug, a bad address is passed, or it fails for any reason, spoke storage could get locked up forever.
- Both Hub-Spoke upgrade patterns introduce an attack vector: if the upgrade access control is not sufficient or if enough accounts controlling the hub decide to act badly, a subverted upgrade could: lock up the app storage forever, sell it to a malevolent actor, steal ether from Spoke contracts, etc. To prevent ether loss from Spokes in such an attack (and thus, eliminating much of the incentive for an attack), Spokes containing ether or other tokens of value can use the (origin) or (caller) opcodes to define the account id for withdraw or transfer methods. However, using (origin) does eliminate the possibility of contracts arbitrarily controlling their own balances.
- Potentially more expensive over time than copying state over.
- Possible incompatibility. Some dapps assume that both are in the same contract, and thus could have potential issues (in terms of who msg.sender is for example when interacting with external dapps).