Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
194ee9f
Test fixes. Added a common module with constants.
MichaelKorchagin Oct 21, 2025
5f9b37a
initial scaffolding
Whytecrowe Oct 21, 2025
ba86fb6
Types fixes
MichaelKorchagin Oct 22, 2025
b81db34
add modules and more repo setup (eslint, tsconfig, etc)
Whytecrowe Oct 24, 2025
5aac8ec
small update to the Hub
Whytecrowe Oct 24, 2025
d2f941e
write function and type wrappers for viem
Whytecrowe Oct 24, 2025
8315e0e
make a smoke test that passes
Whytecrowe Oct 24, 2025
9221664
remove redundant function
Whytecrowe Oct 24, 2025
4353fb4
Source map bug fix.
MichaelKorchagin Oct 25, 2025
dee2c36
Merge remote-tracking branch 'origin/forge-base' into gov-features-ev…
MichaelKorchagin Oct 25, 2025
9094466
Mocha-ethers package removal
MichaelKorchagin Oct 25, 2025
adb52bd
Draft feature tests. WIP
MichaelKorchagin Oct 25, 2025
b1bb444
Test finalization. Added TimelockController to the build in HHConfig.
MichaelKorchagin Oct 25, 2025
2cf1411
End of evaluation. Confirmed all features that we have
MichaelKorchagin Oct 28, 2025
d53a92b
Test for Generic proposal
MichaelKorchagin Oct 30, 2025
9bc29be
Revert "Test for Generic proposal"
MichaelKorchagin Oct 30, 2025
cab87c4
Tests rollback
MichaelKorchagin Oct 30, 2025
04c444b
Voting token renaming. Transfer ban has been removed. Generic proposa…
MichaelKorchagin Oct 31, 2025
fd28815
More tests. Fixture correct to make timelock have funds
MichaelKorchagin Nov 3, 2025
af488fd
Made a signal function to track GENERIC PROPOSALS. Tested with positi…
MichaelKorchagin Nov 5, 2025
11d4ad3
Wrapped existing tests with another describe.
MichaelKorchagin Nov 7, 2025
05306a9
Tests for #genericSignal.
MichaelKorchagin Nov 7, 2025
680829c
Linter fixes
MichaelKorchagin Nov 10, 2025
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
10 changes: 10 additions & 0 deletions contracts/dao/ZDAO.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
import { GovernorPreventLateQuorum } from "@openzeppelin/contracts/governance/extensions/GovernorPreventLateQuorum.sol";


event Signal(string description);

contract ZDAO is
Governor,
GovernorSettings,
Expand Down Expand Up @@ -48,6 +50,14 @@ contract ZDAO is
GovernorVotesQuorumFraction(quorumPercentage_)
GovernorPreventLateQuorum(voteExtension_) {}

function executeSignal(string memory description)
external
onlyGovernance {
emit Signal(
description
);
}

function votingDelay()
public
view
Expand Down
202 changes: 202 additions & 0 deletions contracts/orchestration/ZeroTreasuryHub.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

import { Safe } from "@safe-global/safe-contracts/contracts/Safe.sol";
import { SafeProxyFactory } from "@safe-global/safe-contracts/contracts/proxies/SafeProxyFactory.sol";
import { SafeProxy } from "@safe-global/safe-contracts/contracts/proxies/SafeProxy.sol";


