Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions contracts/dao/TimelockUpgradeableMaster.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {TimelockControllerUpgradeable} from "@openzeppelin/contracts-upgradeable/governance/TimelockControllerUpgradeable.sol";

/**
* @title TimelockUpgradeableMaster
* @notice Thin wrapper to have a local artifact for OZ TimelockControllerUpgradeable so it can be used
* as a master copy for EIP-1167 clones in tests.
*/
contract TimelockUpgradeableMaster is Initializable, TimelockControllerUpgradeable {
function initializeTimelock(
uint256 minDelay,
address[] memory proposers,
address[] memory executors
) public initializer {
__TimelockController_init(minDelay, proposers, executors, msg.sender);
}
}
173 changes: 173 additions & 0 deletions contracts/dao/ZDAOUpgradeable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {GovernorUpgradeable} from "@openzeppelin/contracts-upgradeable/governance/GovernorUpgradeable.sol";
import {GovernorSettingsUpgradeable} from "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorSettingsUpgradeable.sol";
import {GovernorCountingSimpleUpgradeable} from "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorCountingSimpleUpgradeable.sol";
import {GovernorVotesUpgradeable} from "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorVotesUpgradeable.sol";
import {IVotes} from "@openzeppelin/contracts/governance/utils/IVotes.sol";
import {GovernorTimelockControlUpgradeable, TimelockControllerUpgradeable} from "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorTimelockControlUpgradeable.sol";
import {GovernorVotesQuorumFractionUpgradeable} from "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorVotesQuorumFractionUpgradeable.sol";
import {GovernorPreventLateQuorumUpgradeable} from "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorPreventLateQuorumUpgradeable.sol";

/**
* @title ZDAOUpgradeable
* @notice Upgradeable/cloneable variant of ZDAO suitable for EIP-1167 minimal proxy cloning.
* Uses OZ *Upgradeable* governor modules and an initialize() function instead of constructor.
*/
contract ZDAOUpgradeable is
Initializable,
GovernorUpgradeable,
GovernorSettingsUpgradeable,
GovernorCountingSimpleUpgradeable,
GovernorVotesUpgradeable,
GovernorTimelockControlUpgradeable,
GovernorVotesQuorumFractionUpgradeable,
GovernorPreventLateQuorumUpgradeable
{
uint256 public daoId; // simple extra field to mirror original ZDAO constructor arg

function initialize(
uint256 _daoId,
string memory governorName,
IVotes token,
TimelockControllerUpgradeable timelock,
uint48 delay_,
uint32 votingPeriod_,
uint256 proposalThreshold_,
uint256 quorumPercentage_,
uint48 voteExtension_
) public initializer {
daoId = _daoId;

__Governor_init(governorName);
__GovernorVotes_init(token);
__GovernorTimelockControl_init(timelock);
__GovernorSettings_init(delay_, votingPeriod_, proposalThreshold_);
__GovernorVotesQuorumFraction_init(quorumPercentage_);
__GovernorPreventLateQuorum_init(voteExtension_);
}

// --- Overrides required by Solidity ---

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

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

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

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

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

function proposalNeedsQueuing(uint256 proposalId)
public
view
override(GovernorUpgradeable, GovernorTimelockControlUpgradeable)
returns (bool)
{
return super.proposalNeedsQueuing(proposalId);
}

function proposalDeadline(uint256 proposalId)
public
view
override(GovernorUpgradeable, GovernorPreventLateQuorumUpgradeable)
returns (uint256)
{
return super.proposalDeadline(proposalId);
}

// Resolve multiple inheritance requirements (OZ v5.x)
function _queueOperations(
uint256 proposalId,
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
bytes32 descriptionHash
) internal virtual override(GovernorUpgradeable, GovernorTimelockControlUpgradeable) 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 virtual override(GovernorUpgradeable, GovernorTimelockControlUpgradeable) {
super._executeOperations(proposalId, targets, values, calldatas, descriptionHash);
}

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

function _executor()
internal
view
virtual
override(GovernorUpgradeable, GovernorTimelockControlUpgradeable)
returns (address)
{
return super._executor();
}

function _tallyUpdated(uint256 proposalId)
internal
virtual
override(GovernorUpgradeable, GovernorPreventLateQuorumUpgradeable)
{
super._tallyUpdated(proposalId);
}

function supportsInterface(bytes4 interfaceId)
public
view
override(GovernorUpgradeable)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}
30 changes: 30 additions & 0 deletions contracts/mocks/TestERC20Votes.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

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 { Nonces } from "@openzeppelin/contracts/utils/Nonces.sol";

