Decentralized Governance: Developing a Web3-Infused DAO Application (PART-2)

Harith D. Jayawardhane
CoinsBench
Published in
7 min readDec 16, 2023

--

Implementing Solidity Smart Contracts for Our Web3-Infused DAO

Requirements

VS Code

You can use any code editor. But here using the Visual Studio Code😍. If you want to download it, use this 👉https://code.visualstudio.com/

Installing Brownie

We are going to use the Brownie framework. Brownie is a Python-based development and testing framework for smart contracts targeting the Ethereum Virtual Machine.

To know all the basics about the brownie, you can use the official document by this URL: 👉https://eth-brownie.readthedocs.io/en/stable/

To install this, you can use pipx or pip tools.

Below is how it should be installed via pip:

pip install eth-brownie

Sometimes installation will fail due to some dependencies. For this, the recommended Python version is Python 3.9.0 for now.

Clone the Template

Clone the template using the below code:

git clone https://github.com/HarithDilshan/DAO---Template-by-Roxen.git

Go to the cloned folder and open it from VS Code.

cd .\DAO---Template-by-Roxen\
code .

The folder structure should be like below:

You will see a file name called ‘.env.deletethispart’. You should rename it to ‘.env’.

Coding Smart Contracts

We are going to create smart contracts inside the ‘contracts’ folder. The final file structure will be like the below image.

First, see how contracts are created, and after that, I will describe them.

Box.sol

//SPDX-License-Identifier: MIT

pragma solidity ^0.8.7;

import "@openzeppelin/contracts/access/Ownable.sol";

contract Box is Ownable {
uint256 private value;

event ValueChanged(uint256 newValue);

function store(uint256 newValue) public onlyOwner {
value = newValue;
emit ValueChanged(newValue);
}

function retrieve() public view returns (uint256) {
return value;
}
}

TheBox smart contract serves as a straightforward storage mechanism with additional ownership controls. Here are the detailed aspects:

Inheritance:

  • Inherits from Ownable in the OpenZeppelin library, ensuring that only the owner has the authority to modify the stored value.

State Variable:

  • uint256 private value: A private variable to store the integer value.

Events:

  • event ValueChanged(uint256 newValue): Emitted whenever the stored value is modified.

Functions:

  • function store(uint256 newValue) public onlyOwner: Allows the owner to update the stored value. Emits a ValueChanged event upon successful modification.
  • function retrieve() public view returns (uint256): Allows anyone to view the current stored value without modifying it.

This contract is designed for a scenario where an owner-controlled value needs to be stored and potentially retrieved by other users.

GovernanceToken.sol

//SPDX-License-Identifier: MIT

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

pragma solidity ^0.8.7;

contract GovernanceToken is ERC20Votes {
// events for the governance token
event TokenTransfered(
address indexed from,
address indexed to,
uint256 amount
);

// Events
event TokenMinted(address indexed to, uint256 amount);
event TokenBurned(address indexed from, uint256 amount);

// max tokens per user
uint256 constant TOKENS_PER_USER = 1000;
uint256 constant TOTAL_SUPPLY = 1000000 * 10**18;

// Mappings
mapping(address => bool) public s_claimedTokens;

// Number of holders
address[] public s_holders;

constructor(uint256 _keepPercentage)
ERC20("MoralisToken", "MRST")
ERC20Permit("MoralisToken")
{
uint256 keepAmount = (TOTAL_SUPPLY * _keepPercentage) / 100;
_mint(msg.sender, TOTAL_SUPPLY);
_transfer(msg.sender, address(this), TOTAL_SUPPLY - keepAmount);
s_holders.push(msg.sender);
}

// Freely Claim Tokens
function claimTokens() external {
require(!s_claimedTokens[msg.sender], "Already claimed tokens");
s_claimedTokens[msg.sender] = true;
_transfer(address(this), msg.sender, TOKENS_PER_USER * 10**18);
s_holders.push(msg.sender);
}

function getHolderLength() external view returns (uint256) {
return s_holders.length;
}

// Overrides required for Solidiy
function _afterTokenTransfer(
address from,
address to,
uint256 amount
) internal override(ERC20Votes) {
super._afterTokenTransfer(from, to, amount);
emit TokenTransfered(from, to, amount);
}

function _mint(address to, uint256 amount) internal override(ERC20Votes) {
super._mint(to, amount);
emit TokenMinted(to, amount);
}

function _burn(address account, uint256 amount)
internal
override(ERC20Votes)
{
super._burn(account, amount);
emit TokenBurned(account, amount);
}
}

