Core points
The fifth part of this tutorial series introduces building a DApp using Ethereum, and we discuss adding content to the story and how to add the ability to buy tokens from DAO and submit content to the story for participants. Now is the time for the final form of DAO: voting, inclusion/absolution of blacklists, and dividend distribution and withdrawal. We will add some extra helper functions.
If you are confused by these, the complete source code can be found in the code base.
Votes and Proposals
We will use proposals and votes to vote. We need two new structures:
<code>struct Proposal { string description; bool executed; int256 currentResult; uint8 typeFlag; // 1 = delete bytes32 target; // 提案目标的ID。例如,标志1,目标XXXXXX(哈希)表示删除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; }</code>
Proposals will contain a voter map to prevent people from voting twice on one proposal, as well as some other metadata that should be self-explanatory. The vote will be a vote of yes or against and will remember the reasons for the voter and the right to vote—the number of tokens they want to use to vote for this proposal. We also added an array of proposals so that we can store them somewhere, as well as a counter for counting the number of proposals.
Now let's build their supporting functions, starting with the voting function:
<code>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), "Vote power exceeds token balance."); 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; }</code>
Note the function modifier: By adding this modifier to our contract, we can append it to any future function and make sure that only token holders can execute the function. This is a reusable security check!
The voting function performs some sanitary checks, such as the voting right is positive, the voter has enough tokens to actually vote, etc. We then take the proposal from the storage and make sure it is neither expired nor executed. It makes no sense to vote on a completed proposal. We also need to make sure that this person hasn't voted yet. We can allow voting rights to be changed, but this can put DAOs in the face of some loopholes, such as people withdrawing their votes at the last minute, etc. Perhaps a candidate for a future version?
We then register the new vote into the proposal, change the current result to find the score, and finally issue the voting event. But what is token.increaseLockedAmount?
This logic increases the number of locked tokens for users. This function can only be executed by the owner of the token contract (this time it is hoped to be a DAO) and will prevent the user from sending more than the locked amount registered to their account. This lock is released after the proposal fails or is executed.
Now let's write a function for presenting delete entries.
Voting to delete and blacklist
In the first part of this series, we planned three entry deletion functions:
Deleting five entries for a single address will result in blacklisting.
Let's see how we do this now. First, delete the function:
<code>struct Proposal { string description; bool executed; int256 currentResult; uint8 typeFlag; // 1 = delete bytes32 target; // 提案目标的ID。例如,标志1,目标XXXXXX(哈希)表示删除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; }</code>
After submitting, the proposal is added to the proposal list and the target entry is noted by the entry hash. Save the description and add some default values and calculate the deadline based on the proposal type. An add-proposal event is issued and the total number of proposals increases.
Next, let's see how to implement the proposal. To be enforceable, a proposal must have sufficient votes and must exceed its deadline. The execution function will accept the ID of the proposal to be executed. There is no easy way to have the EVM execute all pending proposals at once. There may be too many pending proposals to be executed and they may make significant changes to the data in the DAO, which may exceed the gas limits of the Ethereum block, resulting in transaction failures. It is much easier to build a manual execution function that can be called by anyone who complies with well-defined rules, so that the community can focus on proposals that need to be executed.
<code>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), "Vote power exceeds token balance."); 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; }</code>
We get the proposal by its ID, check that it meets the unexecuted and the deadline has expired requirements, and then if the type of proposal is to delete the proposal and the vote is positive, we use the written delete function and finally issue the New event (add it to the top of the contract). The assert call works the same way as the require statement there: assert is usually used to "assert" the result is true. Require is used for prerequisites. Functionally they are the same, the difference is that the assert statement cannot accept message parameters to handle situations where they fail. This function ends by unlocking tokens for all votes in the proposal.
We can add other types of proposals using the same method, but first, let's update the deleteSubmission function to prohibit users with five or more deletes on their account: This means they're always submitting content that the community objected . Let's update the deleteSubmission function:
<code>struct Proposal { string description; bool executed; int256 currentResult; uint8 typeFlag; // 1 = delete bytes32 target; // 提案目标的ID。例如,标志1,目标XXXXXX(哈希)表示删除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; }</code>
This is better. Automatically blacklisted and deleted five times. It is unfair not to give blacklisted addresses a chance to redeem. We also need to define the blacklist function itself. Let's do both of these things and set the fee to cancel the blacklist to, for example, 0.05 Ether.
<code>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), "Vote power exceeds token balance."); 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; }</code>
Please note that tokens for blacklisted accounts will be locked until they send a fee to cancel the blacklist.
Other types of votes
Try writing other proposals based on the inspiration for the functions we wrote above. For spoilers, check out the project's GitHub code base and copy the final code from there. For brevity, let's move on to the other functions we still have in the DAO.
End of the chapter
Once the time or chapter limit of the story is reached, it is time to end the story. After the date, anyone can call the end function, which will allow the dividend to be withdrawn. First, we need a new StoryDAO property and an event:
<code>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; }</code>
Then, let's build the function:
<code>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); }</code>
Simple: It disables the story after sending the collected fees to the owner and sending an event. But in reality, this doesn't really change the overall situation of DAO: other functions don't react to its end. So let's build another modifier:
<code>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; }</code>
Then we add this modifier to all functions except withdrawToOwner, as shown below:
<code>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 < proposals.length; i++) { if (proposals[i].executed == false && proposals[i].voters[_voter] == true) { return false; } } return true; }</code>
If there are still remaining tokens in the DAO, let's take them back and take ownership of those tokens so that we can use them later on for another story:
<code>bool public active = true; event StoryEnded();</code>
unlockMyTokens function is used to unlock all locked tokens that a particular user may lock. This should not happen, and this function should be removed with a lot of tests.
Dip dividend distribution and withdrawal
Now that the story is over, the fees charged for submissions need to be allocated to all token holders. We can reuse our whitelist to mark all those who have already withdrawn their fees:
<code>function endStory() storyActive external { withdrawToOwner(); active = false; emit StoryEnded(); }</code>
If these dividends are not withdrawn within a certain time limit, the owner can obtain the remaining portion:
<code>modifier storyActive() { require(active == true); _; }</code>
As a homework, consider how easy or difficult it is to reuse this deployed smart contract, clear its data, keep tokens in the pool and restart another chapter from here without redeploying. Try doing this yourself and follow the code base for future updates that this series covers! Also consider additional incentives: maybe the number of tokens in the account will affect the dividends they receive from the end of the story? Your imagination is infinite!
Deployment Issues
Given that our contract is now quite large, deploying and/or testing it may exceed the gas limits of the Ethereum block. This is what restricts large applications from deploying to the Ethereum network. To deploy it anyway, try using the code optimizer during compilation by changing the truffle.js file to include the optimized solc settings as follows:
<code>struct Proposal { string description; bool executed; int256 currentResult; uint8 typeFlag; // 1 = delete bytes32 target; // 提案目标的ID。例如,标志1,目标XXXXXX(哈希)表示删除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; }</code>
This will run the optimizer 200 times on the code to find areas that can be narrowed down, deleted, or abstracted before deployment, which should significantly reduce deployment costs.
Conclusion
This ends our detailed DAO development – but the course is not over yet! We still have to build and deploy the UI for this story. Fortunately, building the front end is much easier because the backend is fully hosted on the blockchain. Let's take a look at this in the penultimate part of this series.
Frequently Asked Questions about Building Ethereum DApps and Voting with Custom Tokens
Blockchain Voting is a decentralized voting system that utilizes the transparency and security of blockchain technology. In theory, it should work perfectly, but in practice it often encounters challenges. The voting process involves creating smart contracts on the Ethereum blockchain, and each vote is a verifiable transaction. However, issues such as voter anonymity, voting manipulation, and the technical complexity of using blockchain platforms may hinder their actual implementation.
DAO (Decentralized Autonomous Organization) voting mechanism is a system that allows token holders in DAO to vote on proposals based on their token ownership. The most common mechanisms include simple majority voting, which is accepted if the proposal receives more than 50% of the votes, and a secondary voting, where the cost of voting multiple votes on the proposal increases exponentially.
Governance in secure tokens is usually handled through a voting system where token holders can vote on various aspects of the project. This may include decisions about project development, token economics, and even changes in the governance system itself. The voting rights of token holders are usually proportional to the number of tokens they hold.
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 the tokens representing the voting rights are distributed to members. Members can then propose and vote for changes to the organization.
Holding DAO governance tokens may be risky due to cryptocurrencies' volatility and regulatory uncertainty surrounding DAO. For example, the Commodity Futures Trading Commission (CFTC) warns that using DAO tokens for voting could be considered a form of market manipulation. Additionally, token holders may lose their investment if the DAO is poorly managed or becomes a victim of hacker attacks.
Creating custom tokens for votes in Ethereum DApp involves writing and deploying smart contracts on the Ethereum blockchain. This contract defines the attributes of the token, such as its name, symbol, and total supply. Once the contract is deployed, the tokens can be distributed to users, and users can then use them to vote on proposals in the DApp.
Blockchain voting offers a variety of 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.
Since voter anonymity in blockchain voting can be challenging due to the transparent nature of blockchain transactions. However, techniques such as zero-knowledge proof can be used to verify the validity of the voter without revealing the identity of the voter.
Implementing blockchain voting can be challenging due to technical complexity, regulatory uncertainty and potential security risks. Users need to be familiar with blockchain technology to participate in the voting process, and regulators may express concerns about the legitimacy and security of the blockchain voting system.
Mitigating risks associated with DAO governance tokens includes careful management of DAOs, thorough security measures, and keeping an eye on regulatory developments at all times. It is also important to diversify your portfolio and not invest more than you can afford.
The above is the detailed content of Building Ethereum DApps: Voting with Custom Tokens. For more information, please follow other related articles on the PHP Chinese website!