dexe-mcp 0.11.0 → 0.14.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.
Files changed (77) hide show
  1. package/CHANGELOG.md +109 -0
  2. package/README.md +11 -6
  3. package/dist/cli/init.d.ts +6 -0
  4. package/dist/cli/init.d.ts.map +1 -1
  5. package/dist/cli/init.js +64 -2
  6. package/dist/cli/init.js.map +1 -1
  7. package/dist/config.d.ts +13 -0
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +12 -0
  10. package/dist/config.js.map +1 -1
  11. package/dist/diag/checks.d.ts.map +1 -1
  12. package/dist/diag/checks.js +29 -0
  13. package/dist/diag/checks.js.map +1 -1
  14. package/dist/env/schema.d.ts +14 -0
  15. package/dist/env/schema.d.ts.map +1 -1
  16. package/dist/env/schema.js +14 -0
  17. package/dist/env/schema.js.map +1 -1
  18. package/dist/index.js +7 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/lib/preflight.d.ts +133 -0
  21. package/dist/lib/preflight.d.ts.map +1 -0
  22. package/dist/lib/preflight.js +239 -0
  23. package/dist/lib/preflight.js.map +1 -0
  24. package/dist/lib/proposalBuilders.d.ts +50 -0
  25. package/dist/lib/proposalBuilders.d.ts.map +1 -0
  26. package/dist/lib/proposalBuilders.js +311 -0
  27. package/dist/lib/proposalBuilders.js.map +1 -0
  28. package/dist/lib/stateStore.d.ts +63 -0
  29. package/dist/lib/stateStore.d.ts.map +1 -0
  30. package/dist/lib/stateStore.js +128 -0
  31. package/dist/lib/stateStore.js.map +1 -0
  32. package/dist/lib/time.d.ts +16 -0
  33. package/dist/lib/time.d.ts.map +1 -0
  34. package/dist/lib/time.js +24 -0
  35. package/dist/lib/time.js.map +1 -0
  36. package/dist/tools/daoCreate.d.ts +6 -0
  37. package/dist/tools/daoCreate.d.ts.map +1 -0
  38. package/dist/tools/daoCreate.js +155 -0
  39. package/dist/tools/daoCreate.js.map +1 -0
  40. package/dist/tools/daoDeploy.d.ts +394 -0
  41. package/dist/tools/daoDeploy.d.ts.map +1 -1
  42. package/dist/tools/daoDeploy.js +393 -422
  43. package/dist/tools/daoDeploy.js.map +1 -1
  44. package/dist/tools/doctor.d.ts.map +1 -1
  45. package/dist/tools/doctor.js +15 -0
  46. package/dist/tools/doctor.js.map +1 -1
  47. package/dist/tools/flow.d.ts +13 -2
  48. package/dist/tools/flow.d.ts.map +1 -1
  49. package/dist/tools/flow.js +97 -16
  50. package/dist/tools/flow.js.map +1 -1
  51. package/dist/tools/gate.d.ts +28 -0
  52. package/dist/tools/gate.d.ts.map +1 -0
  53. package/dist/tools/gate.js +295 -0
  54. package/dist/tools/gate.js.map +1 -0
  55. package/dist/tools/index.d.ts.map +1 -1
  56. package/dist/tools/index.js +14 -1
  57. package/dist/tools/index.js.map +1 -1
  58. package/dist/tools/operationalContext.d.ts +6 -0
  59. package/dist/tools/operationalContext.d.ts.map +1 -0
  60. package/dist/tools/operationalContext.js +122 -0
  61. package/dist/tools/operationalContext.js.map +1 -0
  62. package/dist/tools/otc.d.ts.map +1 -1
  63. package/dist/tools/otc.js +3 -0
  64. package/dist/tools/otc.js.map +1 -1
  65. package/dist/tools/proposalBuildMore.d.ts +69 -0
  66. package/dist/tools/proposalBuildMore.d.ts.map +1 -1
  67. package/dist/tools/proposalBuildMore.js +3 -2
  68. package/dist/tools/proposalBuildMore.js.map +1 -1
  69. package/dist/tools/subgraph.d.ts.map +1 -1
  70. package/dist/tools/subgraph.js +3 -0
  71. package/dist/tools/subgraph.js.map +1 -1
  72. package/package.json +2 -1
  73. package/skills/dexe-create-dao/SKILL.md +99 -0
  74. package/skills/dexe-create-proposal/SKILL.md +84 -0
  75. package/skills/dexe-otc/SKILL.md +73 -0
  76. package/skills/dexe-setup/SKILL.md +108 -0
  77. package/skills/dexe-vote-execute/SKILL.md +64 -0
@@ -161,6 +161,394 @@ function payloadOutputSchema() {
161
161
  predictedGovPool: z.string().nullable(),
162
162
  };
163
163
  }