/**
* @title ZeroTreasuryHub v0.
* @dev A contract that serves as a factory for treasury deployments based on user configs.
* Also works as a registry to keep track of topologies of every treasury system deployed.
* TODO proto: how do we keep track of runtime changes of addresses and such once each treasury is modified by users ?? we shouldn't wire all treasury/dao/safe calls through this contract
*/
contract ZeroTreasuryHub {

// <--- Errors --->
error ZeroAddressPassed();
error TreasuryExistsForDomain(bytes32 domain);
error InvalidSafeParams();

// <--- Events --->
event SafeSystemSet(
address singleton,
address proxyFactory,
address fallbackHandler
);
event SafeTreasuryInstanceCreated(
bytes32 indexed domain,
address indexed safe
);

/**
* @dev All available modules to be installed for any treasury.
* Lists all predeployed preset contracts to be cloned.
*/
// TODO proto: change this to be a mapping where the key = keccak256(abi.encodePacked(namespace, ":", name, ":", versionString))
// e.g.: "OZ:Governor_V1:v1", "ZODIAC:Roles:v4", etc. think on this and make it better.
// this way we don't need to upgrade and we can easily add new modules over time.
// if doing so, we need to store all available keys in an array.
// Another way would be to store a struct with metadata on the end of the mapping instead of just plain address
// Also need to write a deterministic helper that can create and acquire these keys for apps and such. Readable names for modules could help in events.
struct ModuleCatalog {
address safe;
// address governor;
// address timelock;
}

/**
* @dev Addresses of components that make up a deployed treasury system.
*/
struct TreasuryComponents {
address safe;
// address governor;
// address timelock;
}

// TODO proto: figure these proper ones out for ZChain!
struct SafeSystem {
// Safe contract used
address singleton;
// Proxy factory used to deploy new safes
address proxyFactory;
// Fallback handler for the safe
address fallbackHandler;
}

SafeSystem public safeSystem;
ModuleCatalog public moduleCatalog;

/**
* @dev Mapping from ZNS domain hash to the addresses of components for each treasury.
*/
mapping(bytes32 => TreasuryComponents) public treasuries;

// TODO proto: should we add ZNS registry address here in state to verify domain ownership/existence on treasury creation?

// TODO proto: change this to initialize() if decided to make upgradeable
constructor(
address _safeSingleton,
address _safeProxyFactory,
address _safeFallbackHandler
) {
if (
_safeSingleton == address(0) ||
_safeProxyFactory == address(0) ||
_safeFallbackHandler == address(0)
) {
revert ZeroAddressPassed();
}

_setSafeSystem(
_safeSingleton,
_safeProxyFactory,
_safeFallbackHandler
);
}

// <--- Treasury Creation --->
// TODO proto: should these be composable contracts we can evolve over time? Also separate from registry??

function createSafe(
bytes32 domain,
address[] calldata owners,
uint256 threshold,
// TODO proto: make these better if possible. need to avoid errors and collisions. do we need it (adds complexity. including storage) ??
// this outline Safe's purpose/role in the Treasury, so we can deploy multiple Safes if needed
// Optional, only for additional Safes. pass "" for "main"
string memory purpose
) external returns (address) {
if (treasuries[domain].safe != address(0)) revert TreasuryExistsForDomain(domain);
// TODO proto: verify domain ownership!!!

// TODO proto: should we store length in a memory var? does it save gas?
if (owners.length == 0 || threshold == 0 || threshold > owners.length) revert InvalidSafeParams();

// TODO proto: figure out if we ever need to set to/data/payment stuff ?
bytes memory setup = abi.encodeWithSelector(
Safe.setup.selector,
owners,
threshold,
// to
address(0),
// data
bytes(""),
safeSystem.fallbackHandler,
// paymentToken
address(0),
// payment
0,
// paymentReceiver
payable(address(0))
);

SafeProxy safe = SafeProxyFactory(safeSystem.proxyFactory).createProxyWithNonce(
safeSystem.singleton,
setup,
_getSaltNonce(
domain,
purpose
)
);

address safeAddress = address(safe);

treasuries[domain] = TreasuryComponents({ safe: safeAddress });
// TODO proto: extend this event to inclide function parameters for Safe
emit SafeTreasuryInstanceCreated(domain, safeAddress);

return safeAddress;
}

function createDao() external {}

function createHybrid() external {}

// <--- Treasury Management --->

function addModule() external {}

function removeModule() external {}

// <--- Utilities --->
function _getSaltNonce(bytes32 domain, string memory purpose) internal pure returns (uint256) {
string memory actualPurpose = bytes(purpose).length == 0 ? "main" : purpose;

return uint256(keccak256(abi.encodePacked(domain, ":", actualPurpose)));
}

// <--- Setters --->

function setSafeSystem(
address _singleton,
address _proxyFactory,
address _fallbackHandler
) external {
// TODO proto: add access control!
_setSafeSystem(
_singleton,
_proxyFactory,
_fallbackHandler
);
}

function _setSafeSystem(
address _singleton,
address _proxyFactory,
address _fallbackHandler
) internal {
safeSystem = SafeSystem({
singleton: _singleton,
proxyFactory: _proxyFactory,
fallbackHandler: _fallbackHandler
});

emit SafeSystemSet(
_singleton,
_proxyFactory,
_fallbackHandler
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,12 @@ import { IERC5267 } from "@openzeppelin/contracts/interfaces/IERC5267.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";


interface IZeroVotingERC20 is
interface IMockZeroVotingERC20 is
IAccessControl,
IERC20,
IERC5267,
IVotes {

error NonTransferrableToken();
error ZeroAddressPassed();

function mint(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ERC20Votes } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol";
import { IZeroVotingERC20 } from "./IZeroVotingERC20.sol";
import { IMockZeroVotingERC20 } from "./IMockZeroVotingERC20.sol";


/**
Expand All @@ -19,7 +19,7 @@ import { IZeroVotingERC20 } from "./IZeroVotingERC20.sol";
* which should be assigned to the StakingERC20 contract only.
* After that it is also advisable to renounce the admin role to leave control of the token to the staking contract.
*/
contract ZeroVotingERC20 is ERC20Votes, AccessControl, IZeroVotingERC20 {
contract MockZeroVotingERC20 is ERC20Votes, AccessControl, IMockZeroVotingERC20 {

bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
Expand Down Expand Up @@ -102,9 +102,6 @@ contract ZeroVotingERC20 is ERC20Votes, AccessControl, IZeroVotingERC20 {
address to,
uint256 value
) internal override(ERC20Votes) {
if (from != address(0) && to != address(0)) {
revert NonTransferrableToken();
}

super._update(from, to, value);
}
Expand Down
16 changes: 13 additions & 3 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import type { HardhatUserConfig } from "hardhat/config";
// eslint-disable-next-line no-duplicate-imports
import { configVariable } from "hardhat/config";
import hardhatViem from "@nomicfoundation/hardhat-viem";
import hardhatToolboxViem from "@nomicfoundation/hardhat-toolbox-viem";
import hardhatMocha from "@nomicfoundation/hardhat-mocha";


const config: HardhatUserConfig = {
const config : HardhatUserConfig = {
plugins: [
hardhatToolboxViem,
hardhatViem,
hardhatMocha
hardhatMocha,
],
solidity: {
npmFilesToBuild: ["@openzeppelin/contracts/governance/TimelockController.sol"],
compilers: [
{
version: "0.8.30",
Expand All @@ -24,6 +24,16 @@ const config: HardhatUserConfig = {
},
},
],
npmFilesToBuild: [
"@safe-global/safe-contracts/contracts/SafeL2.sol",
"@safe-global/safe-contracts/contracts/proxies/SafeProxyFactory.sol",
"@safe-global/safe-contracts/contracts/libraries/MultiSend.sol",
"@safe-global/safe-contracts/contracts/libraries/MultiSendCallOnly.sol",
"@safe-global/safe-contracts/contracts/libraries/SignMessageLib.sol",
"@safe-global/safe-contracts/contracts/libraries/CreateCall.sol",
"@safe-global/safe-contracts/contracts/handler/CompatibilityFallbackHandler.sol",
"@openzeppelin/contracts/governance/TimelockController.sol",
],
},
networks: {
hardhatMainnet: {
Expand Down
20 changes: 11 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,16 @@
"scripts": {
"typechain": "hardhat typechain",
"compile": "hardhat compile",
"lint-ts": "yarn eslint ./test/** ./src/**",
"lint": "yarn lint-ts",
"build": "yarn run clean && yarn run compile",
"postbuild": "yarn save-tag",
"clean": "hardhat clean",
"test": "hardhat test mocha"
},
"devDependencies": {
"@openzeppelin/contracts": "5.4.0",
"@openzeppelin/contracts-upgradeable": "5.4.0",
"@safe-global/safe-deployments": "1.37.46",
"@safe-global/safe-contracts": "1.4.1-2",
"@gnosis-guild/zodiac-core": "3.0.1",
"@gnosis-guild/zodiac": "4.2.1",
"@gnosis-guild/zodiac-core": "3.0.1",
"@nomicfoundation/hardhat-ethers": "^4.0.2",
"@nomicfoundation/hardhat-ethers-chai-matchers": "^3.0.0",
"@nomicfoundation/hardhat-ignition": "^3.0.0",
Expand All @@ -27,19 +25,24 @@
"@nomicfoundation/hardhat-mocha": "^3.0.0",
"@nomicfoundation/hardhat-network-helpers": "^3.0.1",
"@nomicfoundation/hardhat-node-test-runner": "^3.0.3",
"@nomicfoundation/hardhat-toolbox-mocha-ethers": "^3.0.0",
"@nomicfoundation/hardhat-toolbox-viem": "^5.0.0",
"@nomicfoundation/hardhat-typechain": "^3.0.0",
"@nomicfoundation/hardhat-verify": "^3.0.3",
"@nomicfoundation/hardhat-viem": "^3.0.0",
"@nomicfoundation/hardhat-viem-assertions": "^3.0.2",
"@nomicfoundation/ignition-core": "^3.0.0",
"@safe-global/protocol-kit": "6.1.1",
"@openzeppelin/contracts": "5.4.0",
"@openzeppelin/contracts-upgradeable": "5.4.0",
"@openzeppelin/hardhat-upgrades": "^3.9.1",
"@safe-global/protocol-kit": "6.1.1",
"@safe-global/safe-contracts": "1.4.1-2",
"@safe-global/safe-deployments": "1.37.46",
"@types/chai": "^4.2.0",
"@types/chai-as-promised": "^8.0.1",
"@types/mocha": ">=10.0.10",
"@types/node": "^22.8.5",
"@wagmi/cli": "^2.7.1",
"@zero-tech/eslint-config-cpt": "^0.2.8",
"chai": "^5.1.2",
"ethers": "^6.14.0",
"forge-std": "foundry-rs/forge-std#v1.9.4",
Expand All @@ -48,8 +51,7 @@
"ts-node": "^10.9.2",
"typescript": "^5.9.3",
"viem": "^2.38.3",
"eslint": "^8.37.0",
"@zero-tech/eslint-config-cpt": "0.2.8"
"eslint": "^8.37.0"
},
"type": "module",
"packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
Expand Down
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const DEFAULT_ADMIN_ROLE = "0x0000000000000000000000000000000000000000000000000000000000000000";
Loading