/// @notice Simple non-upgradeable ERC20Votes with initial mint for testing governors.
contract TestERC20Votes is ERC20, ERC20Permit, ERC20Votes {
constructor(string memory name_, string memory symbol_, uint256 initialSupply) ERC20(name_, symbol_) ERC20Permit(name_) {
_mint(msg.sender, initialSupply);
}

// Required overrides
function _update(address from, address to, uint256 value)
internal override(ERC20, ERC20Votes)
{
super._update(from, to, value);
}

function nonces(address owner)
public
view
override(ERC20Permit, Nonces)
returns (uint256)
{
return super.nonces(owner);
}
}
97 changes: 97 additions & 0 deletions contracts/orchestration/ZeroTreasuryDaoFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

import { Clones } from "@openzeppelin/contracts/proxy/Clones.sol";
import { Address } from "@openzeppelin/contracts/utils/Address.sol";
import { ZeroTreasuryRegistry } from "./ZeroTreasuryRegistry.sol";


/**
* @title ZeroTreasuryDaoFactory
* @notice Deploys DAO modules (Governor, Timelock, etc.) as minimal proxy clones from
* pre-deployed master copies stored in a separate Registry, and records the
* resulting deployment addresses per domain in that Registry.
*
* Design goals:
* - Pull master copies by numeric module IDs from the Registry
* - Deploy EIP-1167 minimal proxy clones deterministically per domain
* - Initialize each clone with user-provided calldata (constructor-equivalent)
* - Optionally deploy Governor with or without Timelock in one call
* - Record each deployed module address back into the Registry under the same domain
*/
contract ZeroTreasuryDaoFactory {
using Address for address;

// --- Errors ---
error ZeroAddress();
error LengthMismatch();
error MasterCopyNotSet(uint256 moduleId);

// --- Events ---
event ModuleCloned(bytes32 indexed domain, uint256 indexed moduleId, address indexed clone, bytes32 salt);

// --- Storage ---
ZeroTreasuryRegistry public immutable registry;

constructor(address registryAddress) {
if (registryAddress == address(0)) revert ZeroAddress();
registry = ZeroTreasuryRegistry(registryAddress);
}

// --- Core helpers ---

function getMasterCopy(uint256 moduleId) public view returns (address impl) {
impl = registry.moduleCatalog(moduleId);
if (impl == address(0)) revert MasterCopyNotSet(moduleId);
}

function _salt(bytes32 domain, uint256 moduleId, uint256 instanceId) internal pure returns (bytes32) {
return keccak256(abi.encodePacked(domain, ":", moduleId, ":", bytes32(instanceId)));
}

function predictCloneAddress(
uint256 moduleId,
bytes32 domain,
uint256 instanceId
) external view returns (address predicted) {
address impl = getMasterCopy(moduleId);
bytes32 salt_ = _salt(domain, moduleId, instanceId);
return Clones.predictDeterministicAddress(impl, salt_, address(this));
}

/// Clone, initialize and record single module for a domain
function deployModule(
bytes32 domain,
uint256 moduleId,
bytes calldata initCalldata,
uint256 instanceId
) public returns (address clone) {
address impl = getMasterCopy(moduleId);
bytes32 salt_ = _salt(domain, moduleId, instanceId);
clone = Clones.cloneDeterministic(impl, salt_);

if (initCalldata.length > 0) {
Address.functionCall(clone, initCalldata);
}

// Record in Registry under specific instanceId (non-canonical by default)
registry.recordDomainModuleInstance(domain, moduleId, instanceId, clone, "");

emit ModuleCloned(domain, moduleId, clone, salt_);
}

/// Deploy multiple modules in one call (e.g., Governor + Timelock)
function deployModules(
bytes32 domain,
uint256[] calldata moduleIds,
bytes[] calldata initCalldatas,
uint256[] calldata instanceIds
) external returns (address[] memory clones) {
if (moduleIds.length != initCalldatas.length) revert LengthMismatch();
if (moduleIds.length != instanceIds.length) revert LengthMismatch();
clones = new address[](moduleIds.length);
for (uint256 i = 0; i < moduleIds.length; i++) {
clones[i] = deployModule(domain, moduleIds[i], initCalldatas[i], instanceIds[i]);
}
}
}
Loading