This project implements a complete on-chain governance system using Foundry. It features an ERC20 governance token, a timelock for execution delay, and a governor contract to manage proposals. The system is designed to control a simple Box contract, demonstrating how the DAO can manage ownership and call functions on other smart contracts.
The repository includes a full suite of deployment scripts, interaction scripts, and comprehensive tests, including unit, fuzz, and invariant testing.
You'll know you did it right if you can run git --version and you see a response like git version x.x.x
You'll know you did it right if you can run forge --version and you see a response like forge 0.2.0 (816e00b 2023-03-16T00:05:26.396218Z)
Clone the repository and install the dependencies:
git clone https://github.com/fricpto/foundry-DAO
cd foundry-DAO
forge install
forge buildYou'll want to set your SEPOLIA_RPC_URL and PRIVATE_KEY as environment variables. You can add them to a .env file.
PRIVATE_KEY: The private key of your wallet.NOTE: FOR DEVELOPMENT, PLEASE USE A KEY THAT DOESN'T HAVE ANY REAL FUNDS ASSOCIATED WITH IT.
SEPOLIA_RPC_URL: Your RPC URL for the Sepolia testnet, which you can get for free from a node provider like Alchemy.ETHERSCAN_API_KEY(Optional): Add your Etherscan API key if you want to verify your contract.
Head over to faucets.chain.link to get some testnet ETH and LINK for the Sepolia network.
Core Contracts (src/)
GovToken.sol: An ERC20 token that includes ERC20Votes and ERC20Permit extensions from OpenZeppelin. This token is used for voting power, where 1 token equals 1 vote.
TimeLock.sol: A TimelockController contract that enforces a delay on all actions executed by the DAO. All successful proposals must pass through this timelock, adding a layer of security and allowing users to exit their positions if they disagree with a proposal.
MyGovernor.sol: The core governance module. It inherits from OpenZeppelin's Governor contracts to handle proposal creation, voting, and execution logic. It's configured with a voting delay, voting period, and a 4% quorum requirement.
Box.sol: A simple example contract owned by the TimeLock. The DAO can create proposals to call functions on this contract, such as store(uint256).
This guide walks through the entire lifecycle of a proposal on a local Anvil node.
Open a terminal window and start a local node. Keep this running.
anvilAnvil provides a list of private keys upon startup. Choose one and set it as an environment variable in a .env file.
PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
Run the deployment script. This will set up all the necessary contracts and roles.
forge script script/Deploy.s.sol --rpc-url <your_rpc_url> --private-key <your_private_key> --broadcastAction Required: Copy the deployed addresses for MyGovernor and Box from the output. Paste them into the constant address variables in Propose.s.sol, Vote.s.sol, Queue.s.sol, and Execute.s.sol.
Run the Propose script to create a new proposal to change the value in the Box contract.
forge script script/Propose.s.sol --rpc-url <your_rpc_url> --private-key <your_private_key> --broadcastAction Required: Copy the proposalId from the output logs. Paste it into the PROPOSAL_ID constant in Vote.s.sol, Queue.s.sol, and Execute.s.sol.
The governor has a 1-block voting delay. Mine a block, then run the vote script.
cast rpc anvil_increaseTime 2cast rpc anvil_mineforge script script/Vote.s.sol --rpc-url <your_rpc_url> --private-key <your_private_key> --broadcastThe voting period is set to 10 blocks. Mine 10 blocks to end the voting period.
cast rpc anvil_mine "0xa"At this point, the proposal state should be Succeeded.
Run the Queue script. It will verify the proposal has Succeeded and submit it to the TimeLock.
forge script script/Queue.s.sol --rpc-url <your_rpc_url> --private-key <your_private_key> --broadcastThe proposal is now Queued and waiting for the 1-second timelock delay to pass.
Advance time past the timelock delay, then run the Execute script to enact the proposal.
cast rpc anvil_increaseTime 2cast rpc anvil_mineforge script script/Execute.s.sol --rpc-url <your_rpc_url> --private-key <your_private_key> --broadcastCongratulations! You should see the log message New Box value: 77. You have successfully completed a full governance cycle.
Deploy.s.sol: Deploys all contracts (GovToken, TimeLock, MyGovernor, Box) and sets up initial roles.
Propose.s.sol: Creates a new proposal to call store(77) on the Box contract.
Vote.s.sol: Casts a "For" vote on the created proposal.
Queue.s.sol: Queues a succeeded proposal in the timelock, checking first that its state is Succeeded.
Execute.s.sol: Executes a queued proposal after the timelock delay has passed, checking first that its state is Queued.
✅ Testing The project includes a comprehensive test suite. Run all tests with:
forge testBoxFuzzTest.t.sol: Contains unit and fuzz tests for the Box contract, ensuring its core logic and ownership rules work as expected.
MyGovernorTest.t.sol: An integration test suite for the entire governance lifecycle, covering proposal creation, voting, queuing, execution, failures, and cancellations.
Invariant Tests: These tests check that core properties of the system are never violated. They are split into two strategies based on the fail_on_revert setting in foundry.toml.
StopOnRevertInvariant/BoxInvariantTest.t.sol: To be run with fail_on_revert = false. This test allows reverts and uses vm.expectRevert to ensure that calls from non-owners fail as expected, while calls from the owner succeed.
StrictNoRevert/BoxInvariant.t.sol: To be run with fail_on_revert = true or false.