Skip to main content

Creating a DAO on Velas

Velas provides the ideal infrastructure for building a decentralized, efficient, transparent DAO.

Teams can enjoy various benefits of creating DAOs on Velas. These include having a fully autonomous and transparent organization, leveraging smart contracts built on Ethereum, enjoying lightening fast transaction speed at near-zero cost, and more.

Summary

  • The future of organizations will manifest itself in the form of a DAO, for which Velas provides an efficient infrastructure.
  • Leveraging Velas (VLX) blockchain, developers can build DAOs in multiple languages, and cross-chain interactions are supported.
  • Notably, when building a DAO, the team needs to think comprehensively about the DAO structure, tokenomics, community establishment, etc.
  • DAOs have chosen to join the Velas blockchain to benefit from its low cost, high efficiency, and the Velas $100 million grant program.
  • This article will show developers how to create a DAO on Velas step by step.

Introduction

Briefly speaking, a DAO is a virtual community-led entity without any central leadership, and the decisions are made through voting on the blockchain. As this can bring a more democratic, transparent, and equitable organizational governance, DAO is entering the mainstream of business management, especially favored by the crypto community.

This article will show how to start your DAO on Velas. Before diving into the steps, developers need to have a Velas wallet and own VLX, because dapps interact with the Velas blockchain by sending transactions with one or more instructions.

Follow this link to learn how to use Velas Wallet.

DAOs on Velas are built to scale

Unlike traditional organizations, which have their rules written down on paper, the rules and regulations of a DAO are programmed open-source and run by smart contracts. Leveraging Velas blockchain, developers can build DAOs backed by smart contracts in multiple languages. In addition, DAOs on Velas are built to scale since the Velas blockchain supports cross-chain interactions.

Let’s go through the steps to create your DAO on Velas.

Decide the type of a DAO

The construction of an organization requires careful thought, and so does the creation of a DAO. The reasons and motivations behind the creation of a DAO are worth pondering. These considerations will help determine the best structure for a DAO.

  • What do you want to achieve with your DAO?
  • What are the short and long-term visions of the DAO?
  • Is a decentralized, non-hierarchical ownership structure necessary for your project?

These are just some sample questions to think about.

Organizations can also use their imaginations to come up with their own choice of DAO, but generally, they cover the following types:

  • Protocol DAOs
  • Grant DAOs
  • Social DAOs
  • Collector DAOs
  • Venture DAOs
  • Media DAOs
  • Social Media DAOs
  • Entertainment DAOs

Design the tokenomics

The goal of DAOs is to replace corporations and organize various commercial and non-commercial activities in a bottom-up manner. For these activities to run smoothly, economic incentives are naturally needed. Therefore, strategically considering the distribution and utilities of tokens in DAOs is critical to the development of the community.

DAO tokens can be used for:

  • Rewards and incentives
  • DAO governance and voting on the direction of the DAO
  • Unlocking other benefits and opportunities for your community

Other critical aspects of tokenomics include the token’s initial supply and allocation. They serve as part of the business plan of a DAO; therefore, these matters are essential for investors to consider when making an investment decision.

Build and engage your community

For teams building a DAO for the first time, there is no need to worry too much, as third-party tools are available on the market to help them start quickly.

However, the most crucial part that makes a DAO successful is the people in its community. If a DAO has an engaged, active, and devoted community, it is more likely to survive in the fierce competition and even thrive.

It’s worth mentioning that building a community isn’t limited to making appearances on social media such as Discord, Telegram, or Twitter. A vibrant community relies on builders, creators, and Web3 enthusiasts who share the same vision and contribute together.

A step-by-step guide on creating a DAO on Velas

In this chapter of the article, we will show you how to deploy an actual DAO smart contract on the Velas testnet.

Prerequisites

In this tutorial, we will use Hardhat for development. However, we will not go over the basics of Hardhat and Solidity here. So if you are not familiar with these technologies, please equip yourself with the basics of Solidity and Hardhat first.

Setup

For setup, please create a new Hardhat project on your machine and remove all the files (.sol) in the contract folder created by default.

How to create a governance token

Token-based membership models like MakerDAO are currently the dominant form of DAO organization. Therefore, we will guide you through creating your own governance token. Since Velas is EVM-compatible, the token we create will be an ERC20 token.

Here we are not concerned with how to distribute tokens to users as this is not a real project. If you want to find out strategies for token distribution, it is recommended to study different projects like MakerDAO and compare them.

