Key Takeaways
- Utilize custom tokens within a DAO to enable voting on proposals, including deletion of entries and blacklisting of addresses, enhancing decentralized governance and operational flexibility.
- Implement robust voting mechanisms that require token ownership verification and lock tokens during the voting period to prevent manipulation and ensure commitment.
- Design proposals and voting systems with structured deadlines and execution criteria to maintain order and efficiency within the DAO’s operations.
- Address potential security concerns by limiting proposal creation to members only and enabling emergency functions for urgent deletions, controlled by the DAO owner.
- Explore the technical challenges and solutions in deploying large smart contracts on Ethereum, emphasizing the importance of code optimization to manage gas costs and ensure successful deployment.
In part 5 of this tutorial series on building DApps with Ethereum, we dealt with adding content to the story, looking at how to add the ability for participants to buy tokens from the DAO and to add submissions into the story. It’s now time for the DAO’s final form: voting, blacklisting/unblacklisting, and dividend distribution and withdrawal. We’ll throw in some additional helper functions for good measure.
If you get lost in all this, the full source code is available in the the repo.
Votes and Proposals
We’ll be issuing Proposals and voting with Votes. We need two new structs:
struct Proposal {
string description;
bool executed;
int256 currentResult;
uint8 typeFlag; // 1 = delete
bytes32 target; // ID of the proposal target. I.e. flag 1, target XXXXXX (hash) means proposal to delete submissions[hash]
uint256 creationDate;
uint256 deadline;
mapping (address => bool) voters;
Vote[] votes;
address submitter;
}
Proposal[] public proposals;
uint256 proposalCount = 0;
event ProposalAdded(uint256 id, uint8 typeFlag, bytes32 hash, string description, address submitter);
event ProposalExecuted(uint256 id);
event Voted(address voter, bool vote, uint256 power, string justification);
struct Vote {
bool inSupport;
address voter;
string justification;
uint256 power;
}
A Proposal will have a mapping of voters to prevent people from voting on a proposal twice, and some other metadata which should be self-explanatory. The Vote will either be a yes or no vote, and will remember the voter along with their justification for voting a certain way, and the voting power — the number of tokens they want to devote to voting for this proposal. We also added an array of Proposals so we can store them somewhere, and a counter for counting how many proposals there are.
Let’s build their accompanying functions now, starting with the voting function:
modifier tokenHoldersOnly() {
require(token.balanceOf(msg.sender) >= 10**token.decimals());
_;
}
function vote(uint256 _proposalId, bool _vote, string _description, uint256 _votePower) tokenHoldersOnly public returns (int256) {
require(_votePower > 0, "At least some power must be given to the vote.");
require(uint256(_votePower) <= token.balanceOf(msg.sender), "Voter must have enough tokens to cover the power cost.");
Proposal storage p = proposals[_proposalId];
require(p.executed == false, "Proposal must not have been executed already.");
require(p.deadline > now, "Proposal must not have expired.");
require(p.voters[msg.sender] == false, "User must not have already voted.");
uint256 voteid = p.votes.length++;
Vote storage pvote = p.votes[voteid];
pvote.inSupport = _vote;
pvote.justification = _description;
pvote.voter = msg.sender;
pvote.power = _votePower;
p.voters[msg.sender] = true;
p.currentResult = (_vote) ? p.currentResult + int256(_votePower) : p.currentResult - int256(_votePower);
token.increaseLockedAmount(msg.sender, _votePower);
emit Voted(msg.sender, _vote, _votePower, _description);
return p.currentResult;
}
Notice the function modifier: by adding that modifier into our contract, we can attach it to any future function and make sure only token holders can execute that function. It’s a reusable security check!
The vote function does some sanity checks like the voting power being positive, the voter having enough tokens to actually vote etc. Then we fetch the proposal from storage and make sure it’s neither expired nor already executed. It wouldn’t make sense to vote on a proposal that’s already done. We also need to make sure this person hasn’t yet voted. We could allow changing the vote power, but this opens the DAO to some vulnerabilities like people withdrawing their votes at the last minute etc. Perhaps a candidate for a future version?
Then we register a new Vote into the proposal, change the current result for easy lookup of scores, and finally emit the Voted event. But what’s token.increaseLockedAmount
?
This bit of logic increases the amount of locked tokens for a user. The function is only executable by the owner of the token contract (by this point that’s hopefully the DAO) and will prevent the user from sending an amount of tokens that exceeds the locked amount registered to their account. This lock is lifted after the proposal falls through or executes.
Let’s write the functions for proposing the deletion of an entry now.
Voting to Delete and Blacklist
As established in part 1 in this series, we have three entry deletion functions planned:
- Remove entry: when confirmed by vote, the target entry is removed. Voting time: 48 hours.
- Emergency remove entry [Only Owner]: can only be triggered by Owner. When confirmed by vote, the target entry is removed. Voting time: 24 hours.
- Emergency remove image [Only Owner]: only applies to image entries. Can only be triggered by Owner. When confirmed by vote, the target entry is removed. Voting time: 4 hours.
Five deletions of a single address’ entries lead to a blacklisting.
Let’s see how we can do that now. First up, the deletion functions:
modifier memberOnly() {
require(whitelist[msg.sender]);
require(!blacklist[msg.sender]);
_;
}
function proposeDeletion(bytes32 _hash, string _description) memberOnly public {
require(submissionExists(_hash), "Submission must exist to be deletable");
uint256 proposalId = proposals.length++;
Proposal storage p = proposals[proposalId];
p.description = _description;
p.executed = false;
p.creationDate = now;
p.submitter = msg.sender;
p.typeFlag = 1;
p.target = _hash;
p.deadline = now + 2 days;
emit ProposalAdded(proposalId, 1, _hash, _description, msg.sender);
proposalCount = proposalId + 1;
}
function proposeDeletionUrgent(bytes32 _hash, string _description) onlyOwner public {
require(submissionExists(_hash), "Submission must exist to be deletable");
uint256 proposalId = proposals.length++;
Proposal storage p = proposals[proposalId];
p.description = _description;
p.executed = false;
p.creationDate = now;
p.submitter = msg.sender;
p.typeFlag = 1;
p.target = _hash;
p.deadline = now + 12 hours;
emit ProposalAdded(proposalId, 1, _hash, _description, msg.sender);
proposalCount = proposalId + 1;
}
function proposeDeletionUrgentImage(bytes32 _hash, string _description) onlyOwner public {
require(submissions[_hash].image == true, "Submission must be existing image");
uint256 proposalId = proposals.length++;
Proposal storage p = proposals[proposalId];
p.description = _description;
p.executed = false;
p.creationDate = now;
p.submitter = msg.sender;
p.typeFlag = 1;
p.target = _hash;
p.deadline = now + 4 hours;
emit ProposalAdded(proposalId, 1, _hash, _description, msg.sender);
proposalCount = proposalId + 1;
}
Once proposed, a Proposal is added to the list of proposals and notes which entry is being targeted by the entry hash. The description is saved and some defaults added, and a deadline is calculated depending on proposal type. The proposal added event gets emitted and the total number of proposals is increased.
Next let’s see how to execute a proposal. To be executable, a proposal must have enough votes, and must be past its deadline. The execution function will accept the ID of the proposal to execute. There’s no easy way to make the EVM execute all pending proposals at once. It’s possible that too many would be pending to execute and that they would make big changes to the data in the DAO, which might exceed the gas limit of Ethereum blocks, thereby failing the transaction. It’s much easier to build a manual execution function callable by anyone with well defined rules, so the community can keep an eye on the proposals that need executing.
function executeProposal(uint256 _id) public {
Proposal storage p = proposals[_id];
require(now >= p.deadline && !p.executed);
if (p.typeFlag == 1 && p.currentResult > 0) {
assert(deleteSubmission(p.target));
}
uint256 len = p.votes.length;
for (uint i = 0; i < len; i++) {
token.decreaseLockedAmount(p.votes[i].voter, p.votes[i].power);
}
p.executed = true;
emit ProposalExecuted(_id);
}
We grab the proposal by its ID, check that it meets requirements of not having been executed and deadline being expired, and then if the type of the proposal is a deletion proposal and the voting result is positive, we use the already written deletion function, finally emitting a new event we added (add it to the top of the contract). The assert
call is there serving the same purpose as the require
statement: assert is generally used when you “assert” that a result is true. Require is used for prerequisites. Functionally they’re identical, with the difference of assert
statements not being able to accept message parameters for cases when they fail. The function ends by unlocking the tokens for all the votes in that one proposal.
We can use this same approach to add other types of proposals, but first, let’s update the deleteSubmission
function to ban the users that have five or more deletions on their account: it means they’ve been consistently submitting content the community voted against. Let’s update the deleteSubmission
function:
function deleteSubmission(bytes32 hash) internal returns (bool) {
require(submissionExists(hash), "Submission must exist to be deletable.");
Submission storage sub = submissions[hash];
sub.exists = false;
deletions[submissions[hash].submitter] += 1;
if (deletions[submissions[hash].submitter] >= 5) {
blacklistAddress(submissions[hash].submitter);
}
emit SubmissionDeleted(
sub.index,
sub.content,
sub.image,
sub.submitter
);
nonDeletedSubmissions -= 1;
return true;
}
That’s better. Auto-blacklisting on five deletes. It wouldn’t be fair not to give the blacklisted addresses a chance to redeem themselves, though. We also need to define the blacklisting function itself. Let’s do both of those things and set the unblacklisting fee to, for example, 0.05 ether.
function blacklistAddress(address _offender) internal {
require(blacklist[_offender] == false, "Can't blacklist a blacklisted user :/");
blacklist[_offender] == true;
token.increaseLockedAmount(_offender, token.getUnlockedAmount(_offender));
emit Blacklisted(_offender, true);
}
function unblacklistMe() payable public {
unblacklistAddress(msg.sender);
}
function unblacklistAddress(address _offender) payable public {
require(msg.value >= 0.05 ether, "Unblacklisting fee");
require(blacklist[_offender] == true, "Can't unblacklist a non-blacklisted user :/");
require(notVoting(_offender), "Offender must not be involved in a vote.");
withdrawableByOwner = withdrawableByOwner.add(msg.value);
blacklist[_offender] = false;
token.decreaseLockedAmount(_offender, token.balanceOf(_offender));
emit Blacklisted(_offender, false);
}
function notVoting(address _voter) internal view returns (bool) {
for (uint256 i = 0; i < proposalCount; i++) {
if (proposals[i].executed == false && proposals[i].voters[_voter] == true) {
return false;
}
}
return true;
}
Notice that a blacklisted account’s tokens get locked up until they send in the unblacklisting fee.
Other Types of Votes
Using the inspiration from the functions we wrote above, try writing the other proposals. For spoilers, check out the GitHub repo of the project and copy the final code from there. For brevity, let’s move on to the other functions we still have left in the DAO.
Chapter End
Once the time or chapter limit of the story is reached, it’s time to bring the story to an end. Anyone can call the ending function after the date which will allow withdrawals of dividends. First, we need a new StoryDAO attribute and an event:
bool public active = true;
event StoryEnded();
Then, let’s build the function:
function endStory() storyActive external {
withdrawToOwner();
active = false;
emit StoryEnded();
}
Simple: it deactivates the story after sending the collected fees to the owner and emits the event. But in actuality, this doesn’t really change anything in the DAO as a whole: the other functions don’t react to it being over. So let’s build another modifier:
modifier storyActive() {
require(active == true);
_;
}
Then, we add this modifier to all the functions except withdrawToOwner
, like so:
function whitelistAddress(address _add) storyActive public payable {
In case any tokens are left over in the DAO, let’s take them back and take over ownership of these tokens in order to be able to use them on another story later on:
function withdrawLeftoverTokens() external onlyOwner {
require(active == false);
token.transfer(msg.sender, token.balanceOf(address(this)));
token.transferOwnership(msg.sender);
}
function unlockMyTokens() external {
require(active == false);
require(token.getLockedAmount(msg.sender) > 0);
token.decreaseLockedAmount(msg.sender, token.getLockedAmount(msg.sender));
}
The unlockMyTokens
function is there to unlock all locked tokens in case some stayed locked for a specific user. It shouldn’t happen, and this function should be removed by a good amount of tests.
Dividend Distribution and Withdrawals
Now that the story has ended, the fees collected for submissions need to be distributed to all token holders. We can re-use our whitelist to mark everyone who’s made the withdrawal of the fees:
function withdrawDividend() memberOnly external {
require(active == false);
uint256 owed = address(this).balance.div(whitelistedNumber);
msg.sender.transfer(owed);
whitelist[msg.sender] = false;
whitelistedNumber--;
}
If these dividends aren’t withdrawn within a certain time limit, the owner can grab the rest:
function withdrawEverythingPostDeadline() external onlyOwner {
require(active == false);
require(now > deadline + 14 days);
owner.transfer(address(this).balance);
}
For homework, consider how easy or hard it would be to re-use this same deployed smart contract, clear its data, and keep the tokens in the pot and restart another chapter from this without re-deploying. Try doing this yourself and keep an eye on the repo for future updates to the tutorial series covering this! Also think about additional incentive mechanics: maybe the amount of tokens in an account influences the dividend they get from the story’s end? Your imagination is the limit!
Deployment Issues
Given that our contract is quite large now, deploying and/or testing it might exceed the gas limit of Ethereum blocks. This is what limits large applications from being deployed on the Ethereum network. To get it deployed anyway, try using the code optimizer during compilation by changing the truffle.js
file to include solc
settings for optimization, like so:
// ...
module.exports = {
solc: {
optimizer: {
enabled: true,
runs: 200
}
},
networks: {
development: {
// ...
This will run the optimizer across the code 200 times to find areas that can be minified, removed or abstracted before deployment, which should reduce the deployment cost significantly.
Conclusion
This concludes our exhaustive DAO development — but the course isn’t over yet! We still have to build and deploy the UI for this story. Luckily, with the back end completely hosted on the blockchain, building the front end is much less complicated. Let’s look at that in our penultimate part of this series.
Frequently Asked Questions on Building Ethereum DApps and Voting with Custom Tokens
How does blockchain voting work in practice?
Blockchain voting is a decentralized voting system that leverages the transparency and security features of blockchain technology. In theory, it should work flawlessly, but in practice, it often encounters challenges. The voting process involves creating a smart contract on the Ethereum blockchain, where each vote is a transaction that can be verified. However, issues such as voter anonymity, vote manipulation, and the technical complexity of using blockchain platforms can hinder its practical implementation.
What are DAO voting mechanisms?
DAO (Decentralized Autonomous Organization) voting mechanisms are systems that allow token holders in a DAO to vote on proposals based on their token ownership. The most common mechanisms include simple majority voting, where a proposal is accepted if it receives more than 50% of the votes, and quadratic voting, where the cost of casting multiple votes for a proposal increases exponentially.
How does governance work in security tokens?
Governance in security tokens is typically handled through a voting system where token holders can vote on various aspects of the project. This can include decisions on project development, token economics, and even changes to the governance system itself. The voting power of a token holder is usually proportional to the number of tokens they hold.
How do I set up DAO governance?
Setting up DAO governance involves creating a smart contract on the Ethereum blockchain that outlines the rules of the organization, including voting rights and proposal mechanisms. This contract is then deployed on the blockchain, and tokens representing voting rights are distributed to members. Members can then propose and vote on changes to the organization.
What are the risks associated with holding DAO governance tokens?
Holding DAO governance tokens can be risky due to the volatile nature of cryptocurrencies and the regulatory uncertainties surrounding DAOs. For instance, the Commodity Futures Trading Commission (CFTC) has warned that voting with DAO tokens could potentially be seen as a form of market manipulation. Additionally, if the DAO is poorly managed or falls victim to a hack, token holders could lose their investment.
How do I create custom tokens for voting in Ethereum DApps?
Creating custom tokens for voting in Ethereum DApps involves writing and deploying a smart contract on the Ethereum blockchain. This contract defines the properties of the token, such as its name, symbol, and total supply. Once the contract is deployed, tokens can be distributed to users, who can then use them to vote on proposals within the DApp.
What are the benefits of using blockchain for voting?
Blockchain voting offers several benefits, including transparency, security, and immutability. Votes are recorded as transactions on the blockchain, making them transparent and verifiable. The decentralized nature of blockchain also makes it difficult for any single party to manipulate the voting process.
How can I ensure voter anonymity in blockchain voting?
Ensuring voter anonymity in blockchain voting can be challenging due to the transparent nature of blockchain transactions. However, techniques such as zero-knowledge proofs can be used to verify the validity of a vote without revealing the identity of the voter.
What are the challenges of implementing blockchain voting?
Implementing blockchain voting can be challenging due to technical complexity, regulatory uncertainties, and potential security risks. Users need to be familiar with blockchain technology to participate in the voting process, and regulatory bodies may have concerns about the legality and security of blockchain voting systems.
How can I mitigate the risks associated with DAO governance tokens?
Mitigating the risks associated with DAO governance tokens involves careful management of the DAO, thorough security practices, and staying informed about regulatory developments. It’s also important to diversify your investment portfolio and not invest more than you can afford to lose.
Bruno is a blockchain developer and technical educator at the Web3 Foundation, the foundation that's building the next generation of the free people's internet. He runs two newsletters you should subscribe to if you're interested in Web3.0: Dot Leap covers ecosystem and tech development of Web3, and NFT Review covers the evolution of the non-fungible token (digital collectibles) ecosystem inside this emerging new web. His current passion project is RMRK.app, the most advanced NFT system in the world, which allows NFTs to own other NFTs, NFTs to react to emotion, NFTs to be governed democratically, and NFTs to be multiple things at once.