A DAO (Decentralized Autonomous Organization) is a system that enables collective decision-making through code, without relying on traditional organizational hierarchies such as boards of directors, CEOs, or CTOs. Instead of trust in individuals or institutions, DAOs rely on smart contracts deployed on a blockchain.
At its core, a DAO allows participants to propose, vote, and execute decisions in a transparent and verifiable way. Voting power is typically derived from tokens held by participants, where each token represents a unit of voting weight.
A typical on-chain DAO is composed of three main smart contracts:
- Token contract: Defines the governance token and tracks voting power.
- Governor contract: Manages proposals and voting logic: who can propose, how votes are counted, quorum requirements, and proposal outcomes.
- Timelock contract: Acts as a security layer by enforcing a delay between proposal approval and execution, giving participants time to react to potentially harmful decisions.
The lifecycle of a proposal is simple but powerful: a proposal is submitted to the Governor, votes are collected based on token ownership, and once the proposal is approved, it is forwarded to the Timelock for delayed execution. If the proposal fails, it is simply discarded.
In this article series, we will build a DAO from the ground up using GovernanceToken. This token will later be used to enable on-chain voting and decision-making in the DAO.
The Token Code
Without further ado, here is the code:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import {ERC20Votes} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Nonces} from "@openzeppelin/contracts/utils/Nonces.sol";
contract GovernanceToken is ERC20, ERC20Permit, ERC20Votes, Ownable {
constructor()
ERC20("GovernanceToken", "MGT")
ERC20Permit("GovernanceToken")
Ownable(msg.sender)
{
_mint(msg.sender, 1_000_000 * 10 ** decimals());
}
// Optional: Add controlled minting
function mint(address to, uint256 amount) external {
require(msg.sender == owner(), "Only owner can mint");
_mint(to, amount);
}
// ── Conflict resolution ──
// Both ERC20 and ERC20Votes define _update
function _update(address from, address to, uint256 amount)
internal
override(ERC20, ERC20Votes)
{
super._update(from, to, amount);
}
// Both ERC20Permit and Nonces define nonces()
function nonces(address owner)
public
view
override(ERC20Permit, Nonces)
returns (uint256)
{
return super.nonces(owner);
}
}
Compared to a traditional ERC20 token, GovernanceToken integrates two additional OpenZeppelin modules: ERC20Permit and ERC20Votes.
- ERC20Votes adds governance-specific functionality, most notably
getPastVotes(account, blockNumber). This function returns an account’s voting power at a specific block, rather than its current balance. In a DAO context, this snapshot mechanism is critical: voting power is fixed at the moment a proposal is created, preventing users from manipulating votes by buying or transferring tokens after the fact.
- ERC20Permit enables gasless approvals via signatures (EIP-2612), allowing users to delegate or approve voting power without sending an on-chain transaction.
The most important logic resides in the constructor, which initializes all inherited modules and mints one million governance tokens to the deployer. We also define an optional mint function, restricted to the contract owner, to allow controlled token issuance after deployment (useful for testing or future governance decisions).
Finally, two functions — _update and nonces—must be explicitly overridden. This is required because they are defined in multiple parent contracts. The overrides simply delegate execution to super, ensuring that all inherited behaviors are correctly composed and that the compiler’s inheritance conflicts are resolved cleanly.
Building the Token
To build our governance token, we will use Foundry, a fast and modern Ethereum development toolkit. The following steps assume a Linux environment, but the workflow is similar on macOS.
We start by installing Foundry using the official installation script:
curl -L https://foundry.paradigm.xyz | bash
After installation, the script instructs us to update our shell environment and install the Foundry binaries:
source ~/.bashrc # path may vary depending on your system
foundryup
This installs the full Foundry toolchain: forge (build & test), cast (CLI interactions), anvil (local node), and chisel (REPL).
Next, we initialize a new Foundry project in an empty directory:
mkdir DAO
cd DAO
forge init
This generates a complete project scaffold, including src/, script/, and test/ directories. By default, Foundry creates example Counter contracts and tests. Since we only want the project structure, we can safely remove these example files and replace them with our own contracts.
For now, we add our governance token under src/:
src/
└── GovernanceToken.sol
(Containing the GovernanceToken contract defined in the previous section.)
Because our token relies on OpenZeppelin modules, we must install the OpenZeppelin Contracts library:
forge install OpenZeppelin/openzeppelin-contracts
This command vendors OpenZeppelin into the lib/ directory and makes its contracts available for import within our project.
Finally, we compile the project:
forge build
If everything is set up correctly, the compilation completes successfully and generates an out/ directory. This folder contains the compiled artifacts (ABIs and bytecode) for GovernanceToken as well as all inherited OpenZeppelin dependencies.
At this point, our governance token is fully compiled and ready to be deployed and tested — steps we will cover in the next sections.
Deploying the Token
With the governance token compiled, we can now deploy it to a local blockchain. Foundry makes this process straightforward through deployment scripts.
We start by creating a deployment script DeployGovernanceToken.s.sol under the script/ directory:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Script} from "forge-std/Script.sol";
import {GovernanceToken} from "../src/GovernanceToken.sol";
contract DeployGovernanceToken is Script {
function run() external {
vm.startBroadcast();
new GovernanceToken();
vm.stopBroadcast();
}
}
This script defines a run function that Foundry will execute. The vm.startBroadcast() / vm.stopBroadcast() pair tells Foundry to send transactions to the network, rather than simulating them.
Next, we launch a local Ethereum network using Anvil (in a separate terminal):
anvil
Anvil starts a local node on http://127.0.0.1:8545 and prints a list of pre-funded accounts along with their private keys. These accounts are intended for development and testing only.
With Anvil running, we can deploy the contract using forge script:
forge script script/DeployGovernanceToken.s.sol \
--rpc-url http://127.0.0.1:8545 \
--broadcast \
--private-key <ANVIL_PRIVATE_KEY>
The RPC URL and private key are taken directly from Anvil’s output. When the command succeeds, Foundry prints the transaction hash, deployed contract address, gas usage, and the block number in which the contract was created.
To quickly verify that the deployment worked, we can query the deployed contract using cast. For example, calling totalSupply() confirms that the initial mint occurred as expected:
cast call <DEPLOYED_CONTRACT_ADDRESS> \
"totalSupply()(uint256)" \
--rpc-url http://127.0.0.1:8545
The returned value corresponds to 1,000,000 tokens with 18 decimals (1000000000000000000000000 [1e24]), matching the amount minted in the constructor.
At this stage, our governance token is live on a local network and ready to be used for testing voting, delegation, and — eventually — DAO governance.
Testing the Token
To validate our governance token’s behavior, we can write unit tests using forge-std, Foundry’s testing framework. Tests live in the test/ directory and are written in Solidity.
Below is a simple test that verifies the mint function works as expected:
// test/GovernanceToken.t.sol
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {GovernanceToken} from "../src/GovernanceToken.sol";
contract TokenTest is Test {
GovernanceToken token;
function setUp() public {
token = new GovernanceToken();
}
function testMint() public {
uint256 before = token.balanceOf(address(this));
token.mint(address(this), 100);
uint256 after_ = token.balanceOf(address(this));
assertEq(after_ - before, 100);
}
}
The setUp function is executed before each test and deploys a fresh instance of GovernanceToken, ensuring isolation between test cases. The testMint function then checks that calling mint increases the recipient’s balance by the expected amount.
Running the test suite is as simple as:
forge test
Foundry compiles the contracts, executes the test, and reports the results. A passing test confirms that our token’s minting logic behaves correctly.
Conclusion
In this article, we tackled the first building block of a DAO: the governance token. We began by examining the token contract itself, with particular attention to the OpenZeppelin modules it inherits from and the additional governance-related features they provide.
We then walked through the full development workflow using Foundry — from initializing a project, to deploying the token on a local Anvil network, and finally validating its behavior with unit tests.
This governance token will serve as the foundation for everything that follows. In the next parts of this series, we will build on top of it by introducing delegation, voting mechanics, and the core governance contracts that transform this token into a fully functional on-chain DAO.
I hope you found this article useful. Feel free to like, share, and subscribe for more content in the series.
Miscellaneous: Extra Commands
All commands shown in this article were executed inside a Docker container created with the following command:
docker run -it ubuntu:ubuntu@sha256:72297848456d5d37d1262630108ab308d3e9ec7ed1c3286a32fe09856619a782
Using a pinned image digest ensures full reproducibility, as the environment will always be identical regardless of when or where the container is launched.
To run Anvil in a separate terminal, we simply attached to the same container:
docker exec -it <CONTAINER_NAME> bash
anvil
The variable <CONTAINER_NAME> can be found through the command:
$ docker ps
Foundry also allows you to run deployment scripts without a live network. The following command executes the script in a simulated environment and reports gas usage, without broadcasting any transactions:
forge script script/DeployGovernanceToken.s.sol --broadcast
This mode is useful for quickly validating deployment logic and estimating gas costs. If you want to simulate or execute transactions against an actual network (local or remote), simply provide an RPC URL using the --rpc-url flag.
Miscellaneous: Warnings
During development, you may encounter warnings related to dependencies rather than your own contracts. In our case, the compiler emitted warnings originating from the lib/forge-std library:
Warning (2424): Natspec memory-safe-assembly special comment for inline assembly is deprecated
and scheduled for removal. Use the memory-safe block annotation instead.
--> lib/forge-std/src/StdStorage.sol:301:13
These warnings are caused by a version mismatch between the Solidity compiler and the installed version of forge-std. Newer Solidity versions deprecate the @memory-safe-assembly NatSpec comment in favor of the memory-safe block annotation, while older library versions may still use the deprecated syntax.
Since the issue originates in a dependency, the simplest fix is to update forge-std to the latest version:
cd lib/forge-std
git pull origin master
git checkout master
cd -
After updating the library, the warnings disappear and the project compiles cleanly again.
This is a good reminder that compiler warnings are not always caused by your own code. When working with fast-evolving toolchains like Foundry and Solidity, keeping dependencies up to date is often necessary to avoid noisy or misleading warnings.