To create governance tokens, first, you need to create a file called GovernanceToken.sol in the contracts folder and add the following code to that file.

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract GovernanceERC20 is ERC20 {
constructor(uint256 initialSupply) ERC20("Governance Token", "GT") {
_mint(msg.sender, initialSupply);
}
}

How to write a DAO smart contract

Since smart contract is the crucial part of creating a fully functional DAO, we will go through this process step by step and code each part separately as we need it. To start with, let’s create a barebone contract and add it to a file called DAO.sol. You can create the file in the contracts folder.

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract DAO {

}

Creating constructor

Now we are going to make this DAO contract compatible with any ERC20 token.

We can interact with any ERC20 token by using the IERC20 interface. To do so, we must import the IERC20 interface from Openzeppelin Library and then declare a variable called governancetoken that uses the IERC20 interface.

Now, we need to create the constructor of the smart contract. In that, we will employ an argument called _governancetoken, which is the address of our governance token. Inside the constructor, we will assign a value to the governancetoken variable using IERC20 and pass it to address _governancetoken.

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract DAO {
IERC20 public governancetoken;

constructor(address _governancetoken) {
governancetoken = IERC20(_governancetoken);
}
}

Defining proposal struct

Whenever a member creates a proposal, they have to use a predefined format. We will achieve this by using a struct data type to define the format of each proposal. Now, let’s create a struct named Proposal, as shown below.

struct Proposal {
address proposer;
string name;
uint256 votesForApprove;
uint256 votesForDeny;
uint256 startTime;
Status status;
}

As you can see above, each property will have a data type associated with it. You will know most of those data types, but we have added a custom data type called Status. More details about data type “status” will be explained in the following sections.

Tracking proposals

After creating a proposal, we have to save it in storage. To do that, we use proposal mapping, with index (in this case, uint256) as a key and Proposal as a value. Add the mapping to our smart contract and name it public proposals, making it visible to the public.

mapping(uint256 => Proposal) public proposals;

Other global variables and enums

To make the smart contract fully functional, we still need to add some other global variables and enums to it.

To start with, we will add the Status enum. This data type that we mentioned previously defines the status of a proposal. Enums help us restrict a variable’s value to predefined values. Usually, we want to restrict the value of the status variable in the proposal struct to the following ones.

  • Approved
  • Denied
  • InProgress
  • Expired

Let’s go over each of them. So, the status will be Approved if members holding 51% of governance tokens approve the proposal, and the same condition applies to the Denied status.

If neither approval nor rejection exceeds 51% of the total number of votes cast, the status of the proposal will be changed to Expired. And, of course, when the proposal is in its voting period, it will have the status showing InProgress.

We will also create an enum for an individual member’s vote type. This will restrict members’ voting options to Approve or Deny.

enum Option { Approve, Deny }
enum Status { Approved, Denied, InProgress, Expired}

At this moment, we still need two more variables. One is for giving an index to each proposal when we add them to proposal mapping, and the other is for storing the value of the voting period. We will keep the voting period fixed for all the proposals, which will be seven days.

uint256 public indx;
uint256 constant public VOTING_PERIOD = 7 days;

As we are mid-way through this tutorial, our smart contract should look similar to the following.

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract DAO {

enum Option { Approve, Deny }
enum Status { Approved, Denied, InProgress, Expired}

struct Proposal {
address proposer;
string name;
uint256 votesForApprove;
uint256 votesForDeny;
uint256 startTime;
uint256 votingPeriod;
Status status;
}

mapping(uint256 => Proposal) public proposals;

IERC20 public governancetoken;


uint256 public indx;
uint256 constant public VOTING_PERIOD = 7 days;

constructor(address _governancetoken) {
governancetoken = IERC20(_governancetoken);
}
}

Restricting access to member

Since there are usually many types of members in a DAO, not all members are treated the same in a DAO. So we want to limit some DAO members’ access to certain functionalities. To do that, we will use a modifier. The modifier is called isMember. In our case, we use it only to allow governance token holders to access the function.

 modifier isMember {
uint256 balance = governancetoken.balanceOf(msg.sender);
require(balance > 0, "Not a member of quorum");
_;
}

Function for creating a proposal

With all the preparation done, let’s write the core functionality of our smart contract, proposal creation. So how to write a function that allows members (in this case, people who hold governance tokens) to create proposals?

 function createProposal(string memory _name) public isMember returns(Proposal memory) {

proposals[indx] = Proposal(
msg.sender,
_name,
0,
0,
block.timestamp,
VOTING_PERIOD,
Status.InProgress
);

indx++;

return proposals[indx-1];
}