// Hot Proposal Coming up => Buy a ton of tokens and dump them after voting is over
// We need to prevent that by setting a Snapshop of tokens at a certain block.

TheGovernanceToken smart contract represents a governance token with intricate functionalities. Key details include:

Inheritance:

  • Inherits from ERC20Votes, an extension of OpenZeppelin's ERC-20 token with built-in voting mechanisms.

Events:

  • event TokenTransfered(address indexed from, address indexed to, uint256 amount): Emitted upon token transfers.
  • event TokenMinted(address indexed to, uint256 amount): Emitted when new tokens are minted.
  • event TokenBurned(address indexed from, uint256 amount): Emitted when tokens are burned.

Constants:

  • uint256 constant TOKENS_PER_USER: Specifies the maximum tokens each user can claim.
  • uint256 constant TOTAL_SUPPLY: Represents the total supply of tokens.

Mappings:

  • mapping(address => bool) public s_claimedTokens: Keeps track of users who have claimed tokens.

Constructor:

  • Mints the total supply to the deployer and transfers a portion to the contract itself.

Functions:

  • function claimTokens() external: Allows users to claim tokens, enforcing the limit defined by TOKENS_PER_USER.
  • function getHolderLength() external view returns (uint256): Retrieves the number of token holders.

This contract not only manages token transfers but also incorporates governance-related features, encouraging user participation and ensuring fair distribution.

TimeLock.sol

//SPDX-License-Identifier: MIT

pragma solidity ^0.8.9;

import "@openzeppelin/contracts/governance/TimelockController.sol";

contract TimeLock is TimelockController {
// Min Delay is the minimum time that needs to pass before a proposal can be executed
// Proposers are the addresses that can create proposals
// Executors are the addresses that can execute proposals
// Admin is the address that can set the delay
constructor(
uint256 minDelay,
address[] memory proposers,
address[] memory executors,
address admin
) TimelockController(minDelay, proposers, executors, admin) {}
}

// The time lock is going to be the Owner of the Governed contract
// We use the time lock beacuase we want to wait until a new vote is executed
// Also we want to setup a minium fee to be able to vote, let's say 7 tokens.
// This Gives time to get out if they don't like the proposal.

The TimeLock smart contract extends OpenZeppelin's TimelockController, adding time-based control to proposals. Here's a breakdown:

Inheritance:

  • Inherits from TimelockController in the OpenZeppelin library.

Constructor:

  • Configures the timelock with parameters like minDelay, proposers, executors, and admin.

This contract sets up a timelock mechanism, ensuring a delay between proposal creation and execution while defining roles for proposers, executors, and an admin.

MoralisGovernor.sol

// The voting delay is the time between the proposal being created and the voting starts
// Voting Period is the time between the voting starting and the voting ending
// Proposal Treshold is the minimum amount of votes an account must have to be able to create a proposal
// Quorum is the minimum amount of votes a proposal must have to be able to be executed in percentage
// Updatable Settings allows to change the settings of the contract such as delay, period, treshold.
// Bravo Compatible allows to use the Bravo compatible functions such as getVotes and getReceipts

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

import "@openzeppelin/contracts/governance/Governor.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorSettings.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorCountingSimple.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorVotes.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorVotesQuorumFraction.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol";

