There are so many token sales these days that it must be pretty easy to implement a crowdfunding contract, right? Well, not really. Unfortunately we still lack established design patterns and tested ways of implementing a secure crowdfunding contract. What’s more, even the most basic features are not standardized, so each contract must be built from the ground up.
In anticipation of our own crowdfunding campaign, the Golem team has also followed this path. I’d like to share with you our insight into the process.
So, we wanted to implement a secure crowdfunding contract which would allow us to carry out the crowdfunding process in an efficient, secure, and transparent manner.
From our perspective, the best way to start was to take look at existing code and see how others approached the problem, noting what solutions they came up with. There are quite a few contracts worth inspecting (e.g. Digix DAO, First Blood, Singularity TV, the ERC20 token specification, and even The DAO), and after studying them we knew what we would like to have implemented, and which features were too risky for us to play with.
In particular, we were able to clearly state the assumptions that both crowdfunding and the token contract must fulfill:
- It must be secure
- only the most necessary features are to be implemented (KISS principle)
- upgradability must be available by design
This mapped pretty well to our token design process: take the ERC20 token standard, leave only the bare minimum functionality (namely the most basic transfer capabilities and balance tracking), then add an additional call that can be used to upgrade the token later on.
As for the crowdfunding contract, we also wanted to make it as simple as possible with clearly stated termination conditions for both success and failure modes of the crowdfunding, with the minimal external API being exposed. It boiled down to the following requirements:
- The crowdfunding is active only during a specified time span (beginning and end date of the crowdfunding must be specified).
- The minimum target and cap must be specified.
- In case of a failure (minimum target not reached before the end of the crowdfunding), each funder can easily retrieve his/her ETH.
- During the crowdfunding period, any generated GNT cannot be transferred.
- After the crowdfunding is successfully finalized, an endowment GNT pool is allocated and GNT transfer is enabled.
The endowment GNT pool is time-locked, but according to the KISS principle, it is implemented as an external mechanism, so that the contract code itself is only used to carry out the crowdfunding process.
Having discussed all assumptions and inspected the existing contracts, we were finally ready to start with our own code. We implemented a few quick solutions just to get comfortable with the way such code is written, testing them thoroughly. We then moved towards a much simpler, top-down approach of our own design specification.
The first thing one notices is that the global contract state depends on two main parameters: total tokens generated (corresponding to the total amount of ETH sent to the contract), and the current time. These two parameters span a plane, and a whiteboard is a pretty useful approximation of the part of the plane that we were interested in.
You may chuckle at the following, but it is not a joke — what we began with was the following diagram, prepared by Paweł Bylica:
Its simplicity can be only compared to its expressiveness, and you’ll see the results a few paragraphs from now. Just to make it a bit more human-friendly let’s invert the Y axis, so that it can be read from the top.
This diagram corresponds to the crowdfunding phase only, so let’s add the operational phase to the picture, along with a few simplifications (an invalid state is not required, and similar states can be coupled).
A few additional arrows corresponding to external events — external function calls — have also been added.
It looks almost like a state diagram with two large states (crowdfunding state and operational state) and some internal state changes indicated by the arrows.
So let’s make it explicit and decouple all states.
What you can see here is in fact the whole functionality of both the crowdfunding contract and the token contract, with a migration mechanism shown on a single diagram. There are quite a few details missing, but you can see how the contract state changes both in time (dotted arrows) and via the external calls. This is why we wanted to expose only the minimum external API — to make the contract logic as straightforward as possible.
And that’s it. The high level view of the contract logic is here. All crowdfunding and token related requirements are fulfilled, and the upgrade mechanism is available as soon as the contract is in the operational state (the valid migration contract must also be provided, but this is external/orthogonal to the presented contract logic).
As you can see, our approach to migration enables each user to choose how much GNT she wants to migrate, while leaving the rest in the original contract. This is exactly what was required: a user can keep part of the tokens in the old contract and interact with it as long as necessary, while at the same time, another share of her GNT can be used in the upgraded contract. No voting mechanism is necessary, and each participant is in full control of her tokens without need to wait for voting results of any kind. This can be interpreted as an explicit forking mechanism where fork decision is not based on the opinion of the majority, but simply resolved for each user individually.
With this simple diagram we can go further and prepare the full specification of the contract with all the conditions and state transitions stated explicitly. Based on such design, one can relatively easily implement the contract logic without having to worry too much about keeping track of the progress and the preconditions for each state. Additionally, it is a very convenient way of preparing test scenarios for the contract: each path from the start state to any of the final states corresponds to one such scenario. Now let’s add more details to the state transitions.
What you can see here is almost the final diagram, with all the state transitions and conditions stated explicitly. Each state specifies accepted external calls (depicted by differently styled arrows), and in addition to the active state, there is one additional transition based on the passing of time (dotted arrow), resulting in the success state.
All used symbols are described in the diagram, and correspond to either a global state, or external calls which result in state transitions.
One additional feature depicted is an additional state transition to the example upgraded token from which yet another migration can take place. Of course this is an example only, as the destination token (and any tokens that may follow) may not implement migration of any sort, nor can they implement a means of returning the original token. This is limited only by the code, and requirements of the new token.
OK, we have all the pieces in place and the only thing left is to split the large states into smaller and more contained ones, so that the conditions for each transition are easier to grasp.
The Active state and the Failure state were split into two separate states each along with a few transitions added but that’s all.
This is a fine-grained picture, and possibly not the easiest one to grasp when you see it for the first time. But, if you followed the evolution of this design from the beginning, it should be much more manageable. What’s more, there is almost a one-to-one correspondence to both the crowdfunding and the token contract that we implemented. At the same time, all the details can be seen on a single diagram. There is no need to inspect the code to understand the details of the contract logic.
One thing missing here is the timelocking mechanism for the endowment GNT, but it is kept separate from the contract logic so that no unnecessary code is added to the crowdfunding implementation. There are a few ways of implementing it which are based on proxy-like intermediaries, and you can see such example implementation in the repository. It could of course be added to the diagram, but the additional states would not bring too much insight to the table, at the cost of making the diagram much more complicated.
This blog post was intended to allow you to follow the contract logic without the need of inspecting the source code, while additionally giving some insight into the design process that took place. But there is at least one side-effect here, namely that this may be the way to specify simple contracts. It would definitely require stating more information and formalizing the notation, but this may be an interesting issue itself.