When creating a proposal, the proposer only needs to submit the name of the proposal while calling the function. To do that, we have an argument called _name with type string.

Access to this feature is restricted to governance members. So let’s determine the visibility of public with the isMember modifier. Inside, we are storing a proposal with proposal mapping using indx as a key. In the end, we increment the indx by 1 and return the created proposal.

While creating a proposal, we have to use the proposal struct with all the values as arguments. The way to set the values of a new proposal is as follows.

  • proposer: save the address of the person who calls the function
  • name: set it to a string that is passed as a function argument
  • voteForApprove: set it to 0
  • voteForDeny: set it to 0
  • startTime: current block timestamp
  • status: set it to Status.InProgress

Tracking members who voted

After creating a proposal, voting will begin. The most important thing now to ensure is not to let members vote twice. We will use nested mappings where the first mapping will have proposal index (indx) as a key and boolean as a value, and the outer mapping will have that address as a key and previous mapping as a value.

This way, we will track the address of each member and whether they have voted on a particular proposal or not.

mapping(address => mapping(uint256 => bool)) public voted;

Voting function

Now comes the exciting part, writing a function that allows members to vote. The vote function will take two arguments. The first is the index of the proposal (_proposal), and the second is the voting option (Option struct).

As determined before, this function is still only accessible to the members with the isMember modifier. Since we will change the smart contract’s state, it needs to have visibility to the public.

How does the vote function work?

First, we need to retrieve the proposal stored in the proposal mapping for users to vote on. We can access any proposal using the mapping name followed by square brackets with a key.

Here we are going to use proposals[_proposal]. Store the proposal in a variable called proposal, which has the type of Proposal followed by keyword storage. Keyword storage allows us to access stored data, and data is updated according to changes we make.

Adding more conditions to voting

Our job to create a vote function doesn’t stop here, as we need to add some conditions to restrict voting. For example, what if the voting period has ended, or the user has already voted?

In these cases, we will use the require function for both of them. In the first condition, we want to check if the voting has ended or not. For that, we can use the following statement.

proposal.startTime + VOTING_PERIOD >= block.timestamp 

If the condition is satisfied, it will throw an error saying “Already Voted!”.

In the second condition, we want to check if the user has already voted or not. To do that, we can check if the flag of the user’s address for voting is false or not. Using the statement below can help us achieve that.

voted[msg.sender][_proposal] == false

If the flag is true, it will display an error saying “Already Voted!”.

Whenever a user votes, the balance of their governance token will be considered as the number of votes cast by that user. Therefore, we will update votes accordingly. To get the number of tokens the user has, we can use the balanceOf method of governancetoken.

If the user votes to approve, then add that balance to votesForApprove. On the contrary, if the user votes to deny, then add balance to votesForDeny. Lastly, set the voted flag for that address to true.

 function vote(uint256 _proposal, Option _vote) public isMember {
Proposal storage proposal = proposals[_proposal];
require(proposal.startTime + VOTING_PERIOD >= block.timestamp, "Voting Ended");
require(voted[msg.sender][_proposal] == false, "Already Voted!");

uint256 balance = governancetoken.balanceOf(msg.sender);

if (_vote == Option.Approve) {
proposal.votesForApprove = proposal.votesForApprove + balance;
} else {
proposal.votesForDeny = proposal.votesForDeny + balance;
}

voted[msg.sender][_proposal] = true;
}

Updating the status of the proposal

Now that we have created the function for voting, the next step is to code a function to update the status of the proposal when it ends. The meanings of different statutes for any proposal are explained in Other global variables and enums sections.

Now let’s move on to how to make a decision and update the status of a proposal. For the updateStatus function, we will take the index of the proposal as a function argument. The process also starts with retrieving the proposal from proposal mapping. Then we need to check if the proposal is still within the voting period. If not, we can move on to the next step.

To check the result of a proposal, we need to do some calculations, but how? Firstly, we have to get half of the supply of the governance token (halfOfSupply). To do that, we need to get the total supply using the totalSupply function of governancetoken, and then divide it by two.

If votesForApprove is greater than halfOfSupply, then set the status to be approved (Status.Approved). If votesForDeny is greater than halfOfSupply then set the status to be denied (Status.Denied). What if none of the conditions are satisfied? Then we can set the status to be expired (Status.expired).

