Votings in Chronobank - ahiatsevich/SmartContracts GitHub Wiki
About the voting
ChronoBank as part of its functionality provides an ability to organize polls. They are designed to be used as an instrument for making ecosystem (or platform) or strategic decisions. It was by design that the users hold TIME tokens and receive a fee from any transactions that are handled inside the system; also TIME tokens unlock additional functionality and are used for paying for some key features like platform and token creations, organizing token crowdsale and some others. TIME tokens fuel ChronoBank voting subsystem and define the “weight" of users' votes.
Voting that is designed on a blockchain should fulfill various requirements including security, reliability and, not the last but not the least important - the price for poll manipulations (creation, vote making, closing). So as we could see below, continuous iteration through different approaches improves stability and decreases expenses for deploying the voting contracts and performing poll actions.
The previous approach
So our first step towards the ideal voting subsystem was by designing a couple of contracts (we will call them managers) that would absorb all the work with polls and their management. It’s worth mentioning that keeping data in storage for all kinds of managers uses a special contract StorageManager. StorageManager could allow or deny access to the storage area.
Going back to the voting, we say these contracts will share common storage space and will have an access to the shared variables. On one hand, this allows to separate the voting process into several functional contracts (getting details, voting itself, manipulating poll's data) and break down massive sized code into smaller contracts, but on the other hand, this trick binds all contracts and shares state between them. It becomes hard to make changes in a right way. Besides the aforementioned disadvantage, this implementation has a huge pricing for making votes and supposed to store a lot of statistical data. All that is not what we have expected from our votings - we want users to be a part of the ecosystem with ease and full participation in its life. Here are a couple of numbers that we had with previous votings implementation:
Action | Used gas |
---|---|
create a poll | 787111 |
vote | 411641 |
close poll | 482976 |
delete poll | 95073 |
As you might see this is not excellent numbers and there is a lot of room for improvement!
Let's improve!
So we decided to change our view and use another approach on the way to new votings. As it was previously we leave one contract (VotingsManager) for manipulating, creating polls in the system and getting general information - it will remain the entry point for any users that will decide to use votings. But the next will be the most interesting part: instead of storing polls as a set of properties in the shared scope we will create a brand new contract for each poll. This move will allow us to refactor a lot of logic into a separate contract, simplify contracts and make our intentions clearer.
The one thing which confused us was that every time we are creating a new contract to start a poll that would require more gas for deploying contract than the previous implementation (because of quite a lot of logic and code inside this brand new contract).
Our decision about this concern was to relocate all this logic into a single-instance contract (we call it a backend) that will be deployed once, and also during a poll creation make a new proxy contract instead of fully functional poll contract. The created proxy contract will have a backend contract address and will redirect all calls to that instance. A proxy contract could be easily implemented with delegatecall assembly instruction which executes delegated functions inside the context of a caller contract (i.e. proxy) and the read/write operations will associate their value with the context. Despite all these advantages, we couldn't return multiple values from a delegatecall without additional specific and wordy code, so it was like we should implement our multiple-return functions right in a proxy contract. Thanks to Byzantium update (one of the parts of Metropolis release) there were added very handy and useful assembly instructions - returndatasize and returndatacopy. These instructions return a size of a return data of delegated function and copy return data into а memory. Now we could find them a good place for their application - use them in our proxy contract and clean up its code from any specifics of backend functions. After that, we would have a contract which will be created every time a new poll is going to be created and it would take a small amount of gas to deploy almost empty contract.
Here the numbers what we have accomplished:
Action | Used gas (Old) | Used gas (New) |
---|---|---|
create a poll | 787111 | 849476 |
vote | 411641 | 159331 |
close poll | 482976 | 443473 |
delete poll | 95073 | 56486 |
https://live.amcharts.com/I0ODU/
As you can see we gain almost thrice as fewer expenses while performing vote operations which on a big scale will save a lot of resources for users.
Yes, we have some increase in poll creation but it was an acceptable price for reducing 'vote' operation. Other methods like closing and deleting a poll also show a reduction in gas consumption.
Deploying Voting subsystem:
v.1 - 3089834 + 3789833 + 2891094 = 9.770.761
v.2 - 2440890 + 586243 + 3014376 = 6.041.509