ichaingov-contract-generator 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,112 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.20;
3
+
4
+ import "@openzeppelin/contracts/access/Ownable.sol";
5
+
6
+ contract PolicyRegistry is Ownable {
7
+
8
+ struct Parameter {
9
+ string key;
10
+ uint256 value;
11
+ uint256 createdAt;
12
+ uint256 updatedAt;
13
+ bool exists;
14
+ }
15
+
16
+ mapping(string => Parameter) public parameters;
17
+ string[] public parameterKeys;
18
+
19
+ event ParameterAdded(string key, uint256 value, uint256 timestamp);
20
+ event ParameterUpdated(string key, uint256 oldValue, uint256 newValue, uint256 timestamp);
21
+ event ParameterRemoved(string key, uint256 timestamp);
22
+
23
+ error ParameterAlreadyExists(string key);
24
+ error ParameterNotFound(string key);
25
+ error EmptyKey();
26
+
27
+ constructor(address initialOwner) Ownable(initialOwner) {}
28
+
29
+ function addParameter(
30
+ string calldata key,
31
+ uint256 value
32
+ ) external onlyOwner {
33
+ if (bytes(key).length == 0) revert EmptyKey();
34
+ if (parameters[key].exists) revert ParameterAlreadyExists(key);
35
+
36
+ parameters[key] = Parameter({
37
+ key: key,
38
+ value: value,
39
+ createdAt: block.timestamp,
40
+ updatedAt: block.timestamp,
41
+ exists: true
42
+ });
43
+
44
+ parameterKeys.push(key);
45
+ emit ParameterAdded(key, value, block.timestamp);
46
+ }
47
+
48
+ function setParameter(
49
+ string calldata key,
50
+ uint256 newValue
51
+ ) external onlyOwner {
52
+ if (!parameters[key].exists) revert ParameterNotFound(key);
53
+
54
+ uint256 oldValue = parameters[key].value;
55
+ parameters[key].value = newValue;
56
+ parameters[key].updatedAt = block.timestamp;
57
+
58
+ emit ParameterUpdated(key, oldValue, newValue, block.timestamp);
59
+ }
60
+
61
+ function removeParameter(string calldata key) external onlyOwner {
62
+ if (!parameters[key].exists) revert ParameterNotFound(key);
63
+
64
+ for (uint256 i = 0; i < parameterKeys.length; i++) {
65
+ if (keccak256(bytes(parameterKeys[i])) == keccak256(bytes(key))) {
66
+ parameterKeys[i] = parameterKeys[parameterKeys.length - 1];
67
+ parameterKeys.pop();
68
+ break;
69
+ }
70
+ }
71
+
72
+ delete parameters[key];
73
+ emit ParameterRemoved(key, block.timestamp);
74
+ }
75
+
76
+ // functions to be called from outside of this contract (testing)
77
+
78
+ function getParameter(string calldata key)
79
+ external view
80
+ returns (Parameter memory)
81
+ {
82
+ if (!parameters[key].exists) revert ParameterNotFound(key);
83
+ return parameters[key];
84
+ }
85
+
86
+ function getValue(string calldata key)
87
+ external view
88
+ returns (uint256)
89
+ {
90
+ if (!parameters[key].exists) revert ParameterNotFound(key);
91
+ return parameters[key].value;
92
+ }
93
+
94
+ function getAllParameters()
95
+ external view
96
+ returns (Parameter[] memory)
97
+ {
98
+ Parameter[] memory result = new Parameter[](parameterKeys.length);
99
+ for (uint256 i = 0; i < parameterKeys.length; i++) {
100
+ result[i] = parameters[parameterKeys[i]];
101
+ }
102
+ return result;
103
+ }
104
+
105
+ function getParameterCount() external view returns (uint256) {
106
+ return parameterKeys.length;
107
+ }
108
+
109
+ function parameterExists(string calldata key) external view returns (bool) {
110
+ return parameters[key].exists;
111
+ }
112
+ }
@@ -0,0 +1,19 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.20;
3
+
4
+ import "@openzeppelin/contracts/governance/TimelockController.sol";
5
+
6
+ contract Timelock is TimelockController {
7
+ // minDelay: delay in seconds before execution
8
+ // proposers: addresses allowed to queue proposals
9
+ // executors: addresses allowed to execute proposals (empty = anyone)
10
+ // admin: initial admin
11
+ constructor(
12
+ uint256 minDelay,
13
+ address[] memory proposers,
14
+ address[] memory executors,
15
+ address admin
16
+ )
17
+ TimelockController(minDelay, proposers, executors, admin)
18
+ {}
19
+ }
@@ -0,0 +1,49 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.20;
3
+
4
+ import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
5
+ import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
6
+ import "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
7
+
8
+ contract Token is ERC20, EIP712, ERC20Votes {
9
+
10
+ address public governance;
11
+
12
+ constructor(
13
+ string memory name,
14
+ string memory symbol,
15
+ uint256 supply,
16
+ address dao
17
+ ) ERC20(name, symbol) EIP712(name, "1") {
18
+ governance = dao;
19
+ _mint(dao, supply * 10 ** decimals());
20
+
21
+ }
22
+
23
+ modifier onlyGovernance() {
24
+ require(msg.sender == governance, "Token: not governance");
25
+ _;
26
+ }
27
+
28
+ function reclaimTokensToAddress(address account, address recipient) external onlyGovernance {
29
+ uint256 amount = balanceOf(account);
30
+ if (amount == 0) return;
31
+ _transfer(account, recipient, amount);
32
+ }
33
+
34
+ function burnAllTokensFromAccount(address account) external onlyGovernance {
35
+ _burn(account, balanceOf(account));
36
+ }
37
+
38
+ function transferOwnership(address newGovernance) external onlyGovernance {
39
+ require(newGovernance != address(0), "Token: zero address");
40
+ governance = newGovernance;
41
+ }
42
+
43
+ function _update(address from, address to, uint256 amount)
44
+ internal
45
+ override(ERC20, ERC20Votes)
46
+ {
47
+ super._update(from, to, amount);
48
+ }
49
+ }
@@ -0,0 +1,59 @@
1
+ import fs from "fs-extra";
2
+ import path from "path";
3
+ import Handlebars from "handlebars";
4
+ import { encodeValue } from "../core/encodeParameter.js";
5
+ import { mapVotingSystemIndex } from "./mappers.js";
6
+
7
+ export async function generateDeploy(config) {
8
+ const templatePath = path.join(
9
+ path.dirname(new URL(import.meta.url).pathname),
10
+ "../index/templates/deploy.js.hbs"
11
+ );
12
+
13
+ if (!await fs.pathExists(templatePath)) {
14
+ throw new Error(`Deploy template not found: ${templatePath}`);
15
+ }
16
+ const protocolParameters = (config.protocolParameters || []).map(p => ({
17
+ key: p.key,
18
+ value: encodeValue(p.value).toString(),
19
+ originalValue: p.value
20
+ }));
21
+
22
+ let index = mapVotingSystemIndex(config.governance.votingSystem);
23
+
24
+ const data = {
25
+ daoName: config.name,
26
+ tokenName: config.tokenomics.name,
27
+ tokenSymbol: config.tokenomics.symbol,
28
+ totalSupply: config.tokenomics.supply,
29
+ tokensPerMember: config.tokenomics.defaultMemberTokens,
30
+ organizations: config.organizations.map(org => ({
31
+ wallet: org.address,
32
+ name: org.name,
33
+ reputation: org.reputation || 0,
34
+ gateways: (org.gateways || []).map(gw => ({
35
+ publicKey: typeof gw === "string" ? gw : gw.publicKey,
36
+ name: typeof gw === "string" ? "" : gw.name
37
+ }))
38
+ })),
39
+ isTimelock: config.timelock.enabled,
40
+ timelockDelay: config.timelock.minDelay,
41
+ contractName: config.timelock.enabled ? "GovernanceWithTimelock" : "Governance",
42
+ votingDelay: config.governance.votingDelay,
43
+ votingPeriod: config.governance.votingPeriod,
44
+ proposalThreshold: config.governance.proposalThreshold,
45
+ quorumFraction: config.governance.quorumFraction,
46
+ votingSystemIndex: index,
47
+ protocolParameters: protocolParameters,
48
+ isWeightedReputation: index === 2,
49
+ };
50
+
51
+ const raw = await fs.readFile(templatePath, "utf-8");
52
+ const template = Handlebars.compile(raw);
53
+ const output = template(data);
54
+
55
+ await fs.ensureDir("scripts");
56
+ const outPath = "scripts/deploy.js";
57
+ await fs.writeFile(outPath, output);
58
+ console.log(`Generated ${outPath}`);
59
+ }
@@ -0,0 +1,37 @@
1
+ import fs from "fs-extra";
2
+ import path from "path";
3
+
4
+ function packageRoot() {
5
+ return path.dirname(new URL(import.meta.url).pathname);
6
+ }
7
+
8
+ function baseContractsDir() {
9
+ return path.join(packageRoot(), "../index/base-contracts");
10
+ }
11
+
12
+ export async function generate(config) {
13
+ const isTimelock = config.timelock.enabled;
14
+ const sourceFile = isTimelock ? "GovernanceWithTimelock.sol" : "Governance.sol";
15
+
16
+ await fs.ensureDir("contracts");
17
+
18
+ const baseSrc = baseContractsDir();
19
+ const baseDest = "contracts";
20
+
21
+ if (!await fs.pathExists(baseSrc)) {
22
+ throw new Error(`Base contracts directory not found: ${baseSrc}`);
23
+ }
24
+
25
+ const files = await fs.readdir(baseSrc);
26
+ for (const file of files) {
27
+ if (!file.endsWith(".sol")) continue;
28
+ if (file === "Timelock.sol" && !isTimelock) {
29
+ console.log(`Skipped ${file} (timelock not enabled)`);
30
+ continue;
31
+ }
32
+ if (file === "GovernanceWithTimelock.sol" && !isTimelock) continue;
33
+ if (file === "Governance.sol" && isTimelock) continue;
34
+
35
+ await fs.copy(path.join(baseSrc, file), path.join(baseDest, file), { overwrite: true });
36
+ }
37
+ }
@@ -0,0 +1,19 @@
1
+ export function mapVotingSystem(vs) {
2
+ switch (vs) {
3
+ case "token-based": return "VotingSystem.TokenBased";
4
+ case "quadratic": return "VotingSystem.Quadratic";
5
+ case "weighted-reputation": return "VotingSystem.WeightedReputation";
6
+ default:
7
+ throw new Error(`Unknown votingSystem: "${vs}"`);
8
+ }
9
+ }
10
+
11
+ export function mapVotingSystemIndex(vs) {
12
+ switch (vs) {
13
+ case "token-based": return 0;
14
+ case "quadratic": return 1;
15
+ case "weighted-reputation": return 2;
16
+ default:
17
+ throw new Error(`Unknown votingSystem: "${vs}"`);
18
+ }
19
+ }
@@ -0,0 +1,136 @@
1
+ const { ethers } = require("hardhat");
2
+
3
+ async function main() {
4
+ const [deployer] = await ethers.getSigners();
5
+ console.log("Deployer:", deployer.address);
6
+ console.log("Balance:", ethers.formatEther(await deployer.provider.getBalance(deployer.address)), "ETH\n");
7
+
8
+ console.log("Deploying Token...");
9
+ const Token = await ethers.getContractFactory("Token");
10
+ const token = await Token.deploy(
11
+ "{{tokenName}}",
12
+ "{{tokenSymbol}}",
13
+ {{totalSupply}},
14
+ deployer.address
15
+ );
16
+ await token.waitForDeployment();
17
+ const tokenAddr = await token.getAddress();
18
+ console.log(" Token:", tokenAddr);
19
+
20
+ {{#if isTimelock}}
21
+ console.log("Deploying Timelock...");
22
+ const Timelock = await ethers.getContractFactory("Timelock");
23
+ const timelock = await Timelock.deploy(
24
+ {{timelockDelay}},
25
+ [],
26
+ [],
27
+ deployer.address
28
+ );
29
+ await timelock.waitForDeployment();
30
+ const timelockAddr = await timelock.getAddress();
31
+ console.log(" Timelock:", timelockAddr);
32
+ {{/if}}
33
+
34
+ console.log("Deploying GatewayRegistry...");
35
+ const GatewayRegistry = await ethers.getContractFactory("GatewayRegistry");
36
+ const registry = await GatewayRegistry.deploy(deployer.address);
37
+ await registry.waitForDeployment();
38
+ const registryAddr = await registry.getAddress();
39
+ console.log(" GatewayRegistry:", registryAddr);
40
+
41
+ console.log("Deploying PolicyRegistry...");
42
+ const PolicyRegistry = await ethers.getContractFactory("PolicyRegistry");
43
+ const policy = await PolicyRegistry.deploy(deployer.address);
44
+ await policy.waitForDeployment();
45
+ const policyAddr = await policy.getAddress();
46
+ console.log(" PolicyRegistry:", policyAddr);
47
+
48
+ console.log("\nRegistering organizations...");
49
+ const tokensPerMember = ethers.parseEther("{{tokensPerMember}}");
50
+
51
+ {{#each organizations}}
52
+ await registry.registerOrganization("{{this.wallet}}", "{{this.name}}", {{this.reputation}});
53
+ await token.transfer("{{this.wallet}}", tokensPerMember);
54
+ console.log(" Registered: {{this.name}} ({{this.wallet}})");
55
+
56
+ {{#each this.gateways}}
57
+ await registry.registerGateway("{{this.publicKey}}", "{{../this.wallet}}","{{this.name}}");
58
+ console.log(" Gateway registered: {{this.publicKey}} for {{../this.name}}");
59
+ {{/each}}
60
+ {{/each}}
61
+
62
+ console.log("\nAdding protocol parameters...");
63
+ {{#each protocolParameters}}
64
+ // {{this.key}} = {{this.originalValue}} (encoded as {{this.value}})
65
+ await policy.addParameter("{{this.key}}", {{this.value}}n);
66
+ {{/each}}
67
+
68
+ const remaining = await token.balanceOf(deployer.address);
69
+ {{#if isTimelock}}
70
+ await token.transfer(timelockAddr, remaining);
71
+ console.log(" timelock:", ethers.formatEther(remaining), "{{tokenSymbol}}");
72
+ {{else}}
73
+ console.warn(" Warning: No timelock configured; treasury remains with deployer.");
74
+ {{/if}}
75
+
76
+ console.log("isWeightedReputation:", {{isWeightedReputation}});
77
+
78
+ console.log("\nDeploying {{contractName}}...");
79
+ const Governor = await ethers.getContractFactory("{{contractName}}");
80
+ const governor = await Governor.deploy(
81
+ "{{daoName}}",
82
+ tokenAddr,
83
+ {{#if isTimelock}}timelockAddr,{{/if}}
84
+ {{votingDelay}},
85
+ {{votingPeriod}},
86
+ {{proposalThreshold}},
87
+ {{quorumFraction}},
88
+ {{votingSystemIndex}},
89
+ {{#if isWeightedReputation}}registryAddr{{else}}ethers.ZeroAddress{{/if}}
90
+ );
91
+ await governor.waitForDeployment();
92
+ const governorAddr = await governor.getAddress();
93
+ console.log(" {{contractName}}:", governorAddr);
94
+
95
+ {{#if isTimelock}}
96
+ console.log("\nConfiguring timelock roles...");
97
+ const PROPOSER_ROLE = await timelock.PROPOSER_ROLE();
98
+ const CANCELLER_ROLE = await timelock.CANCELLER_ROLE();
99
+ const EXECUTOR_ROLE = await timelock.EXECUTOR_ROLE();
100
+ const ADMIN_ROLE = await timelock.DEFAULT_ADMIN_ROLE();
101
+
102
+ await timelock.grantRole(PROPOSER_ROLE, governorAddr);
103
+ await timelock.grantRole(CANCELLER_ROLE, governorAddr);
104
+ await timelock.grantRole(EXECUTOR_ROLE, ethers.ZeroAddress);
105
+ console.log(" PROPOSER -> governor");
106
+ console.log(" CANCELLER -> governor");
107
+ console.log(" EXECUTOR -> open (address(0))");
108
+
109
+ console.log("\nTransferring ownership of gateway registry, policy registry and token contracts to timelock...");
110
+ await registry.transferOwnership(timelockAddr);
111
+ await policy.transferOwnership(timelockAddr);
112
+ await token.transferOwnership(timelockAddr);
113
+
114
+ console.log("Revoking deployer admin role...");
115
+ await timelock.revokeRole(ADMIN_ROLE, deployer.address);
116
+ console.log(" Deployer has no remaining privileges");
117
+ {{else}}
118
+ await registry.transferOwnership(governorAddr);
119
+ await policy.transferOwnership(governorAddr);
120
+ await token.transferOwnership(governorAddr);
121
+ console.log(" Ownership transferred directly to governor (no timelock).");
122
+ {{/if}}
123
+ console.log("\nDeployment summary:");
124
+ console.log(" Token: ", tokenAddr);
125
+ {{#if isTimelock}}
126
+ console.log(" Timelock: ", timelockAddr);
127
+ {{/if}}
128
+ console.log(" GatewayRegistry: ", registryAddr);
129
+ console.log(" PolicyRegistry: ", policyAddr);
130
+ console.log(" Governor: ", governorAddr);
131
+ }
132
+
133
+ main().catch((err) => {
134
+ console.error(err);
135
+ process.exit(1);
136
+ });
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "TestDao",
3
+ "tokenomics": {
4
+ "name": "TestDao",
5
+ "symbol": "RDAO",
6
+ "supply": 1000000,
7
+ "defaultMemberTokens": 1000
8
+ },
9
+ "organizations": [
10
+ {
11
+ "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
12
+ "name": "Organization A",
13
+ "gateways": [
14
+ {
15
+ "publicKey": "0x02e68acfc0253a10620dff079256c9efb7df3a7eb5b4dc8d03aefc3cbd5c20a1b0",
16
+ "name": "GatewayA1"
17
+ },
18
+ {
19
+ "publicKey": "0x03b17e6b5f7b87e2b0ef3e1c4d942f19a8d1f5e3c9b4872e3e9e3e3e3e3e3e3e",
20
+ "name": "GatewayA2"
21
+ }
22
+ ]
23
+ },
24
+ {
25
+ "address": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
26
+ "name": "Organization B",
27
+ "gateways": [
28
+ {
29
+ "publicKey": "0x027a1b2c3d4e5f60718293a4b5c6d7e8f90123456789abcdef0123456789abcdef",
30
+ "name": "GatewayB1"
31
+ }
32
+ ]
33
+ },
34
+ {
35
+ "address": "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC",
36
+ "name": "Organization C",
37
+ "gateways": [
38
+ {
39
+ "publicKey": "0x03f123456789abcdef0123456789abcdef0123456789abcdef0123456789abcd",
40
+ "name": "GatewayC1"
41
+ }
42
+ ]
43
+ }
44
+ ],
45
+ "governance": {
46
+ "votingPeriod": 5,
47
+ "votingDelay": 1,
48
+ "proposalThreshold": 0,
49
+ "quorumFraction": 0,
50
+ "votingSystem": "token-based"
51
+ },
52
+ "timelock": {
53
+ "enabled": false,
54
+ "minDelay": 0
55
+ },
56
+ "protocolParameters": [
57
+ { "key": "satp.version", "value": "v02" },
58
+ { "key": "satp.crash.version", "value": "v02" },
59
+ { "key": "satp.session.lockExpirationTime", "value": "34" },
60
+ { "key": "signingAlgorithm", "value": "SECP256K1" },
61
+ { "key": "claimFormat", "value": "3" }
62
+ ]
63
+ }
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "ichaingov-contract-generator",
3
+ "version": "1.0.0",
4
+ "description": "ichaingov contract generator — generates governance contracts from config.json",
5
+ "type": "module",
6
+ "bin": {
7
+ "ichaingov-contract-generator": "./bin/cli.js"
8
+ },
9
+ "license": "MIT",
10
+ "keywords": [
11
+ "dao",
12
+ "governance"
13
+ ],
14
+ "author": "Ana Sa",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/bh19cp/ichaingov-contract-generator.git"
18
+ },
19
+ "files": [
20
+ "bin",
21
+ "core",
22
+ "index"
23
+ ],
24
+ "dependencies": {
25
+ "@openzeppelin/contracts": "^5.6.1",
26
+ "ethers": "^6.16.0",
27
+ "fs-extra": "^11.2.0",
28
+ "handlebars": "^4.7.8",
29
+ "zod": "^3.25.76"
30
+ },
31
+ "devDependencies": {
32
+ "@nomicfoundation/hardhat-toolbox": "^4.0.0",
33
+ "hardhat": "^2.28.6"
34
+ },
35
+ "engines": {
36
+ "node": ">=18.0.0"
37
+ }
38
+ }