function updateStatus(uint256 _proposal) public returns(Status){
Proposal storage proposal = proposals[_proposal];
require(proposal.startTime + VOTING_PERIOD <= block.timestamp, "Voting In Progress");
uint256 halfOfSupply = governancetoken.totalSupply() / 2;

if (proposal.votesForApprove > halfOfSupply) {
proposal.status = Status.Approved;
} else if (proposal.votesForDeny > halfOfSupply) {
proposal.status = Status.Denied;
} else {
proposal.status = Status.Expired;
}

return proposal.status;
}

Preview of the whole smart contract

By now, the hardest part is over. Let’s have a look at what we’ve achieved. The DAO smart contract should look like the following.

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract DAO {

enum Option { Approve, Deny }
enum Status { Approved, Denied, InProgress, Expired}

struct Proposal {
address proposer;
string name;
uint256 votesForApprove;
uint256 votesForDeny;
uint256 startTime;
Status status;
}


mapping(uint256 => Proposal) public proposals;

mapping(address => mapping(uint256 => bool)) public voted;

IERC20 public governancetoken;

uint256 public indx;
uint256 constant public VOTING_PERIOD = 7 days;

constructor(address _governancetoken) {
governancetoken = IERC20(_governancetoken);
}

function createProposal(string memory _name) public isMember returns(Proposal memory) {

proposals[indx] = Proposal(
msg.sender,
_name,
0,
0,
block.timestamp,
Status.InProgress
);

indx++;

return proposals[indx-1];
}

function vote(uint256 _proposal, Option _vote) public isMember {
Proposal storage proposal = proposals[_proposal];
require(proposal.startTime + VOTING_PERIOD >= block.timestamp, "Voting Ended");
require(voted[msg.sender][_proposal] == false, "Already Voted!");

uint256 balance = governancetoken.balanceOf(msg.sender);

if (_vote == Option.Approve) {
proposal.votesForApprove = proposal.votesForApprove + balance;
} else {
proposal.votesForDeny = proposal.votesForDeny + balance;
}

voted[msg.sender][_proposal] = true;
}

function updateStatus(uint256 _proposal) public returns(Status){
Proposal storage proposal = proposals[_proposal];
require(proposal.startTime + VOTING_PERIOD <= block.timestamp, "Voting In Progress");
uint256 halfOfSupply = governancetoken.totalSupply() / 2;

if (proposal.votesForApprove > halfOfSupply) {
proposal.status = Status.Approved;
} else if (proposal.votesForDeny > halfOfSupply) {
proposal.status = Status.Denied;
} else {
proposal.status = Status.Expired;
}

return proposal.status;
}

modifier isMember {
uint256 balance = governancetoken.balanceOf(msg.sender);
require(balance > 0, "Not a member of quorum");
_;
}
}

Deploying contract on Velas Testnet

Congratulations! You are one step away from running your own DAO. Are you ready to deploy your smart contract on Velas?

To deploy the contract, we have to write a script that deploys the contract. Create a deploy.js file inside the scripts folder and paste the following code into that file.

const hre = require("hardhat");

async function main () {
// We get the contract to deploy
const GovernanceToken = await hre.ethers.getContractFactory('GovernanceERC20');
console.log("Deploying Governance Token...");
const governanceToken = await GovernanceToken.deploy(1000);
await governanceToken.deployed();
console.log('Governance Token deployed to:', governanceToken.address);

const DAO = await hre.ethers.getContractFactory('DAO');
console.log('Deploying DAO...');
const dao = await DAO.deploy(governanceToken.address);
await dao.deployed();
console.log('DAO deployed to:', dao.address);
}

main()
.then(() => process.exit(0))
.catch(error => {
console.error(error);
process.exit(1);
});

Before deploying the contract, we have to change module.exports in hardhat.config.js file.

module.exports = {
solidity: "0.8.4",
networks: {
velastestnet: {
url: `https://evmexplorer.testnet.velas.com/rpc`,
accounts: [process.env.PRIVATE_KEY]
}
}
};

This adds Velas testnet details to our hardhat config. Also, before deploying the contract, you must add private keys for your wallet to the .env file. Moreover, make sure that you have some testnet Velas tokens in the wallet. You can get testnet tokens from here.

Now that everything is set up, run the following command in the terminal.

npx hardhat run scripts/deploy.js --network velastestnet

Check the output.

Deploying Governance Token...
Governance Token deployed to: 0x158C94E88d3AC6967e7690eAeac3FB6c8BF68502
Deploying DAO...
DAO deployed to: 0x190b8BF3f887D4F00AcF57e95AE478270Cc7fdC5

If your output is similar to what you see above, you have deployed the contract successfully!

Last but not least, all the information related to creating DAO smart contracts is in this repo. If you want to check the code or see the tests for the smart contract, check out this page.