164
+ // ---------- shared deploy params schema (reused by dexe_dao_create) ----------
165
+ /**
166
+ * Full `deployGovPool` parameter object. `descriptionURL` and `name` are the
167
+ * two fields `dexe_dao_create` derives itself (from the uploaded DAO metadata
168
+ * + daoName), so it consumes `DeployParamsSchema.omit({ descriptionURL, name })`.
169
+ */
170
+ export const DeployParamsSchema = z.object({
171
+ settingsParams: SettingsDeployParamsSchema,
172
+ validatorsParams: ValidatorsDeployParamsSchema.optional(),
173
+ userKeeperParams: UserKeeperDeployParamsSchema,
174
+ tokenParams: TokenParamsSchema,
175
+ votePowerParams: VotePowerDeployParamsSchema,
176
+ verifier: z.string().default("0x0000000000000000000000000000000000000000"),
177
+ onlyBABTHolders: z.boolean().default(false),
178
+ descriptionURL: z.string().describe("ipfs://<cid> of DAO metadata JSON"),
179
+ name: z.string().min(1),
180
+ });
181
+ /**
182
+ * Pure builder for the `PoolFactory.deployGovPool` tx. Extracted from the
183
+ * `dexe_dao_build_deploy` handler so the `dexe_dao_create` composite reuses the
184
+ * exact same predicted-address wiring, settings auto-expand, cap/minted guard,
185
+ * executorDescription IPFS upload, and encoding. Returns a discriminated result
186
+ * instead of tool-shaped content.
187
+ */
188
+ export async function buildDeployGovPool(input, ctx, rpc) {
189
+ const { chainId, poolFactory, deployer, params } = input;
190
+ const fail = (error) => ({ ok: false, error });
191
+ const chain = resolveChain(ctx.config, chainId);
192
+ const isTokenCreation = params.tokenParams.name.length > 0;
193
+ const hasValidators = !!(params.validatorsParams && params.validatorsParams.validators.length > 0);
194
+ // ---------- auto-encode vote power initData ----------
195
+ let votePowerInitData;
196
+ try {
197
+ if (params.votePowerParams.voteType === "CUSTOM_VOTES") {
198
+ votePowerInitData = params.votePowerParams.initData ?? "0x";
199
+ }
200
+ else {
201
+ votePowerInitData = encodeVotePowerInitData(params.votePowerParams.voteType, params.votePowerParams.polynomialCoefficients);
202
+ }
203
+ }
204
+ catch (err) {
205
+ return fail(`Failed to encode vote power initData: ${err instanceof Error ? err.message : String(err)}`);
206
+ }
207
+ // ---------- default validatorsParams when omitted ----------
208
+ const VT_SUFFIX = "-VT";
209
+ const rawValidatorsParams = params.validatorsParams ?? {
210
+ name: "Validator Token",
211
+ symbol: "VT",
212
+ proposalSettings: {
213
+ duration: params.settingsParams.proposalSettings[0].duration,
214
+ executionDelay: params.settingsParams.proposalSettings[0].executionDelay,
215
+ quorum: params.settingsParams.proposalSettings[0].quorum,
216
+ },
217
+ validators: [],
218
+ balances: [],
219
+ };
220
+ const validatorsParams = {
221
+ ...rawValidatorsParams,
222
+ name: rawValidatorsParams.name.endsWith(VT_SUFFIX) ? rawValidatorsParams.name : rawValidatorsParams.name + VT_SUFFIX,
223
+ symbol: rawValidatorsParams.symbol.endsWith(VT_SUFFIX) ? rawValidatorsParams.symbol : rawValidatorsParams.symbol + VT_SUFFIX,
224
+ };
225
+ // ---------- validate leaf addresses ----------
226
+ if (poolFactory && !isAddress(poolFactory))
227
+ return fail(`Invalid poolFactory: ${poolFactory}`);
228
+ if (!isAddress(deployer))
229
+ return fail(`Invalid deployer: ${deployer}`);
230
+ if (isTokenCreation && params.tokenParams.users.length === 0)
231
+ return fail("tokenParams.users must have at least one recipient when creating a new token");
232
+ if (!isAddress(params.verifier))
233
+ return fail(`Invalid verifier: ${params.verifier}`);
234
+ if (!isAddress(params.userKeeperParams.tokenAddress))
235
+ return fail(`Invalid userKeeperParams.tokenAddress`);
236
+ if (!isAddress(params.userKeeperParams.nftAddress))
237
+ return fail(`Invalid userKeeperParams.nftAddress`);
238
+ if (!isAddress(params.votePowerParams.presetAddress))
239
+ return fail(`Invalid votePowerParams.presetAddress`);
240
+ for (const v of validatorsParams.validators) {
241
+ if (!isAddress(v))
242
+ return fail(`Invalid validator: ${v}`);
243
+ }
244
+ if (validatorsParams.validators.length !== validatorsParams.balances.length) {
245
+ return fail("validatorsParams.validators and .balances must be the same length");
246
+ }
247
+ for (const u of params.tokenParams.users) {
248
+ if (!isAddress(u))
249
+ return fail(`Invalid tokenParams.users entry: ${u}`);
250
+ }
251
+ if (params.tokenParams.users.length !== params.tokenParams.amounts.length) {
252
+ return fail("tokenParams.users and .amounts must be the same length");
253
+ }
254
+ // Bug #28: ERC20Gov init reverts silently when cap == mintedTotal.
255
+ if (isTokenCreation) {
256
+ const capBn = BigInt(params.tokenParams.cap);
257
+ const mintedBn = BigInt(params.tokenParams.mintedTotal);
258
+ if (capBn > 0n && capBn <= mintedBn) {
259
+ return fail(`tokenParams.cap (${capBn}) must be strictly greater than tokenParams.mintedTotal (${mintedBn}). ERC20Gov requires headroom for future minting; pass cap=0 for uncapped, or cap > mintedTotal.`);
260
+ }
261
+ }
262
+ // ---------- resolve PoolFactory address ----------
263
+ let factoryAddress = poolFactory;
264
+ if (!factoryAddress) {
265
+ try {
266
+ const pr = rpc.tryProvider(chain.chainId);
267
+ if ("error" in pr)
268
+ return fail(`${pr.error}\n${pr.remediation}`);
269
+ const provider = pr.ok;
270
+ const book = new AddressBook({
271
+ provider,
272
+ chainId: chain.chainId,
273
+ registryOverride: chain.registryOverride ?? ctx.config.registryOverride,
274
+ });
275
+ factoryAddress = await book.resolve(CONTRACT_NAMES.POOL_FACTORY);
276
+ }
277
+ catch (err) {
278
+ return fail(`poolFactory address needed: pass it explicitly, or configure DEXE_RPC_URL so the ContractsRegistry lookup works. (${err instanceof Error ? err.message : String(err)})`);
279
+ }
280
+ }
281
+ // ---------- predict addresses (always required — wires executors) ----------
282
+ let predictedGovPool;
283
+ let predictedGovToken;
284
+ let predictedDistribution;
285
+ let predictedTokenSale;
286
+ try {
287
+ const pr = rpc.tryProvider(chain.chainId);
288
+ if ("error" in pr)
289
+ return fail(`${pr.error}\n${pr.remediation}`);
290
+ const provider = pr.ok;
291
+ const predictIface = new Interface([
292
+ "function predictGovAddresses(address deployer, string poolName) view returns (tuple(address govPool, address govTokenSale, address govToken, address distributionProposal, address expertNft, address nftMultiplier))",
293
+ ]);
294
+ const { Contract } = await import("ethers");
295
+ const factory = new Contract(factoryAddress, predictIface, provider);
296
+ const res = await factory.getFunction("predictGovAddresses").staticCall(deployer, params.name);
297
+ predictedGovPool = res.govPool;
298
+ predictedGovToken = res.govToken;
299
+ predictedDistribution = res.distributionProposal;
300
+ predictedTokenSale = res.govTokenSale;
301
+ }
302
+ catch (err) {
303
+ return fail(`Failed to predict gov addresses: ${err instanceof Error ? err.message : String(err)}`);
304
+ }
305
+ const ZERO = "0x0000000000000000000000000000000000000000";
306
+ const effectiveTokenAddress = isTokenCreation && predictedGovToken ? predictedGovToken : params.userKeeperParams.tokenAddress;
307
+ // Bug #12: substitute predicted govToken as rewardToken when reward amounts set.
308
+ if (isTokenCreation && predictedGovToken) {
309
+ const hasRewardAmounts = (r) => BigInt(r.creationReward) > 0n || BigInt(r.executionReward) > 0n || BigInt(r.voteRewardsCoefficient) > 0n;
310
+ for (const s of params.settingsParams.proposalSettings) {
311
+ if ((s.rewardsInfo.rewardToken === ZERO || s.rewardsInfo.rewardToken === "") && hasRewardAmounts(s.rewardsInfo)) {
312
+ s.rewardsInfo.rewardToken = predictedGovToken;
313
+ }
314
+ }
315
+ }
316
+ const effectiveExecutors = [...params.settingsParams.additionalProposalExecutors];
317
+ if (predictedDistribution && !effectiveExecutors.includes(predictedDistribution))
318
+ effectiveExecutors.push(predictedDistribution);
319
+ if (predictedTokenSale && !effectiveExecutors.includes(predictedTokenSale))
320
+ effectiveExecutors.push(predictedTokenSale);
321
+ if (effectiveTokenAddress === ZERO && params.userKeeperParams.nftAddress === ZERO) {
322
+ return fail("GovUK requires at least one governance asset: set userKeeperParams.tokenAddress (existing ERC20), userKeeperParams.nftAddress (existing ERC721), or provide tokenParams.name to create a new token");
323
+ }
324
+ // ---------- auto-expand proposal settings (1 → 5) ----------
325
+ let expandedSettings = params.settingsParams.proposalSettings;
326
+ if (expandedSettings.length === 1) {
327
+ const base = expandedSettings[0];
328
+ const baseWithValidatorFallback = {
329
+ ...base,
330
+ validatorsVote: true,
331
+ durationValidators: hasValidators ? base.durationValidators : base.duration,
332
+ quorumValidators: hasValidators ? base.quorumValidators : base.quorum,
333
+ };
334
+ const dpSettings = { ...baseWithValidatorFallback, delegatedVotingAllowed: false, earlyCompletion: false };
335
+ expandedSettings = [
336
+ baseWithValidatorFallback,
337
+ baseWithValidatorFallback,
338
+ baseWithValidatorFallback,
339
+ dpSettings,
340
+ baseWithValidatorFallback,
341
+ ];
342
+ }
343
+ else if (expandedSettings.length !== 5) {
344
+ return fail(`proposalSettings must be 1 (auto-expand to 5) or exactly 5. Got ${expandedSettings.length}.`);
345
+ }
346
+ // ---------- treasury-safety advisory: quorum floor ----------
347
+ let quorumWarning = "";
348
+ if (ctx.config.treasuryGuard !== "off") {
349
+ const floor = ctx.config.minSafeQuorumPct;
350
+ const showPct = (p) => (Number.isFinite(p) ? `${p}%` : "unparseable");
351
+ const lowQuorum = [];
352
+ expandedSettings.forEach((s, i) => {
353
+ const pct = quorumPctFromRaw(s.quorum);
354
+ if (judgeQuorum(pct, floor) !== "SAFE")
355
+ lowQuorum.push(`proposalSettings[${i}] quorum=${showPct(pct)}`);
356
+ });
357
+ const vpct = quorumPctFromRaw(validatorsParams.proposalSettings.quorum);
358
+ if (judgeQuorum(vpct, floor) !== "SAFE")
359
+ lowQuorum.push(`validatorsParams quorum=${showPct(vpct)}`);
360
+ if (lowQuorum.length > 0) {
361
+ quorumWarning =
362
+ `\n⚠️ Quorum below the ${floor}% safe floor (DEXE_MIN_SAFE_QUORUM_PCT): ${lowQuorum.join(", ")}. ` +
363
+ `Low quorum reduces the participation required to pass a proposal; for a DAO that will hold ` +
364
+ `treasury assets, set quorum ≥50% (51%+ recommended). The safe value is DAO-specific and must ` +
365
+ `be verified. [governance-safety advisory]`;
366
+ }
367
+ }
368
+ // ---------- auto-upload executorDescription to IPFS ----------
369
+ let pinataWarning = "";
370
+ if (ctx.config.pinataJwt) {
371
+ const pinata = new PinataClient(ctx.config.pinataJwt);
372
+ const settingsToUpload = [
373
+ { index: 0, label: "default" },
374
+ { index: 3, label: "distributionProposal" },
375
+ ];
376
+ for (const { index, label } of settingsToUpload) {
377
+ const s = expandedSettings[index];
378
+ if (!s.executorDescription || s.executorDescription === "") {
379
+ try {
380
+ const settingsJson = {
381
+ earlyCompletion: s.earlyCompletion,
382
+ delegatedVotingAllowed: s.delegatedVotingAllowed,
383
+ validatorsVote: s.validatorsVote,
384
+ duration: s.duration,
385
+ durationValidators: s.durationValidators,
386
+ quorum: s.quorum,
387
+ quorumValidators: s.quorumValidators,
388
+ minVotesForVoting: s.minVotesForVoting,
389
+ minVotesForCreating: s.minVotesForCreating,
390
+ executionDelay: s.executionDelay,
391
+ rewardsInfo: {
392
+ rewardToken: s.rewardsInfo.rewardToken,
393
+ creationReward: s.rewardsInfo.creationReward,
394
+ executionReward: s.rewardsInfo.executionReward,
395
+ voteRewardsCoefficient: s.rewardsInfo.voteRewardsCoefficient,
396
+ },
397
+ minVotesForReadProposalDiscussion: "0",
398
+ minVotesForCreatingComment: "1000000000000000000",
399
+ };
400
+ const res = await pinata.pinJson(settingsJson, { name: `dao-settings-${label}:${params.name.slice(0, 40)}` });
401
+ const cid = `ipfs://${res.cid}`;
402
+ if (index === 0) {
403
+ expandedSettings[0] = { ...expandedSettings[0], executorDescription: cid };
404
+ expandedSettings[1] = { ...expandedSettings[1], executorDescription: cid };
405
+ expandedSettings[2] = { ...expandedSettings[2], executorDescription: cid };
406
+ expandedSettings[4] = { ...expandedSettings[4], executorDescription: cid };
407
+ }
408
+ else {
409
+ expandedSettings[3] = { ...expandedSettings[3], executorDescription: cid };
410
+ }
411
+ }
412
+ catch (err) {
413
+ pinataWarning += `\n⚠️ Failed to upload ${label} executorDescription: ${err instanceof Error ? err.message : String(err)}`;
414
+ }
415
+ }
416
+ }
417
+ }
418
+ else {
419
+ const missingDesc = expandedSettings.some((s) => !s.executorDescription || s.executorDescription === "");
420
+ if (missingDesc) {
421
+ pinataWarning =
422
+ "\n⚠️ DEXE_PINATA_JWT not configured — executorDescription fields are empty. " +
423
+ "DAOs created without executorDescription will display broken proposal settings in the frontend UI. " +
424
+ "Set DEXE_PINATA_JWT to enable auto-upload.";
425
+ }
426
+ }
427
+ // ---------- build the tuple arg ----------
428
+ const paramsTuple = [
429
+ [
430
+ expandedSettings.map((s) => [
431
+ s.earlyCompletion,
432
+ s.delegatedVotingAllowed,
433
+ s.validatorsVote,
434
+ BigInt(s.duration),
435
+ BigInt(s.durationValidators),
436
+ BigInt(s.executionDelay),
437
+ BigInt(s.quorum),
438
+ BigInt(s.quorumValidators),
439
+ BigInt(s.minVotesForVoting),
440
+ BigInt(s.minVotesForCreating),
441
+ [
442
+ s.rewardsInfo.rewardToken,
443
+ BigInt(s.rewardsInfo.creationReward),
444
+ BigInt(s.rewardsInfo.executionReward),
445
+ BigInt(s.rewardsInfo.voteRewardsCoefficient),
446
+ ],
447
+ s.executorDescription,
448
+ ]),
449
+ effectiveExecutors,
450
+ ],
451
+ [
452
+ validatorsParams.name,
453
+ validatorsParams.symbol,
454
+ [
455
+ BigInt(validatorsParams.proposalSettings.duration),
456
+ BigInt(validatorsParams.proposalSettings.executionDelay),
457
+ BigInt(validatorsParams.proposalSettings.quorum),
458
+ ],
459
+ validatorsParams.validators,
460
+ validatorsParams.balances.map((b) => BigInt(b)),
461
+ ],
462
+ [
463
+ effectiveTokenAddress,
464
+ params.userKeeperParams.nftAddress,
465
+ BigInt(params.userKeeperParams.individualPower),
466
+ BigInt(params.userKeeperParams.nftsTotalSupply),
467
+ ],
468
+ [
469
+ params.tokenParams.name,
470
+ params.tokenParams.symbol,
471
+ params.tokenParams.users,
472
+ BigInt(params.tokenParams.cap),
473
+ BigInt(params.tokenParams.mintedTotal),
474
+ params.tokenParams.amounts.map((a) => BigInt(a)),
475
+ ],
476
+ [
477
+ VOTE_POWER_TYPES.indexOf(params.votePowerParams.voteType),
478
+ votePowerInitData,
479
+ params.votePowerParams.presetAddress,
480
+ ],
481
+ params.verifier,
482
+ params.onlyBABTHolders,
483
+ params.descriptionURL.replace(/^ipfs:\/\//, ""),
484
+ params.name,
485
+ ];
486
+ // ---------- get ABI (artifact > fallback) ----------
487
+ let iface;
488
+ let ifaceSource;
489
+ try {
490
+ const records = ctx.artifacts.get("PoolFactory");
491
+ if (records.length > 0) {
492
+ iface = new Interface(records[0].abi);
493
+ ifaceSource = "compiled artifact";
494
+ try {
495
+ iface.getFunction("deployGovPool");
496
+ }
497
+ catch {
498
+ iface = new Interface(FALLBACK_POOL_FACTORY_ABI);
499
+ ifaceSource = "fallback (compiled artifact missing deployGovPool)";
500
+ }
501
+ }
502
+ else {
503
+ iface = new Interface(FALLBACK_POOL_FACTORY_ABI);
504
+ ifaceSource = "fallback (artifacts not loaded — run dexe_compile for strict parity)";
505
+ }
506
+ }
507
+ catch (err) {
508
+ if (err instanceof ArtifactsMissingError) {
509
+ iface = new Interface(FALLBACK_POOL_FACTORY_ABI);
510
+ ifaceSource = "fallback (no artifacts — run dexe_compile for strict parity)";
511
+ }
512
+ else {
513
+ return fail(`Failed to load PoolFactory ABI: ${err instanceof Error ? err.message : String(err)}`);
514
+ }
515
+ }
516
+ // ---------- encode ----------
517
+ let payload;
518
+ try {
519
+ payload = buildPayload({
520
+ to: factoryAddress,
521
+ iface,
522
+ method: "deployGovPool",
523
+ args: [paramsTuple],
524
+ chainId: chain.chainId,
525
+ contractLabel: "PoolFactory",
526
+ description: `PoolFactory.deployGovPool("${params.name}") via ${ifaceSource}`,
527
+ });
528
+ }
529
+ catch (err) {
530
+ return fail(`deployGovPool encoding failed: ${err instanceof Error ? err.message : String(err)}`);
531
+ }
532
+ let note = ifaceSource.includes("fallback")
533
+ ? "⚠️ Using fallback tuple ABI. For guaranteed parity, run dexe_compile to populate artifacts."
534
+ : "Encoded against compiled artifact — strict parity with deployed PoolFactory.";
535
+ if (pinataWarning)
536
+ note += pinataWarning;
537
+ if (quorumWarning)
538
+ note += quorumWarning;
539
+ return {
540
+ ok: true,
541
+ payload,
542
+ predictedGovPool,
543
+ note,
544
+ predicted: {
545
+ govPool: predictedGovPool,
546
+ govToken: predictedGovToken,
547
+ distributionProposal: predictedDistribution,
548
+ govTokenSale: predictedTokenSale,
549
+ },
550
+ };
551
+ }
164
552
  // ---------- dexe_dao_build_deploy ----------
165
553
  function registerBuildDeploy(server, ctx, rpc) {
166
554
  server.registerTool("dexe_dao_build_deploy", {
@@ -202,431 +590,14 @@ function registerBuildDeploy(server, ctx, rpc) {
202
590
  deployer: z
203
591
  .string()
204
592
  .describe("tx.origin that will send the deploy tx — required for address prediction"),
205
- params: z.object({
206
- settingsParams: SettingsDeployParamsSchema,
207
- validatorsParams: ValidatorsDeployParamsSchema.optional(),
208
- userKeeperParams: UserKeeperDeployParamsSchema,
209
- tokenParams: TokenParamsSchema,
210
- votePowerParams: VotePowerDeployParamsSchema,
211
- verifier: z.string().default("0x0000000000000000000000000000000000000000"),
212
- onlyBABTHolders: z.boolean().default(false),
213
- descriptionURL: z.string().describe("ipfs://<cid> of DAO metadata JSON"),
214
- name: z.string().min(1),
215
- }),
593
+ params: DeployParamsSchema,
216
594
  },
217
595
  outputSchema: payloadOutputSchema(),
218
596
  }, async ({ chainId, poolFactory, deployer, params }) => {
219
- const chain = resolveChain(ctx.config, chainId);
220
- const isTokenCreation = params.tokenParams.name.length > 0;
221
- const hasValidators = !!(params.validatorsParams && params.validatorsParams.validators.length > 0);
222
- // ---------- auto-encode vote power initData ----------
223
- let votePowerInitData;
224
- try {
225
- if (params.votePowerParams.voteType === "CUSTOM_VOTES") {
226
- // CUSTOM_VOTES: use caller-provided initData (contract skips .call for custom)
227
- votePowerInitData = params.votePowerParams.initData ?? "0x";
228
- }
229
- else {
230
- // LINEAR / POLYNOMIAL: auto-encode the correct initializer
231
- votePowerInitData = encodeVotePowerInitData(params.votePowerParams.voteType, params.votePowerParams.polynomialCoefficients);
232
- }
233
- }
234
- catch (err) {
235
- return errorResult(`Failed to encode vote power initData: ${err instanceof Error ? err.message : String(err)}`);
236
- }
237
- // ---------- default validatorsParams when omitted ----------
238
- const VT_SUFFIX = "-VT";
239
- const rawValidatorsParams = params.validatorsParams ?? {
240
- name: "Validator Token",
241
- symbol: "VT",
242
- proposalSettings: {
243
- duration: params.settingsParams.proposalSettings[0].duration,
244
- executionDelay: params.settingsParams.proposalSettings[0].executionDelay,
245
- quorum: params.settingsParams.proposalSettings[0].quorum,
246
- },
247
- validators: [],
248
- balances: [],
249
- };
250
- // Bug #11: Frontend appends "-VT" suffix to validator token name and symbol.
251
- // The form UI always stores name/symbol with the suffix already included.
252
- // Replicate that here: if the suffix isn't already present, append it.
253
- const validatorsParams = {
254
- ...rawValidatorsParams,
255
- name: rawValidatorsParams.name.endsWith(VT_SUFFIX)
256
- ? rawValidatorsParams.name
257
- : rawValidatorsParams.name + VT_SUFFIX,
258
- symbol: rawValidatorsParams.symbol.endsWith(VT_SUFFIX)
259
- ? rawValidatorsParams.symbol
260
- : rawValidatorsParams.symbol + VT_SUFFIX,
261
- };
262
- // ---------- validate leaf addresses ----------
263
- if (poolFactory && !isAddress(poolFactory))
264
- return errorResult(`Invalid poolFactory: ${poolFactory}`);
265
- if (!isAddress(deployer))
266
- return errorResult(`Invalid deployer: ${deployer}`);
267
- if (isTokenCreation && params.tokenParams.users.length === 0)
268
- return errorResult("tokenParams.users must have at least one recipient when creating a new token");
269
- if (!isAddress(params.verifier))
270
- return errorResult(`Invalid verifier: ${params.verifier}`);
271
- if (!isAddress(params.userKeeperParams.tokenAddress))
272
- return errorResult(`Invalid userKeeperParams.tokenAddress`);
273
- if (!isAddress(params.userKeeperParams.nftAddress))
274
- return errorResult(`Invalid userKeeperParams.nftAddress`);
275
- if (!isAddress(params.votePowerParams.presetAddress))
276
- return errorResult(`Invalid votePowerParams.presetAddress`);
277
- for (const v of validatorsParams.validators) {
278
- if (!isAddress(v))
279
- return errorResult(`Invalid validator: ${v}`);
280
- }
281
- if (validatorsParams.validators.length !== validatorsParams.balances.length) {
282
- return errorResult("validatorsParams.validators and .balances must be the same length");
283
- }
284
- for (const u of params.tokenParams.users) {
285
- if (!isAddress(u))
286
- return errorResult(`Invalid tokenParams.users entry: ${u}`);
287
- }
288
- if (params.tokenParams.users.length !== params.tokenParams.amounts.length) {
289
- return errorResult("tokenParams.users and .amounts must be the same length");
290
- }
291
- // Bug #28: ERC20Gov init reverts inside _initGovPool with the generic
292
- // "Address: low-level delegate call failed" when cap == mintedTotal.
293
- // The token contract requires strict headroom (cap > minted) or cap=0
294
- // (uncapped). Pre-flight reject so callers get a clear error instead of
295
- // a 10M-gas revert hiding the real cause.
296
- if (isTokenCreation) {
297
- const capBn = BigInt(params.tokenParams.cap);
298
- const mintedBn = BigInt(params.tokenParams.mintedTotal);
299
- if (capBn > 0n && capBn <= mintedBn) {
300
- return errorResult(`tokenParams.cap (${capBn}) must be strictly greater than tokenParams.mintedTotal (${mintedBn}). ERC20Gov requires headroom for future minting; pass cap=0 for uncapped, or cap > mintedTotal.`);
301
- }
302
- }
303
- // ---------- resolve PoolFactory address ----------
304
- let factoryAddress = poolFactory;
305
- if (!factoryAddress) {
306
- try {
307
- const pr = rpc.tryProvider(chain.chainId);
308
- if ("error" in pr)
309
- return errorResult(`${pr.error}\n${pr.remediation}`);
310
- const provider = pr.ok;
311
- const book = new AddressBook({
312
- provider,
313
- chainId: chain.chainId,
314
- registryOverride: chain.registryOverride ?? ctx.config.registryOverride,
315
- });
316
- factoryAddress = await book.resolve(CONTRACT_NAMES.POOL_FACTORY);
317
- }
318
- catch (err) {
319
- return errorResult(`poolFactory address needed: pass it explicitly, or configure DEXE_RPC_URL so the ContractsRegistry lookup works. (${err instanceof Error ? err.message : String(err)})`);
320
- }
321
- }
322
- // ---------- predict addresses (always required — wires executors) ----------
323
- let predictedGovPool;
324
- let predictedGovToken;
325
- let predictedDistribution;
326
- let predictedTokenSale;
327
- try {
328
- const pr = rpc.tryProvider(chain.chainId);
329
- if ("error" in pr)
330
- return errorResult(`${pr.error}\n${pr.remediation}`);
331
- const provider = pr.ok;
332
- const predictIface = new Interface([
333
- "function predictGovAddresses(address deployer, string poolName) view returns (tuple(address govPool, address govTokenSale, address govToken, address distributionProposal, address expertNft, address nftMultiplier))",
334
- ]);
335
- const { Contract } = await import("ethers");
336
- const factory = new Contract(factoryAddress, predictIface, provider);
337
- const res = await factory.getFunction("predictGovAddresses").staticCall(deployer, params.name);
338
- predictedGovPool = res.govPool;
339
- predictedGovToken = res.govToken;
340
- predictedDistribution = res.distributionProposal;
341
- predictedTokenSale = res.govTokenSale;
342
- }
343
- catch (err) {
344
- return errorResult(`Failed to predict gov addresses: ${err instanceof Error ? err.message : String(err)}`);
345
- }
346
- const ZERO = "0x0000000000000000000000000000000000000000";
347
- // Wire predicted govToken into userKeeperParams when creating a new token
348
- const effectiveTokenAddress = isTokenCreation && predictedGovToken
349
- ? predictedGovToken
350
- : params.userKeeperParams.tokenAddress;
351
- // Bug #12: When "use governance token for rewards" is checked, the frontend
352
- // sets rewardToken to the predicted govToken address. When creating a new
353
- // token, the address isn't known at input time, so callers pass ZERO_ADDR.
354
- // Detect this case: rewardToken is zero BUT reward amounts are non-zero
355
- // → substitute the predicted govToken address (matching frontend behavior).
356
- if (isTokenCreation && predictedGovToken) {
357
- const hasRewardAmounts = (r) => BigInt(r.creationReward) > 0n || BigInt(r.executionReward) > 0n || BigInt(r.voteRewardsCoefficient) > 0n;
358
- for (const s of params.settingsParams.proposalSettings) {
359
- if ((s.rewardsInfo.rewardToken === ZERO || s.rewardsInfo.rewardToken === "") &&
360
- hasRewardAmounts(s.rewardsInfo)) {
361
- s.rewardsInfo.rewardToken = predictedGovToken;
362
- }
363
- }
364
- }
365
- // Always wire predicted distribution + tokenSale into additionalProposalExecutors
366
- const effectiveExecutors = [...params.settingsParams.additionalProposalExecutors];
367
- if (predictedDistribution && !effectiveExecutors.includes(predictedDistribution))
368
- effectiveExecutors.push(predictedDistribution);
369
- if (predictedTokenSale && !effectiveExecutors.includes(predictedTokenSale))
370
- effectiveExecutors.push(predictedTokenSale);
371
- // Validate: after wiring, at least one governance asset must be non-zero
372
- if (effectiveTokenAddress === ZERO && params.userKeeperParams.nftAddress === ZERO) {
373
- return errorResult("GovUK requires at least one governance asset: set userKeeperParams.tokenAddress (existing ERC20), userKeeperParams.nftAddress (existing ERC721), or provide tokenParams.name to create a new token");
374
- }
375
- // ---------- auto-expand proposal settings (1 → 5) ----------
376
- let expandedSettings = params.settingsParams.proposalSettings;
377
- if (expandedSettings.length === 1) {
378
- const base = expandedSettings[0];
379
- const baseWithValidatorFallback = {
380
- ...base,
381
- validatorsVote: true,
382
- durationValidators: hasValidators ? base.durationValidators : base.duration,
383
- quorumValidators: hasValidators ? base.quorumValidators : base.quorum,
384
- };
385
- const dpSettings = {
386
- ...baseWithValidatorFallback,
387
- delegatedVotingAllowed: false,
388
- earlyCompletion: false,
389
- };
390
- expandedSettings = [
391
- baseWithValidatorFallback, // [0] default
392
- baseWithValidatorFallback, // [1] internal
393
- baseWithValidatorFallback, // [2] validators
394
- dpSettings, // [3] distributionProposal
395
- baseWithValidatorFallback, // [4] tokenSale
396
- ];
397
- }
398
- else if (expandedSettings.length !== 5) {
399
- return errorResult(`proposalSettings must be 1 (auto-expand to 5) or exactly 5. Got ${expandedSettings.length}.`);
400
- }
401
- // ---------- treasury-safety advisory: quorum floor (Layer 2) ----------
402
- // Low quorum reduces the participation required to pass a proposal — a
403
- // governance-safety risk for a DAO that will hold treasury assets. Flag it
404
- // at birth so the operator sets an adequate quorum.
405
- let quorumWarning = "";
406
- if (ctx.config.treasuryGuard !== "off") {
407
- const floor = ctx.config.minSafeQuorumPct;
408
- const showPct = (p) => (Number.isFinite(p) ? `${p}%` : "unparseable");
409
- const lowQuorum = [];
410
- expandedSettings.forEach((s, i) => {
411
- const pct = quorumPctFromRaw(s.quorum);
412
- if (judgeQuorum(pct, floor) !== "SAFE") {
413
- lowQuorum.push(`proposalSettings[${i}] quorum=${showPct(pct)}`);
414
- }
415
- });
416
- const vpct = quorumPctFromRaw(validatorsParams.proposalSettings.quorum);
417
- if (judgeQuorum(vpct, floor) !== "SAFE") {
418
- lowQuorum.push(`validatorsParams quorum=${showPct(vpct)}`);
419
- }
420
- if (lowQuorum.length > 0) {
421
- const detail = `Quorum below the ${floor}% safe floor (DEXE_MIN_SAFE_QUORUM_PCT): ${lowQuorum.join(", ")}. ` +
422
- `Low quorum reduces the participation required to pass a proposal; for a DAO that will hold ` +
423
- `treasury assets, set quorum ≥50% (51%+ recommended). The safe value is DAO-specific and must ` +
424
- `be verified. [governance-safety advisory]`;
425
- // Advisory only — never blocks the deploy. The durable control is an
426
- // adequate on-chain quorum threshold configured per DAO.
427
- quorumWarning = `\n⚠️ ${detail}`;
428
- }
429
- }
430
- // ---------- auto-upload executorDescription to IPFS (when Pinata configured) ----------
431
- // The frontend uploads each proposal settings object as JSON to IPFS and stores
432
- // the CID as `executorDescription`. Without this, DAOs display broken metadata in
433
- // the UI. We replicate the frontend's behavior: for each settings slot, if
434
- // executorDescription is empty and Pinata is available, upload the settings JSON
435
- // and set executorDescription = "ipfs://<cid>".
436
- //
437
- // The uploaded JSON shape matches the frontend exactly:
438
- // { earlyCompletion, delegatedVotingAllowed, validatorsVote, duration, durationValidators,
439
- // quorum, quorumValidators, minVotesForVoting, minVotesForCreating, executionDelay,
440
- // rewardsInfo: { rewardToken, creationReward, executionReward, voteRewardsCoefficient } }
441
- let pinataWarning = "";
442
- if (ctx.config.pinataJwt) {
443
- const pinata = new PinataClient(ctx.config.pinataJwt);
444
- // We only auto-upload for default (index 0) and DP (index 3) settings,
445
- // matching the frontend which shares the same CID across default/internal/validators/tokenSale
446
- // and a separate CID for DP settings.
447
- const settingsToUpload = [
448
- { index: 0, label: "default" },
449
- { index: 3, label: "distributionProposal" },
450
- ];
451
- for (const { index, label } of settingsToUpload) {
452
- const s = expandedSettings[index];
453
- if (!s.executorDescription || s.executorDescription === "") {
454
- try {
455
- // Bug #13: Frontend includes minVotesForReadProposalDiscussion and
456
- // minVotesForCreatingComment in the settings JSON uploaded to IPFS.
457
- // Defaults from frontend: 0 (read) and 1e18 (comment = 1 token).
458
- const settingsJson = {
459
- earlyCompletion: s.earlyCompletion,
460
- delegatedVotingAllowed: s.delegatedVotingAllowed,
461
- validatorsVote: s.validatorsVote,
462
- duration: s.duration,
463
- durationValidators: s.durationValidators,
464
- quorum: s.quorum,
465
- quorumValidators: s.quorumValidators,
466
- minVotesForVoting: s.minVotesForVoting,
467
- minVotesForCreating: s.minVotesForCreating,
468
- executionDelay: s.executionDelay,
469
- rewardsInfo: {
470
- rewardToken: s.rewardsInfo.rewardToken,
471
- creationReward: s.rewardsInfo.creationReward,
472
- executionReward: s.rewardsInfo.executionReward,
473
- voteRewardsCoefficient: s.rewardsInfo.voteRewardsCoefficient,
474
- },
475
- minVotesForReadProposalDiscussion: "0",
476
- minVotesForCreatingComment: "1000000000000000000",
477
- };
478
- const res = await pinata.pinJson(settingsJson, {
479
- name: `dao-settings-${label}:${params.name.slice(0, 40)}`,
480
- });
481
- const cid = `ipfs://${res.cid}`;
482
- // Apply the CID to the relevant slots (frontend shares CIDs across slots)
483
- if (index === 0) {
484
- // Default CID shared across: default(0), internal(1), validators(2), tokenSale(4)
485
- expandedSettings[0] = { ...expandedSettings[0], executorDescription: cid };
486
- expandedSettings[1] = { ...expandedSettings[1], executorDescription: cid };
487
- expandedSettings[2] = { ...expandedSettings[2], executorDescription: cid };
488
- expandedSettings[4] = { ...expandedSettings[4], executorDescription: cid };
489
- }
490
- else {
491
- // DP settings get their own CID
492
- expandedSettings[3] = { ...expandedSettings[3], executorDescription: cid };
493
- }
494
- }
495
- catch (err) {
496
- pinataWarning += `\n⚠️ Failed to upload ${label} executorDescription: ${err instanceof Error ? err.message : String(err)}`;
497
- }
498
- }
499
- }
500
- }
501
- else {
502
- // Check if any executorDescription is missing — warn about UI impact
503
- const missingDesc = expandedSettings.some((s) => !s.executorDescription || s.executorDescription === "");
504
- if (missingDesc) {
505
- pinataWarning =
506
- "\n⚠️ DEXE_PINATA_JWT not configured — executorDescription fields are empty. " +
507
- "DAOs created without executorDescription will display broken proposal settings in the frontend UI. " +
508
- "Set DEXE_PINATA_JWT to enable auto-upload.";
509
- }
510
- }
511
- // ---------- build the tuple arg ----------
512
- const paramsTuple = [
513
- // settingsParams
514
- [
515
- expandedSettings.map((s) => [
516
- s.earlyCompletion,
517
- s.delegatedVotingAllowed,
518
- s.validatorsVote,
519
- BigInt(s.duration),
520
- BigInt(s.durationValidators),
521
- BigInt(s.executionDelay),
522
- BigInt(s.quorum),
523
- BigInt(s.quorumValidators),
524
- BigInt(s.minVotesForVoting),
525
- BigInt(s.minVotesForCreating),
526
- [
527
- s.rewardsInfo.rewardToken,
528
- BigInt(s.rewardsInfo.creationReward),
529
- BigInt(s.rewardsInfo.executionReward),
530
- BigInt(s.rewardsInfo.voteRewardsCoefficient),
531
- ],
532
- s.executorDescription,
533
- ]),
534
- effectiveExecutors,
535
- ],
536
- // validatorsParams
537
- [
538
- validatorsParams.name,
539
- validatorsParams.symbol,
540
- [
541
- BigInt(validatorsParams.proposalSettings.duration),
542
- BigInt(validatorsParams.proposalSettings.executionDelay),
543
- BigInt(validatorsParams.proposalSettings.quorum),
544
- ],
545
- validatorsParams.validators,
546
- validatorsParams.balances.map((b) => BigInt(b)),
547
- ],
548
- // userKeeperParams
549
- [
550
- effectiveTokenAddress,
551
- params.userKeeperParams.nftAddress,
552
- BigInt(params.userKeeperParams.individualPower),
553
- BigInt(params.userKeeperParams.nftsTotalSupply),
554
- ],
555
- // tokenParams
556
- [
557
- params.tokenParams.name,
558
- params.tokenParams.symbol,
559
- params.tokenParams.users,
560
- BigInt(params.tokenParams.cap),
561
- BigInt(params.tokenParams.mintedTotal),
562
- params.tokenParams.amounts.map((a) => BigInt(a)),
563
- ],
564
- // votePowerParams
565
- [
566
- VOTE_POWER_TYPES.indexOf(params.votePowerParams.voteType), // uint8 enum
567
- votePowerInitData,
568
- params.votePowerParams.presetAddress,
569
- ],
570
- params.verifier,
571
- params.onlyBABTHolders,
572
- params.descriptionURL.replace(/^ipfs:\/\//, ""),
573
- params.name,
574
- ];
575
- // ---------- get ABI (artifact > fallback) ----------
576
- let iface;
577
- let ifaceSource;
578
- try {
579
- const records = ctx.artifacts.get("PoolFactory");
580
- if (records.length > 0) {
581
- iface = new Interface(records[0].abi);
582
- ifaceSource = "compiled artifact";
583
- // Sanity: ensure it has deployGovPool
584
- try {
585
- iface.getFunction("deployGovPool");
586
- }
587
- catch {
588
- iface = new Interface(FALLBACK_POOL_FACTORY_ABI);
589
- ifaceSource = "fallback (compiled artifact missing deployGovPool)";
590
- }
591
- }
592
- else {
593
- iface = new Interface(FALLBACK_POOL_FACTORY_ABI);
594
- ifaceSource = "fallback (artifacts not loaded — run dexe_compile for strict parity)";
595
- }
596
- }
597
- catch (err) {
598
- if (err instanceof ArtifactsMissingError) {
599
- iface = new Interface(FALLBACK_POOL_FACTORY_ABI);
600
- ifaceSource = "fallback (no artifacts — run dexe_compile for strict parity)";
601
- }
602
- else {
603
- return errorResult(`Failed to load PoolFactory ABI: ${err instanceof Error ? err.message : String(err)}`);
604
- }
605
- }
606
- // ---------- encode ----------
607
- let payload;
608
- try {
609
- payload = buildPayload({
610
- to: factoryAddress,
611
- iface,
612
- method: "deployGovPool",
613
- args: [paramsTuple],
614
- chainId: chain.chainId,
615
- contractLabel: "PoolFactory",
616
- description: `PoolFactory.deployGovPool("${params.name}") via ${ifaceSource}`,
617
- });
618
- }
619
- catch (err) {
620
- return errorResult(`deployGovPool encoding failed: ${err instanceof Error ? err.message : String(err)}`);
621
- }
622
- let note = ifaceSource.includes("fallback")
623
- ? "⚠️ Using fallback tuple ABI. For guaranteed parity, run dexe_compile to populate artifacts."
624
- : "Encoded against compiled artifact — strict parity with deployed PoolFactory.";
625
- if (pinataWarning)
626
- note += pinataWarning;
627
- if (quorumWarning)
628
- note += quorumWarning;
629
- return payloadResult(payload, { predictedGovPool, note });
597
+ const res = await buildDeployGovPool({ chainId, poolFactory, deployer, params }, ctx, rpc);
598
+ if (!res.ok)
599
+ return errorResult(res.error);
600
+ return payloadResult(res.payload, { predictedGovPool: res.predictedGovPool, note: res.note });
630
601
  });
631
602
  }
632
603
  //# sourceMappingURL=daoDeploy.js.map