contract MoralisGovernor is
Governor,
GovernorSettings,
GovernorCountingSimple,
GovernorVotes,
GovernorVotesQuorumFraction,
GovernorTimelockControl
{
// Proposal Counts
uint256 public s_proposalCount;

constructor(
IVotes _token,
TimelockController _timelock,
uint256 _votingDelay,
uint256 _votingPeriod,
uint256 _quorumPercentage
)
Governor("Governor")
GovernorSettings(
_votingDelay, /* 1 => 1 block */
_votingPeriod, /* 300 blocks => 1 hour */
0 /* 0 => Because we want anyone to be able to create a proposal */
)
GovernorVotes(_token)
GovernorVotesQuorumFraction(_quorumPercentage) /* 4 => 4% */
GovernorTimelockControl(_timelock)
{
s_proposalCount = 0;
}

// The following functions are overrides required by Solidity.

function votingDelay()
public
view
override(IGovernor, GovernorSettings)
returns (uint256)
{
return super.votingDelay();
}

function votingPeriod()
public
view
override(IGovernor, GovernorSettings)
returns (uint256)
{
return super.votingPeriod();
}

function quorum(uint256 blockNumber)
public
view
override(IGovernor, GovernorVotesQuorumFraction)
returns (uint256)
{
return super.quorum(blockNumber);
}

function state(uint256 proposalId)
public
view
override(Governor, GovernorTimelockControl)
returns (ProposalState)
{
return super.state(proposalId);
}

function propose(
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
string memory description
) public override(Governor, IGovernor) returns (uint256) {
s_proposalCount++;
return super.propose(targets, values, calldatas, description);
}

function proposalThreshold()
public
view
override(Governor, GovernorSettings)
returns (uint256)
{
return super.proposalThreshold();
}

function _execute(
uint256 proposalId,
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
bytes32 descriptionHash
) internal override(Governor, GovernorTimelockControl) {
super._execute(proposalId, targets, values, calldatas, descriptionHash);
}

function _cancel(
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
bytes32 descriptionHash
) internal override(Governor, GovernorTimelockControl) returns (uint256) {
return super._cancel(targets, values, calldatas, descriptionHash);
}

function _executor()
internal
view
override(Governor, GovernorTimelockControl)
returns (address)
{
return super._executor();
}

function supportsInterface(bytes4 interfaceId)
public
view
override(Governor, GovernorTimelockControl)
returns (bool)
{
return super.supportsInterface(interfaceId);
}

function getNumberOfProposals() public view returns (uint256) {
return s_proposalCount;
}
}

The MoralisGovernor smart contract combines various OpenZeppelin extensions to create a feature-rich governance system. Detailed features include:

Inheritance:

  • Inherits from multiple OpenZeppelin governance-related extensions: Governor, GovernorSettings, GovernorCountingSimple, GovernorVotes, GovernorVotesQuorumFraction, GovernorTimelockControl.

Constructor:

  • Initializes the governance contract with parameters like votingDelay, votingPeriod, and quorumPercentage.

Events:

  • Includes various events related to token transfers, minting, and burning.

Functions:

  • function propose(...) public override returns (uint256): Allows anyone to propose a governance action.
  • Internal functions like _execute(), _cancel(), _executor() manage proposal execution and cancellation.

Settings:

  • Parameters like votingDelay, votingPeriod, quorumPercentage are configurable through the constructor.

Additional Functionality:

  • getNumberOfProposals(): Returns the total number of proposals created.

This contract is a comprehensive governance solution, leveraging OpenZeppelin’s extensions to create a flexible and secure governance framework for decentralized decision-making.

As we wrap up this coding odyssey, we’ve laid the foundation for our DAO’s blockchain architecture. In Part 3, we’ll shift gears to explore the Python-powered smart contract deployment, revealing the secrets behind This DAO’s dynamic functionality.

--

--

Passionate software engineer crafting engaging tech articles. Simplifying complex concepts and sharing insightful knowledge for the tech community.