2024

Aegis Protocol —
 DAO Governance

Decentralized governance protocol with Solidity smart contracts: on-chain proposals, token-weighted voting, timelock execution, and Hardhat test suite.

Aegis Protocol — DAO Governance Smart Contracts

Concept

Aegis Protocol is a DAO (Decentralized Autonomous Organization) governance system that allows token holders to propose, vote on, and execute protocol changes — entirely on-chain with no trusted intermediary.

The challenge in DAO design is preventing governance attacks: a whale acquiring tokens right before a vote, voting for a malicious proposal, then dumping the tokens. Aegis addresses this with a snapshot mechanism and a timelock executor.


Architecture

┌─────────────────────────────────────────────────────────┐
│                   Aegis Governance Stack                 │
│                                                         │
│  ┌──────────────────┐    ┌───────────────────────────┐  │
│  │   AegisToken     │    │    AegisGovernor          │  │
│  │   (ERC-20 +      │    │  (OpenZeppelin Governor)  │  │
│  │    ERC-20Votes)  │    │  - propose()              │  │
│  │                  │───▶│  - castVote()             │  │
│  │  vote weight =   │    │  - execute()              │  │
│  │  past snapshot   │    │  - quorum: 4% of supply   │  │
│  └──────────────────┘    └──────────┬────────────────┘  │
│                                     │ queues proposals   │
│                          ┌──────────▼────────────────┐  │
│                          │    TimelockController      │  │
│                          │  MinDelay: 2 days          │  │
│                          │  Executor: governor only   │  │
│                          └───────────────────────────┘  │
└─────────────────────────────────────────────────────────┘

The timelock is critical: even if a malicious proposal passes the vote, there is a 2-day delay before execution. This gives honest token holders time to exit or counter the attack.


AegisToken — Governance Token

// contracts/AegisToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

/**
 * @title AegisToken
 * @dev ERC-20 governance token with vote delegation and snapshot support.
 *
 * Voting power is based on token balance at proposal snapshot block,
 * NOT at vote time — prevents last-minute token acquisition attacks.
 */
contract AegisToken is ERC20Votes, Ownable {
    uint256 public constant MAX_SUPPLY = 1_000_000 * 10**18; // 1M tokens

    constructor(address initialOwner)
        ERC20("Aegis Protocol Token", "AEGIS")
        EIP712("Aegis Protocol Token", "1")
        Ownable(initialOwner)
    {
        // Mint initial supply to deployer for distribution
        _mint(initialOwner, MAX_SUPPLY);
    }

    /**
     * @dev Override required by ERC20Votes — tracks vote checkpoints.
     * Users must call delegate(address(self)) to activate voting power.
     */
    function _update(address from, address to, uint256 value)
        internal override(ERC20Votes)
    {
        super._update(from, to, value);
    }
}

AegisGovernor — Proposal & Voting Engine

// contracts/AegisGovernor.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

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 AegisGovernor is
    Governor,
    GovernorSettings,
    GovernorCountingSimple,
    GovernorVotes,
    GovernorVotesQuorumFraction,
    GovernorTimelockControl
{
    constructor(IVotes _token, TimelockController _timelock)
        Governor("AegisGovernor")
        GovernorSettings(
            1 days,    // Voting delay: 1 day after proposal
            1 weeks,   // Voting period: 1 week
            10_000e18  // Proposal threshold: 10,000 tokens to propose
        )
        GovernorVotes(_token)
        GovernorVotesQuorumFraction(4)  // 4% of total supply needed for quorum
        GovernorTimelockControl(_timelock)
    {}

    // Required by Solidity multiple inheritance
    function quorum(uint256 blockNumber)
        public view override(Governor, GovernorVotesQuorumFraction)
        returns (uint256)
    { return super.quorum(blockNumber); }

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

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

    function _executeOperations(
        uint256 proposalId, address[] memory targets,
        uint256[] memory values, bytes[] memory calldatas,
        bytes32 descriptionHash
    ) internal override(Governor, GovernorTimelockControl)
    { super._executeOperations(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(); }
}

Hardhat Tests

// test/AegisGovernor.test.ts
import { ethers } from "hardhat"
import { expect } from "chai"
import { mine } from "@nomicfoundation/hardhat-network-helpers"

describe("AegisGovernor", () => {
  async function deployFixture() {
    const [deployer, voter1, voter2, attacker] = await ethers.getSigners()

    const Token = await ethers.getContractFactory("AegisToken")
    const token = await Token.deploy(deployer.address)

    // Distribute tokens
    await token.transfer(voter1.address, ethers.parseEther("50000"))
    await token.transfer(voter2.address, ethers.parseEther("30000"))
    // Attacker gets tokens but forgets to delegate — attack fails
    await token.transfer(attacker.address, ethers.parseEther("100000"))

    // Voters delegate to themselves (required to activate voting power)
    await token.connect(voter1).delegate(voter1.address)
    await token.connect(voter2).delegate(voter2.address)
    // Attacker doesn't delegate — their tokens have zero voting power

    const Timelock = await ethers.getContractFactory("TimelockController")
    const timelock = await Timelock.deploy(
      2 * 24 * 3600,  // 2-day min delay
      [], [], deployer.address
    )

    const Governor = await ethers.getContractFactory("AegisGovernor")
    const governor = await Governor.deploy(token.target, timelock.target)

    return { governor, token, timelock, deployer, voter1, voter2, attacker }
  }

  it("undelegated tokens have no voting power", async () => {
    const { token, attacker } = await deployFixture()
    const votes = await token.getVotes(attacker.address)
    expect(votes).to.equal(0n)
  })

  it("proposal reaches quorum and passes", async () => {
    const { governor, token, voter1, voter2 } = await deployFixture()

    // Create a proposal (encode calldata for an arbitrary action)
    const calldata = token.interface.encodeFunctionData("transfer", [
      voter1.address,
      ethers.parseEther("1")
    ])

    const tx = await governor.connect(voter1).propose(
      [token.target], [0n], [calldata],
      "Transfer tokens: Proposal #1"
    )
    const receipt = await tx.wait()
    const proposalId = receipt!.logs[0].topics[1]

    // Mine past voting delay
    await mine(1 * 24 * 3600 / 12 + 1)

    // Vote
    await governor.connect(voter1).castVote(proposalId, 1)  // For
    await governor.connect(voter2).castVote(proposalId, 1)  // For

    // Mine past voting period
    await mine(7 * 24 * 3600 / 12)

    const state = await governor.state(proposalId)
    expect(state).to.equal(4n)  // ProposalState.Succeeded
  })
})

Security Considerations

The three main governance attack vectors and how Aegis mitigates them:

AttackMitigation
Flash loan votingSnapshot voting power at proposal creation block — borrowed tokens can’t vote
Governance takeover4% quorum + 1-week voting period makes accumulating enough votes expensive and public
Malicious proposal execution2-day timelock gives community time to react and exit
Proposal spam10,000 token threshold to propose prevents noise
Explore more projects