Vote Contract
Description: This is the most complex contract in this guide. It covers voting mechanisms, security considerations, and advanced data structures to ensure a fair and transparent voting process.
Purpose: To teach you about complex logic implementation, security best practices, and efficient data management in smart contracts.
Difficulty Level: Difficult
Step 1 - Setting up your development environment#
Install Required Packages
1dotnet new --install AElf.ContractTemplates
AELF.ContractTemplates contains various predefined templates for the ease of developing smart contracts on the aelf blockchain.
1dotnet tool install --global aelf.deploy
aelf.deploy is a utility tool for deploying smart contracts on the aelf blockchain. Please remember to export PATH after installing aelf.deploy.
Note: If you have installed aelf.deploy and your terminal says that there is no such command available, please uninstall and install aelf.deploy.
Install Node.js and Yarn
1sudo npm i -g aelf-command
aelf-command is a CLI tool for interacting with the aelf blockchain, enabling tasks like creating wallets and managing transactions. Provide required permissions while installing aelf-command globally.
Install Git
As we will be using a ready made project, we will require git to clone from the project.
Step 2 - Develop Smart Contract#
Project Setup#
1mkdir capstone_aelf2cd capstone_aelf
1dotnet new aelf -n BuildersDAO
Adding Your Smart Contract Code#
Defining Methods and Messages#
Let's add the RPC methods and message definitions to our Voting dApp.
1syntax = "proto3";23import "aelf/core.proto";4import "aelf/options.proto";5import "google/protobuf/empty.proto";6import "Protobuf/reference/acs12.proto";78// The namespace of this class9option csharp_namespace = "AElf.Contracts.BuildersDAO";1011service BuildersDAO {12// The name of the state class the smart contract is going to use to access13// blockchain state14option (aelf.csharp_state) = "AElf.Contracts.BuildersDAO.BuildersDAOState";15option (aelf.base) = "Protobuf/reference/acs12.proto";1617// Actions -> Methods that change state of smart contract18// This method sets up the initial state of our StackUpDAO smart contract19rpc Initialize(google.protobuf.Empty) returns (google.protobuf.Empty);2021// This method allows a user to become a member of the DAO by taking in their22// address as an input parameter23rpc JoinDAO(aelf.Address) returns (google.protobuf.Empty);2425// This method allows a user to create a proposal for other users to vote on.26// The method takes in a "CreateProposalInput" message which comprises of an27// address, a title, description and a vote threshold (i.e how many votes28// required for the proposal to pass)29rpc CreateProposal(CreateProposalInput) returns (Proposal);3031// This method allows a user to vote on proposals towards a specific proposal.32// This method takes in a "VoteInput" message which takes in the address of33// the voter, specific proposal and a boolean which represents their vote34rpc VoteOnProposal(VoteInput) returns (Proposal);3536// Views -> Methods that does not change state of smart contract37// This method allows a user to fetch a list of proposals that had been38// created by members of the DAO39rpc GetAllProposals(google.protobuf.Empty) returns (ProposalList) {40option (aelf.is_view) = true;41}4243// aelf requires explicit getter methods to access the state value,44// so we provide these three getter methods for accessing the state45// This method allows a user to fetch a proposal by proposalId46rpc GetProposal (google.protobuf.StringValue) returns (Proposal) {47option (aelf.is_view) = true;48}4950// This method allows a user to fetch the member count that joined DAO51rpc GetMemberCount (google.protobuf.Empty) returns (google.protobuf.Int32Value) {52option (aelf.is_view) = true;53}5455// This method allows a user to check whether this member is exist by address56rpc GetMemberExist (aelf.Address) returns (google.protobuf.BoolValue) {57option (aelf.is_view) = true;58}59}6061// Message definitions62message Member {63aelf.Address address = 1;64}6566message Proposal {67string id = 1;68string title = 2;69string description = 3;70repeated aelf.Address yesVotes = 4;71repeated aelf.Address noVotes = 5;72string status = 6; // e.g., "IN PROGRESS", "PASSED", "DENIED"73int32 voteThreshold = 7;74}7576message CreateProposalInput {77aelf.Address creator = 1;78string title = 2;79string description = 3;80int32 voteThreshold = 4;81}8283message VoteInput {84aelf.Address voter = 1;85string proposalId = 2;86bool vote = 3; // true for yes, false for no87}8889message MemberList {90repeated Member members = 1;91}9293message ProposalList {94repeated Proposal proposals = 1;95}
Understanding the Code#
Defining Contract State#
1using System.Collections.Generic;2using System.Diagnostics.CodeAnalysis;3using AElf.Sdk.CSharp.State;4using AElf.Types;56namespace AElf.Contracts.BuildersDAO7{8// The state class is access the blockchain state9public class BuildersDAOState : ContractState10{11public BoolState Initialized { get; set; }12public MappedState<Address, bool> Members { get; set; }13public MappedState<string, Proposal> Proposals { get; set; }14public Int32State MemberCount { get; set; }15public Int32State NextProposalId { get; set; }16}17}
Understanding the Code#
Next Step#
Implement Voting Smart Contract Logic#
Checking Smart Contract Logics#
1using System.Collections.Generic;2using System.Security.Principal;3using AElf.Sdk.CSharp;4using AElf.Sdk.CSharp.State;5using AElf.Types;6using Google.Protobuf.WellKnownTypes;78namespace AElf.Contracts.BuildersDAO9{10public class BuildersDAO : BuildersDAOContainer.BuildersDAOBase11{12const string author = "REPLACE PLACEHOLDER HERE";1314// Implement Initialize Smart Contract Logic15public override Empty Initialize(Empty input) { }1617// Implement Join DAO Logic18public override Empty JoinDAO(Address input) { }1920// Implement Create Proposal Logic21public override Proposal CreateProposal(CreateProposalInput input) { }2223// Implement Vote on Proposal Logic24public override Proposal VoteOnProposal(VoteInput input) { }2526// Implement Get All Proposals Logic27public override ProposalList GetAllProposals(Empty input) { }2829// Implement Get Proposal Logic30public override Proposal GetProposal(StringValue input) { }3132// Implement Get Member Count Logic33public override Int32Value GetMemberCount(Empty input) { }3435// Implement Get Member Exist Logic36public override BoolValue GetMemberExist(Address input) { }37}38}
Implementing Initialize Function#
1// Implement Initialize Smart Contract Logic2public override Empty Initialize(Empty input)3{4Assert(!State.Initialized.Value, "already initialized");5var initialProposal = new Proposal6{7Id = "0",8Title = "Proposal #1",9Description = "This is the first proposal of the DAO",10Status = "IN PROGRESS",11VoteThreshold = 1,12};13State.Proposals[initialProposal.Id] = initialProposal;14State.NextProposalId.Value = 1;15State.MemberCount.Value = 0;1617State.Initialized.Value = true;1819return new Empty();20}
Implementing Join DAO Function#
You'll implement this function. Once done, you can proceed to the next page to compare your code with the reference implementation.
1// Implement Join DAO Logic2public override Empty JoinDAO(Address input)3{4// Based on the address, determine whether the address has joined the DAO. If it has, throw an exception5// If the address has not joined the DAO, then join and update the state's value to true6// Read the value of MemberCount in the state, increment it by 1, and update it in the state7// Using 'return null' to ensure the contract compiles successfully. Please update it to the correct return value when implementing8return null;9}
Implementing Create Proposal Function#
Now, use the provided code snippet to fill in the CreateProposal function.
1// Implement Create Proposal Logic2public override Proposal CreateProposal(CreateProposalInput input)3{4Assert(State.Members[input.Creator], "Only DAO members can create proposals");5var proposalId = State.NextProposalId.Value.ToString();6var newProposal = new Proposal7{8Id = proposalId,9Title = input.Title,10Description = input.Description,11Status = "IN PROGRESS",12VoteThreshold = input.VoteThreshold,13YesVotes = { }, // Initialize as empty14NoVotes = { }, // Initialize as empty15};16State.Proposals[proposalId] = newProposal;17State.NextProposalId.Value += 1;18return newProposal; // Ensure return19}
Implementing Vote On Proposal Function#
Now, use the provided code snippet to complete the VoteOnProposal function.
1// Implement Vote on Proposal Logic2public override Proposal VoteOnProposal(VoteInput input)3{4Assert(State.Members[input.Voter], "Only DAO members can vote");5var proposal = State.Proposals[input.ProposalId]; // ?? new proposal6Assert(proposal != null, "Proposal not found");7Assert(8!proposal.YesVotes.Contains(input.Voter) && !proposal.NoVotes.Contains(input.Voter),9"Member already voted"10);1112// Add the vote to the appropriate list13if (input.Vote)14{15proposal.YesVotes.Add(input.Voter);16}17else18{19proposal.NoVotes.Add(input.Voter);20}2122// Update the proposal in state23State.Proposals[input.ProposalId] = proposal;2425// Check if the proposal has reached its vote threshold26if (proposal.YesVotes.Count >= proposal.VoteThreshold)27{28proposal.Status = "PASSED";29}30else if (proposal.NoVotes.Count >= proposal.VoteThreshold)31{32proposal.Status = "DENIED";33}3435return proposal;36}
Implementing Get All Proposals Function#
You'll implement this function. Once done, you can proceed to the next page to compare your code with the reference implementation.
1// Implement Get All Proposals Logic2public override ProposalList GetAllProposals(Empty input)3{4// Create a new list called ProposalList5// Start iterating through Proposals from index 0 until the value of NextProposalId, read the corresponding proposal, add it to ProposalList, and finally return ProposalList6// Using 'return null' to ensure the contract compiles successfully. Please update it to the correct return value when implementing7return null;8}
Implementing Get Proposal / Get Member Count / Get Member Exist Functions#
Implement these methods to access different states effectively in your smart contract.
1// Implement Get Proposal Logic2public override Proposal GetProposal(StringValue input)3{4var proposal = State.Proposals[input.Value];5return proposal;6}78// Implement Get Member Count Logic9public override Int32Value GetMemberCount(Empty input)10{11var memberCount = new Int32Value {Value = State.MemberCount.Value};12return memberCount;13}1415// Implement Get Member Exist Logic16public override BoolValue GetMemberExist(Address input)17{18var exist = new BoolValue {Value = State.Members[input]};19return exist;20}
With that, we have implemented all the functionalities of our Voting dApp smart contract.
In the next step, we will compile our smart contract and deploy our written smart contract to the aelf sidechain.
Complete Implementation#
Implementing Join DAO Function#
1public override Empty JoinDAO(Address input)2{3// Based on the address, determine whether the address has joined the DAO. If it has, throw an exception4Assert(!State.Members[input], "Member is already in the DAO");5// If the address has not joined the DAO, then join and update the state's value to true6State.Members[input] = true;7// Read the value of MemberCount in the state, increment it by 1, and update it in the state8var currentCount = State.MemberCount.Value;9State.MemberCount.Value = currentCount + 1;10return new Empty();11}
Implementing Get All Proposals Function#
1public override ProposalList GetAllProposals(Empty input)2{3// Create a new list called ProposalList4var proposals = new ProposalList();5// Start iterating through Proposals from index 0 until the value of NextProposalId, read the corresponding proposal, add it to ProposalList, and finally return ProposalList6for (var i = 0; i < State.NextProposalId.Value; i++)7{8var proposalCount = i.ToString();9var proposal = State.Proposals[proposalCount];10proposals.Proposals.Add(proposal);11}12return proposals;13}
Once you've implemented these two methods and run the unit tests again, you should see that all test cases pass.
Step 3 - Deploy Smart Contract#
Create A Wallet#
To send transactions on the aelf blockchain, you must have a wallet.
Run this command to create aelf wallet.
1aelf-command create
You will be prompted to save your account, please do save your account as shown below:
1? Save account info into a file? (Y/n) Y
Next, enter and confirm your password. Then export your wallet password as shown below:
1export WALLET_PASSWORD="YOUR_WALLET_PASSWORD"
Acquire Testnet Tokens (Faucet) for Development#
To deploy smart contracts or execute on-chain transactions on aelf, you'll require testnet ELF tokens.
Get ELF Tokens
Run the following command to get testnet ELF tokens from faucet. Remember to either export your wallet address and wallet password or replace ππ΄πΏπΏπΈππ΄π·π·π πΈπππππWALLETADDRESSandWALLET_ADDRESS with your wallet address and wallet password respectively.
1export WALLET_ADDRESS="YOUR_WALLET_ADDRESS"2curl -X POST "https://faucet.aelf.dev/api/claim?walletAddress=$WALLET_ADDRESS" -H "accept: application/json" -d ""
To check your wallet's current ELF balance:
1aelf-command call ASh2Wt7nSEmYqnGxPPzp4pnVDU4uhj1XW9Se5VeZcX2UDdyjx -a $WALLET_ADDRESS -p $WALLET_PASSWORD -e https://tdvw-test-node.aelf.io GetBalance
You will be prompted for the following:
1Enter the required param <symbol>: ELF2Enter the required param <owner>: $WALLET_ADDRESS
You should see the result displaying your wallet's ELF balance.
The smart contract needs to be deployed on the chain before users can interact with it.
Run the following command to deploy a contract. Remember to export the path of LotteryGame.dll.patched to CONTRACT_PATH.
1export CONTRACT_PATH=$(find ~+ . -path "*patched*" | head -n 1)
1aelf-deploy -a $WALLET_ADDRESS -p $WALLET_PASSWORD -c $CONTRACT_PATH -e https://tdvw-test-node.aelf.io/
Please wait for approximately 1 to 2 minutes. If the deployment is successful, it will provide you with the contract address.
Export your smart contract address:
1export CONTRACT_ADDRESS="YOUR_SMART_CONTRACT_ADDRESS e.g. 2LUmicHyH4RXrMjG4beDwuDsiWJESyLkgkwPdGTR8kahRzq5XS"
Step 4 - Interact with Your Deployed Smart Contract#
Project Setup#
Let's start by cloning the frontend project repository from GitHub.
Terminal
1git clone https://github.com/AElfProject/vote-contract-frontend.git
Terminal
1cd vote-contract-frontend
Install necessary libraries#
Terminal
1npm install
We are now ready to build the frontend components of our Voting dApp.
Configure Portkey Provider & Write Connect Wallet Function#
We'll set up our Portkey provider to let users connect their Portkey wallets to our app and interact with our voting smart contract.
src/useDAOSmartContract.ts
1//Step A - Setup Portkey Wallet Provider2useEffect(() => {3(async () => {4if (!provider) return null;56try {7// 1. get the sidechain tDVW using provider.getChain8const chain = await provider?.getChain("tDVW");9if (!chain) throw new Error("No chain");1011//Address of DAO Smart Contract12//Replace with Address of Deployed Smart Contract13const address = "2GkJoDicXLqo7cR9YhjCEnCXQt8KUFUTPfCkeJEaAxGFYQo2tb";1415// 2. get the DAO contract16const daoContract = chain?.getContract(address);17setSmartContract(daoContract);18} catch (error) {19console.log(error, "====error");20}21})();22}, [provider]);
The HomeDAO.tsx file is the landing page of our Voting dApp. It allows users to interact with the deployed smart contract, join the DAO, view proposals, and vote on them.
Before users can interact with the smart contract, we need to write the Connect Wallet function.
Find the comment Step B - Connect Portkey Wallet. Replace the existing connect function with this code snippet:
src/HomeDAO.ts
1const connect = async () => {2//Step B - Connect Portkey Wallet3const accounts = await provider?.request({4method: MethodsBase.REQUEST_ACCOUNTS,5});6const account = accounts?.tDVW?.[0];7setCurrentWalletAddress(account);8setIsConnected(true);9alert("Successfully connected");10};
In this code, we fetch the Portkey wallet account using the provider and update the wallet address state variable. An alert notifies the user that their wallet is successfully connected.
With the Connect Wallet function defined, we're ready to write the remaining functions in the next steps.
Write Initialize Smart Contract & Join DAO Functions#
Let's write the Initialize and Join DAO functions.
src/HomeDAO.ts
1const initializeAndJoinDAO = async () => {2//Step C - Write Initialize Smart Contract and Join DAO Logic3try {4const accounts = await provider?.request({5method: MethodsBase.ACCOUNTS,6});7if (!accounts) throw new Error("No accounts");89const account = accounts?.tDVW?.[0];10if (!account) throw new Error("No account");1112if (!initialized) {13await DAOContract?.callSendMethod("Initialize", account, {});14setInitialized(true);15alert("DAO Contract Successfully Initialized");16}1718await DAOContract?.callSendMethod("JoinDAO", account, account);19setJoinedDAO(true);20alert("Successfully Joined DAO");21} catch (error) {22console.error(error, "====error");23}24};
Here's what the function does:#
Now, wrap the initializeAndJoinDAO function in the "Join DAO" button to trigger both Initialize and JoinDAO when clicked.
Next, we'll write the Create Proposal function.
Write Create Proposal Function#
Let's write the Create Proposal function.
src/CreateProposal.tsx
1//Step D - Configure Proposal Form2const form = useForm<z.infer<typeof formSchema>>({3resolver: zodResolver(formSchema),4defaultValues: {5address: currentWalletAddress,6title: "",7description: "",8voteThreshold: 0,9},10});
Here's what the function does:#
Now your form is ready for users to fill in the necessary details for their proposal.
Now, let's write the Create Proposal function for the form submission.
src/CreateProposal.tsx
1// Step E - Write Create Proposal Logic2function onSubmit(values: z.infer<typeof formSchema>) {3const proposalInput: IProposalInput = {4creator: currentWalletAddress,5title: values.title,6description: values.description,7voteThreshold: values.voteThreshold,8};910setCreateProposalInput(proposalInput);1112const createNewProposal = async () => {13try {14await DAOContract?.callSendMethod(15"CreateProposal",16currentWalletAddress,17createProposalInput18);1920navigate("/");21alert("Successfully created proposal");22} catch (error) {23console.error(error);24}25};2627createNewProposal();28}
Here's what the function does:#
Next, we'll write the Vote and Fetch Proposal functions to complete the frontend components of our Voting dApp.
Write Vote & Fetch Proposals Function#
In this step, we'll write the Vote and Fetch Proposals functions to complete our Voting dApp's frontend components.
src/HomeDAO.tsx
1const voteYes = async (index: number) => {2//Step F - Write Vote Yes Logic3try {4const accounts = await provider?.request({5method: MethodsBase.ACCOUNTS,6});78if (!accounts) throw new Error("No accounts");910const account = accounts?.tDVW?.[0];1112if (!account) throw new Error("No account");1314const createVoteInput: IVoteInput = {15voter: account,16proposalId: index,17vote: true,18};1920await DAOContract?.callSendMethod(21"VoteOnProposal",22account,23createVoteInput24);25alert("Voted on Proposal");26setHasVoted(true);27} catch (error) {28console.error(error, "=====error");29}30};
Here's what the function does:#
The voteNo function works similarly but sets the vote to false.
src/HomeDAO.tsx
1useEffect(() => {2// Step G - Use Effect to Fetch Proposals3const fetchProposals = async () => {4try {5const accounts = await provider?.request({6method: MethodsBase.ACCOUNTS,7});89if (!accounts) throw new Error("No accounts");1011const account = accounts?.tDVW?.[0];1213if (!account) throw new Error("No account");1415const proposalResponse = await DAOContract?.callViewMethod<IProposals>(16"GetAllProposals",17""18);1920setProposals(proposalResponse?.data);21alert("Fetched Proposals");22} catch (error) {23console.error(error);24}25};2627fetchProposals();28}, [DAOContract, hasVoted, isConnected, joinedDAO]);
Here's what the function does:#
Now that we've written all the necessary frontend functions and components, we're ready to run the Voting dApp application in the next step.
Run Application#
In this step, we will run the Voting dApp application.
Terminal
1npm run dev
Create Portkey Wallet#
Sign up
With that, you have successfully created your very first Portkey wallet within seconds. How easy was that?
Connect Portkey Wallet
Once you have successfully joined the DAO, you should observe now that the landing page renders the proposal we have defined in our smart contract as shown below.
Upon a successful vote transaction, you should now observe that the proposal status has been updated to "PASSED" as shown below as the Yes vote count has reached the vote threshold.
π Congratulations Learners! You have successfully built your Voting dApp and this is no mean feat.
Edited on: 12 July 2024 06:01:57 GMT+0