@stabilitydao/host 0.2.1

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.
package/src/host.ts ADDED
@@ -0,0 +1,1212 @@
1
+ /**
2
+ Host prototype.
3
+ */
4
+
5
+ import { ChainName, chains, getChainByName } from "./chains";
6
+ import { IAgent } from "./agents";
7
+ import { IBuilderActivity } from "./activity/builder";
8
+ import { Activity } from "./activity";
9
+ import { IDAOAPIData } from "./api";
10
+
11
+ export const HOST_DESCRIPTION = "A Cozy Home for DAOs";
12
+
13
+ /**
14
+ Represents a DAO running on Host.
15
+
16
+ todo: Optimize for cross-chain
17
+ host-contracts: `IHost.DAOData`
18
+
19
+ @version 0.2.0
20
+ @alpha
21
+ @interface
22
+ */
23
+ export interface IDAOData {
24
+ /** SEGMENT 1: ON-CHAIN on all chains where Host deployed */
25
+
26
+ /**
27
+ * Tradeable interchain ERC-20 token symbol.
28
+ * Lowercased used as slug.
29
+ * While token symbol is SYM then additional DAO tokens symbols are:
30
+ * seedSYM, saleSYM, xSYM, SYM_DAO
31
+ *
32
+ * host-contracts: HostLib.OsStorage.usedSymbols
33
+ */
34
+ symbol: string;
35
+
36
+ /** SEGMENT 2: ON-CHAIN on chains where DAO bridged */
37
+
38
+ /** Unique ID of DAO */
39
+ uid?: string;
40
+
41
+ /** Name of the DAO, used in token names. Without DAO word. */
42
+ name: string;
43
+
44
+ /**
45
+ DAO lifecycle phase.
46
+ Changes permissionless when next phase start timestamp reached.
47
+ */
48
+ phase: LifecyclePhase;
49
+
50
+ /**
51
+ Deployed smart-contracts.
52
+ host-contracts: deployments of current instance chain only.
53
+ */
54
+ deployments: IDAODeployments;
55
+
56
+ /** Settings of DAO for current chain. This is the only place to save settings of DAO for chains. */
57
+ chainSettings: IDAOChainSettings;
58
+
59
+ /** IDs of Units running on current chain */
60
+ unitIds?: string[];
61
+
62
+ /** On-chain DAO parameters for tokenomics and revenue sharing */
63
+ params: IDAOParameters;
64
+
65
+ /** SEGMENT 3: ON-CHAIN on initial chain of DAO */
66
+
67
+ /** Where initial deployment became */
68
+ initialChain: ChainName;
69
+
70
+ /** Community socials. Update by `Host.updateSocials` */
71
+ socials: string[];
72
+
73
+ /** Activities of the organization. */
74
+ activity: Activity[];
75
+
76
+ /** Images of tokens. Absolute or relative from stabilitydao/.github repo /os/ folder. */
77
+ images: IDAOImages;
78
+
79
+ /** Revenue generating units owned by the organization. */
80
+ units: IUnit[];
81
+
82
+ /** Fundraising */
83
+ funding: IFunding[];
84
+
85
+ /** Vesting allocations */
86
+ vesting: IVesting[];
87
+
88
+ /** Settings of DAO Governance */
89
+ governanceSettings: IGovernanceSettings;
90
+
91
+ /** Deployer of a DAO have power only at DRAFT phase. */
92
+ deployer: string;
93
+
94
+ /** DAO custom metadata stored off-chain. */
95
+ daoMetaDataLocation?: string; // "local","https://..."
96
+
97
+ /** SEGMENT 4: OFF-CHAIN emitted data */
98
+
99
+ unitsMetaData: IUnitMetaData[];
100
+
101
+ /** SEGMENT 5: OFF-CHAIN custom data managed by DAO */
102
+
103
+ /** Storage for BUILDER activity and Agents data. */
104
+ daoMetaData?: IDAOMetaData;
105
+
106
+ /** SEGMENT 6: API data of DAO */
107
+
108
+ /** Hot data updates each minute */
109
+ api?: IDAOAPIData;
110
+ }
111
+
112
+ export interface IDAOMetaData {
113
+ /** DAOs engaging BUILDER activity settings */
114
+ builderActivity?: IBuilderActivity;
115
+
116
+ /** Operating agents managed by the organization. */
117
+ agents?: IAgent[];
118
+ }
119
+
120
+ /** Images of tokens. Absolute or relative from stabilitydao/.github repo /os/ folder. */
121
+ export interface IDAOImages {
122
+ seedToken?: string;
123
+ tgeToken?: string;
124
+ token?: string;
125
+ xToken?: string;
126
+ daoToken?: string;
127
+ }
128
+
129
+ /**
130
+ Lifecycle phase represents DAO tokenomics stage.
131
+ */
132
+ export enum LifecyclePhase {
133
+ /** Created */
134
+ DRAFT = "DRAFT",
135
+
136
+ /**
137
+ Initial funding. DAO project passed requirements.
138
+ Since SEED started a DAO become real DAO:
139
+ - noncustodial
140
+ - tokenized share holdings
141
+ - collective management via voting
142
+ */
143
+ SEED = "SEED",
144
+
145
+ /** Seed was not success. Raised funds sent back to seeders. */
146
+ SEED_FAILED = "SEED_FAILED",
147
+
148
+ /** Using SEED funds to launch MVP / Unit generating */
149
+ DEVELOPMENT = "DEVELOPMENT",
150
+
151
+ /** TGE is funding event for token liquidity and DAO developments (optionally) */
152
+ TGE = "TGE",
153
+
154
+ /** Delay before any vesting allocation started */
155
+ LIVE_CLIFF = "LIVE_CLIFF",
156
+
157
+ /** Vesting period active */
158
+ LIVE_VESTING = "LIVE_VESTING",
159
+
160
+ /** Vesting ended - token fully distributed */
161
+ LIVE = "LIVE",
162
+ }
163
+
164
+ /**
165
+ Parameters of VE-tokenomics and revenue sharing.
166
+ @interface
167
+ */
168
+ export interface IDAOParameters {
169
+ /** Vested Escrow period, days. */
170
+ vePeriod: number;
171
+ /** Instant exit fee, percent */
172
+ pvpFee: number;
173
+ /** Minimal power (min stake amount) to be a holder of DAO */
174
+ minPower?: number;
175
+ /** Share of total DAO revenue going to accidents compensations, percent */
176
+ recoveryShare?: number;
177
+ }
178
+
179
+ export interface IDAOChainSettings {
180
+ bbRate: number;
181
+ }
182
+
183
+ export interface IGovernanceSettings {
184
+ /** Minimal total voting power (self and delegated) need to create a proposal */
185
+ proposalThreshold?: number;
186
+ /** Bribe share for Tokenomics Transactions (vested funds spending), percent */
187
+ ttBribe?: number;
188
+ }
189
+
190
+ export interface IFunding {
191
+ type: FundingType;
192
+ start: number;
193
+ end: number;
194
+ minRaise: number;
195
+ maxRaise: number;
196
+ raised: number;
197
+ claim?: number;
198
+ }
199
+
200
+ export enum FundingType {
201
+ SEED = "SEED",
202
+ TGE = "TGE",
203
+ }
204
+
205
+ /**
206
+ Vesting allocation data
207
+ @interface
208
+ */
209
+ export interface IVesting {
210
+ /** Short name of vesting allocation */
211
+ name: string;
212
+ /** How must be spent */
213
+ description?: string;
214
+ /** Vesting supply. 10 == 10e18 TOKEN */
215
+ allocation: number;
216
+ /** Start timestamp */
217
+ start: number;
218
+ /** End timestamp */
219
+ end: number;
220
+ }
221
+
222
+ /**
223
+ Deployments of running DAO on blockchains.
224
+
225
+ @interface
226
+ */
227
+ export interface IDAODeployments {
228
+ [chainId: string]: {
229
+ /** Seed round receipt token. */
230
+ seedToken?: `0x${string}`;
231
+ /** TGE pre-sale receipt token. */
232
+ tgeToken?: `0x${string}`;
233
+ /** Main tradable DAO token. */
234
+ token?: `0x${string}`;
235
+ /** VE-tokenomics entry token. */
236
+ xToken?: `0x${string}`;
237
+ /** Staking contract. */
238
+ staking?: `0x${string}`;
239
+ /** Governance token. */
240
+ daoToken?: `0x${string}`;
241
+ /** Revenue utilization and distributing contract. */
242
+ revenueRouter?: `0x${string}`;
243
+ /** Accident recovery system contract. */
244
+ recovery?: `0x${string}`;
245
+ /** Set of vesting contracts. */
246
+ vesting?: { [name: string]: `0x${string}` };
247
+ /** Bridge for Token */
248
+ tokenBridge?: `0x${string}`;
249
+ /** Bridge for XToken */
250
+ xTokenBridge?: `0x${string}`;
251
+ /** Bridge for Governance token */
252
+ daoTokenBridge?: `0x${string}`;
253
+ };
254
+ }
255
+
256
+ /**
257
+ Revenue generating unit owned by a DAO.
258
+ @interface
259
+ */
260
+ export interface IUnit {
261
+ /** Unique unit string id. For DeFi protocol its defiOrg:protocolKey. */
262
+ unitId: string;
263
+ /** Blockchains where Unit deployed. Filled only for initial DAO chain Host instance. */
264
+ chainIds?: string[];
265
+ /** DAO UID of Unit Developer (Pool tasks solver) */
266
+ developerUid?: string;
267
+ }
268
+
269
+ /** Unit data that emitted, indexed and saved, translated by Host API later. */
270
+ export interface IUnitMetaData {
271
+ /** Short name of the unit */
272
+ name: string;
273
+ /** Status of unit changes appear when unit starting to work and starting earning revenue */
274
+ status: UnitStatus;
275
+ /** Supported type of the Unit */
276
+ type: UnitType;
277
+ /** The share of a Unit's profit received by the DAO to which it belongs. 100 - 100%. */
278
+ revenueShare: number;
279
+ /** A unique emoji for the shortest possible representation of a Unit. */
280
+ emoji?: string;
281
+ /** Frontend endpoints of Unit */
282
+ ui?: IUnitUILink[];
283
+ /** Links to API of the Unit */
284
+ api?: string[];
285
+ /** Components of the Unit. */
286
+ //components?: { [category in UnitComponentCategory]?: UnitComponent[] };
287
+ }
288
+
289
+ /** Supported unit types */
290
+ export enum UnitType {
291
+ /** VE-token early exit fees */
292
+ PVP = "PVP",
293
+ /** Decentralized finance protocol */
294
+ DEFI_PROTOCOL = "DEFI_PROTOCOL",
295
+ /** Maximum Extractable Value opportunities searcher and submitter. */
296
+ MEV_SEARCHER = "MEV_SEARCHER",
297
+ /** Software as a Service business */
298
+ //SAAS = "SAAS",
299
+ }
300
+
301
+ /** Unit status can be changed automatically on DAO lifecycle phase changes or manually by DAO holders */
302
+ export enum UnitStatus {
303
+ RESEARCH = "RESEARCH",
304
+ BUILDING = "BUILDING",
305
+ LIVE = "LIVE",
306
+ }
307
+
308
+ /** Supported categories of running units. */
309
+ export enum UnitComponentCategory {
310
+ CHAIN_SUPPORT = "CHAIN_SUPPORT",
311
+ ENGINE_SUPPORT = "ENGINE_SUPPORT",
312
+ DEFI_STRATEGY = "DEFI_STRATEGY",
313
+ MEV_STRATEGY = "MEV_STRATEGY",
314
+ }
315
+
316
+ export interface IUnitUILink {
317
+ href: `https://${string}`;
318
+ title: string;
319
+ }
320
+
321
+ //export type UnitComponent = StrategyShortId | ChainName | LendingEngine;
322
+
323
+ /**
324
+ Typescript implementation of the Host
325
+ Object of this class is Host instance deployed on a single blockchain.
326
+
327
+ @class
328
+ */
329
+ export class Host {
330
+ /** Chain ID where instance deployed */
331
+ chainId: string;
332
+
333
+ /** Chain block timestamp */
334
+ blockTimestamp: number = Math.floor(new Date().getTime() / 1000);
335
+
336
+ /** Local DAOs storage (in form of a mapping) */
337
+ daos: { [symbol: string]: IDAOData } = {};
338
+
339
+ /** Actual DAO symbols at all blockchains */
340
+ usedSymbols: { [name: string]: boolean } = {};
341
+
342
+ /** All emitted events */
343
+ events: string[] = [];
344
+
345
+ /** Governance proposals. Can be created only at initialChain of DAO. */
346
+ proposals: { [proposalId: string]: IProposal } = {};
347
+
348
+ /** Current user address */
349
+ from: string = "0x00";
350
+
351
+ settings: IOSSettings = {
352
+ priceDao: 1000,
353
+ priceUnit: 1000,
354
+ priceOracle: 1000,
355
+ priceBridge: 1000,
356
+ minNameLength: 1,
357
+ maxNameLength: 20,
358
+ minSymbolLength: 1,
359
+ maxSymbolLength: 7,
360
+ minVePeriod: 14,
361
+ maxVePeriod: 365 * 4,
362
+ minPvPFee: 10,
363
+ maxPvPFee: 100,
364
+ minFundingDuration: 1,
365
+ maxFundingDuration: 180,
366
+ };
367
+
368
+ constructor(chainId: string) {
369
+ this.chainId = chainId;
370
+ }
371
+
372
+ static getTokensNaming(name: string, symbol: string) {
373
+ return {
374
+ seedName: `${name} SEED`,
375
+ seedSymbol: `seed${symbol}`,
376
+ tgeName: `${name} PRESALE`,
377
+ tgeSymbol: `sale${symbol}`,
378
+ tokenName: name,
379
+ tokenSymbol: symbol,
380
+ xName: `x${name}`,
381
+ xSymbol: `x${symbol}`,
382
+ daoName: `${name} DAO`,
383
+ daoSymbol: `${symbol}_DAO`,
384
+ };
385
+ }
386
+
387
+ static isLiveDAO(phase: LifecyclePhase) {
388
+ return [
389
+ LifecyclePhase.LIVE_CLIFF,
390
+ LifecyclePhase.LIVE_VESTING,
391
+ LifecyclePhase.LIVE,
392
+ ].includes(phase);
393
+ }
394
+
395
+ /**
396
+ * Create new DAO
397
+ * @throws Error
398
+ */
399
+ createDAO(
400
+ name: string,
401
+ symbol: string,
402
+ activity: Activity[],
403
+ params: IDAOParameters,
404
+ funding: IFunding[],
405
+ metaDataLocation?: string,
406
+ ): IDAOData {
407
+ const dao: IDAOData = {
408
+ phase: LifecyclePhase.DRAFT,
409
+ name,
410
+ symbol,
411
+ activity,
412
+ socials: [],
413
+ images: {},
414
+ deployments: {},
415
+ units: [],
416
+ params,
417
+ chainSettings: {
418
+ bbRate: 50,
419
+ },
420
+ initialChain: chains[this.chainId].name,
421
+ funding,
422
+ vesting: [],
423
+ governanceSettings: {},
424
+ deployer: this.from,
425
+ daoMetaDataLocation: metaDataLocation,
426
+ unitsMetaData: [],
427
+ };
428
+
429
+ this.validate(dao);
430
+
431
+ this.daos[dao.symbol] = dao;
432
+ this.usedSymbols[dao.symbol] = true;
433
+ this._emit("DAO created");
434
+ this._sendCrossChainMessage(CROSS_CHAIN_MESSAGE.NEW_DAO_SYMBOL, {
435
+ symbol,
436
+ });
437
+ return dao;
438
+ }
439
+
440
+ /** Add live compatible DAO */
441
+ addLiveDAO(dao: IDAOData) {
442
+ // todo _onlyVerifier
443
+ this.validate(dao);
444
+ this.daos[dao.symbol] = dao;
445
+ this.usedSymbols[dao.symbol] = true;
446
+ this._emit("DAO created");
447
+ this._sendCrossChainMessage(CROSS_CHAIN_MESSAGE.NEW_DAO_SYMBOL, {
448
+ symbol: dao.symbol,
449
+ });
450
+ }
451
+
452
+ getDAOMetaData(
453
+ daoMetaData: { [symbolLowerCase: string]: IDAOMetaData },
454
+ symbol: string,
455
+ ): IDAOMetaData {
456
+ const dao = this.getDAO(symbol);
457
+ if (dao.daoMetaDataLocation === "local") {
458
+ return daoMetaData[symbol.toLowerCase()] as IDAOMetaData;
459
+ }
460
+ return {};
461
+ }
462
+
463
+ /** Change lifecycle phase of a DAO */
464
+ changePhase(symbol: string) {
465
+ // anybody can call this
466
+
467
+ const dao = this.getDAO(symbol);
468
+ const currentTasks = this.tasks(symbol);
469
+ if (currentTasks.length > 0) {
470
+ throw new Error("SolveTasksFirst");
471
+ }
472
+
473
+ if (dao.phase === LifecyclePhase.DRAFT) {
474
+ const seed = dao.funding[this.getFundingIndex(symbol, FundingType.SEED)];
475
+ if (seed.start > this.blockTimestamp) {
476
+ throw new Error("WaitFundingStart");
477
+ }
478
+ // SEED can be started not later than 1 week after must start
479
+ // todo settings.maxSeedStartDelay
480
+ if (
481
+ seed.start < this.blockTimestamp &&
482
+ this.blockTimestamp - seed.start > 7 * 86400
483
+ ) {
484
+ throw new Error("TooLateSoSetupFundingAgain");
485
+ }
486
+ /*// SEED can be started not later than 1 week before end
487
+ if (seed.end - this.blockTimestamp < 7 * 86400) {
488
+ throw new Error("TooLateSoSetupFundingAgain")
489
+ }*/
490
+
491
+ // deploy seedToken
492
+ this.daos[symbol].deployments[this.chainId] = {
493
+ seedToken: "0xProxyDeployed",
494
+ };
495
+
496
+ this.daos[symbol].phase = LifecyclePhase.SEED;
497
+ } else if (dao.phase === LifecyclePhase.SEED) {
498
+ const seed = dao.funding[this.getFundingIndex(symbol, FundingType.SEED)];
499
+ if (seed.end > this.blockTimestamp) {
500
+ throw new Error("WaitFundingEnd");
501
+ }
502
+
503
+ const sucess = seed.raised >= seed.minRaise;
504
+
505
+ if (sucess) {
506
+ this.daos[symbol].phase = LifecyclePhase.DEVELOPMENT;
507
+ } else {
508
+ // send all raised back to seeders
509
+
510
+ this.daos[symbol].phase = LifecyclePhase.SEED_FAILED;
511
+ }
512
+ } else if (dao.phase === LifecyclePhase.DEVELOPMENT) {
513
+ const tge = dao.funding[this.getFundingIndex(symbol, FundingType.TGE)];
514
+ if (tge.start > this.blockTimestamp) {
515
+ throw new Error("WaitFundingStart");
516
+ }
517
+
518
+ // deploy tgeToken
519
+ this.daos[symbol].deployments[this.chainId].tgeToken =
520
+ "0xProxyDeployedTge";
521
+
522
+ this.daos[symbol].phase = LifecyclePhase.TGE;
523
+ } else if (dao.phase === LifecyclePhase.TGE) {
524
+ const tge = dao.funding[this.getFundingIndex(symbol, FundingType.TGE)];
525
+
526
+ if (tge.end > this.blockTimestamp) {
527
+ throw new Error("WaitFundingEnd");
528
+ }
529
+
530
+ const success = tge.raised >= tge.minRaise;
531
+
532
+ if (success) {
533
+ // deploy token, xToken, staking, daoToken
534
+ this.daos[symbol].deployments[this.chainId].token = "0xProxyToken";
535
+ this.daos[symbol].deployments[this.chainId].xToken = "0xProxyXToken";
536
+ this.daos[symbol].deployments[this.chainId].staking = "0xProxyStaking";
537
+ this.daos[symbol].deployments[this.chainId].daoToken =
538
+ "0xProxyDAOToken";
539
+
540
+ // todo deploy vesting contracts and allocate token
541
+
542
+ // todo seedToken holders became xToken holders by predefined rate
543
+
544
+ // todo deploy v2 liquidity from TGE funds at predefined price
545
+
546
+ this.daos[symbol].phase = LifecyclePhase.LIVE_CLIFF;
547
+ } else {
548
+ // send all raised TGE funds back to funders
549
+
550
+ this.daos[symbol].phase = LifecyclePhase.DEVELOPMENT;
551
+ }
552
+ } else if (dao.phase === LifecyclePhase.LIVE_CLIFF) {
553
+ // if any vesting started then phase changed
554
+ const isVestingStarted = !!dao.vesting?.filter(
555
+ (v) => v.start < this.blockTimestamp,
556
+ ).length;
557
+ if (!isVestingStarted) {
558
+ throw new Error("WaitVestingStart");
559
+ }
560
+
561
+ this.daos[symbol].phase = LifecyclePhase.LIVE_VESTING;
562
+ } else if (dao.phase === LifecyclePhase.LIVE_VESTING) {
563
+ // if any vesting started then phase changed
564
+ const isVestingEnded = !dao.vesting?.filter(
565
+ (v) => v.end > this.blockTimestamp,
566
+ ).length;
567
+ if (!isVestingEnded) {
568
+ throw new Error("WaitVestingEnd");
569
+ }
570
+
571
+ this.daos[symbol].phase = LifecyclePhase.LIVE;
572
+ } else {
573
+ // nothing to change
574
+ throw new Error("ForeverLive");
575
+ }
576
+ }
577
+
578
+ /** @throws Error */
579
+ updateImages(symbol: string, images: IDAOImages) {
580
+ // check DAO symbol
581
+ const dao = this.getDAO(symbol);
582
+
583
+ // instant execute for DRAFT
584
+ if (dao.phase === LifecyclePhase.DRAFT) {
585
+ this._onlyOwnerOf(symbol);
586
+ this._updateImages(symbol, images);
587
+ return true;
588
+ }
589
+
590
+ // create proposal for other phases
591
+ return this._proposeAction(symbol, DAOAction.UPDATE_IMAGES, {
592
+ images,
593
+ });
594
+ }
595
+
596
+ /** @throws Error */
597
+ updateSocials(symbol: string, socials: string[]) {
598
+ // check DAO symbol
599
+ const dao = this.getDAO(symbol);
600
+
601
+ // instant execute for DRAFT
602
+ if (dao.phase === LifecyclePhase.DRAFT) {
603
+ this._onlyOwnerOf(symbol);
604
+ this._updateSocials(symbol, socials);
605
+ return true;
606
+ }
607
+
608
+ // create proposal for other phases
609
+ return this._proposeAction(symbol, DAOAction.UPDATE_SOCIALS, {
610
+ socials,
611
+ });
612
+ }
613
+
614
+ /** @throws Error */
615
+ updateUnits(
616
+ symbol: string,
617
+ units: IUnit[],
618
+ unitsMetaData: IUnitMetaData[],
619
+ ): string | true {
620
+ // check DAO symbol
621
+ const dao = this.getDAO(symbol);
622
+
623
+ // instant execute for DRAFT
624
+ if (dao.phase === LifecyclePhase.DRAFT) {
625
+ this._onlyOwnerOf(symbol);
626
+ this._updateUnits(symbol, units, unitsMetaData);
627
+ return true;
628
+ }
629
+
630
+ // create proposal for other phases
631
+ return this._proposeAction(symbol, DAOAction.UPDATE_UNITS, {
632
+ units,
633
+ unitsMetaData,
634
+ });
635
+ }
636
+
637
+ /** @throws Error */
638
+ updateFunding(symbol: string, funding: IFunding): string | true {
639
+ // check DAO symbol
640
+ const dao = this.getDAO(symbol);
641
+
642
+ // validate payload
643
+ this._validateFunding(dao.phase, [funding]);
644
+
645
+ // instant execute for DRAFT
646
+ if (dao.phase === LifecyclePhase.DRAFT) {
647
+ this._onlyOwnerOf(symbol);
648
+ this._updateFunding(symbol, funding);
649
+ return true;
650
+ }
651
+
652
+ // create proposal for other phases
653
+ return this._proposeAction(symbol, DAOAction.UPDATE_FUNDING, {
654
+ funding,
655
+ });
656
+ }
657
+
658
+ private _updateSocials(symbol: string, socials: string[]) {
659
+ this.daos[symbol].socials = socials;
660
+ this._emit(`Action ${DAOAction.UPDATE_SOCIALS}`);
661
+ }
662
+
663
+ private _updateUnits(
664
+ symbol: string,
665
+ units: IUnit[],
666
+ unitsMetaData: IUnitMetaData[],
667
+ ) {
668
+ this.daos[symbol].units = units;
669
+ this.daos[symbol].unitsMetaData = unitsMetaData;
670
+ this._emit(`Action ${DAOAction.UPDATE_UNITS}`);
671
+ }
672
+
673
+ private _updateFunding(symbol: string, funding: IFunding) {
674
+ const dao = this.getDAO(symbol);
675
+
676
+ const fundingExist =
677
+ dao.funding.filter((f) => f.type === funding.type).length === 1;
678
+ if (fundingExist) {
679
+ const fundingIndex = this.getFundingIndex(symbol, funding.type);
680
+ this.daos[symbol].funding[fundingIndex] = funding;
681
+ } else {
682
+ this.daos[symbol].funding.push(funding);
683
+ }
684
+
685
+ this._emit(`Action ${DAOAction.UPDATE_FUNDING}`);
686
+ }
687
+
688
+ updateVesting(symbol: string, vestings: IVesting[]) {
689
+ // check DAO symbol
690
+ const dao = this.getDAO(symbol);
691
+
692
+ // validate
693
+ this._validateVesting(dao.phase, vestings);
694
+
695
+ // instant execute for DRAFT
696
+ if (dao.phase === LifecyclePhase.DRAFT) {
697
+ this._onlyOwnerOf(symbol);
698
+ this._updateVesting(symbol, vestings);
699
+ return true;
700
+ }
701
+
702
+ // create proposal for other phases
703
+ return this._proposeAction(symbol, DAOAction.UPDATE_VESTING, {
704
+ vestings,
705
+ });
706
+ }
707
+
708
+ fund(symbol: string, amount: number) {
709
+ // todo settings.minFunding
710
+ const dao = this.getDAO(symbol);
711
+ if (dao.phase === LifecyclePhase.SEED) {
712
+ const seedIndex = this.getFundingIndex(symbol, FundingType.SEED);
713
+ const seed = dao.funding[seedIndex];
714
+ if (seed.raised + amount >= seed.maxRaise) {
715
+ throw new Error("RaiseMaxExceed");
716
+ }
717
+
718
+ // transfer amount of exchangeAsset to seedToken contract
719
+ this.daos[symbol].funding[seedIndex].raised += amount;
720
+
721
+ // mint seedToken to user
722
+
723
+ return;
724
+ }
725
+
726
+ if (dao.phase === LifecyclePhase.TGE) {
727
+ const tgeIndex = this.getFundingIndex(symbol, FundingType.TGE);
728
+ const tge = dao.funding[tgeIndex];
729
+ if (tge.raised + amount >= tge.maxRaise) {
730
+ throw new Error("RaiseMaxExceed");
731
+ }
732
+
733
+ // transfer amount of exchangeAsset to tgeToken contract
734
+
735
+ this.daos[symbol].funding[tgeIndex].raised += amount;
736
+
737
+ // mint tgeToken to user
738
+
739
+ return;
740
+ }
741
+
742
+ throw new Error("NotFundingPhase");
743
+ }
744
+
745
+ receiveVotingResults(proposalId: string, succeed: boolean) {
746
+ const proposal = this.proposals[proposalId];
747
+ if (!proposal) {
748
+ throw new Error("IncorrectProposal");
749
+ }
750
+ if (proposal.status !== VotingStatus.VOTING) {
751
+ throw new Error("AlreadyReceived");
752
+ }
753
+ this.proposals[proposalId].status = succeed
754
+ ? VotingStatus.APPROVED
755
+ : VotingStatus.REJECTED;
756
+
757
+ if (succeed) {
758
+ if (proposal.action === DAOAction.UPDATE_IMAGES) {
759
+ this._updateImages(proposal.symbol, proposal.payload.images);
760
+ }
761
+ if (proposal.action === DAOAction.UPDATE_SOCIALS) {
762
+ this._updateSocials(proposal.symbol, proposal.payload.socials);
763
+ }
764
+ if (proposal.action === DAOAction.UPDATE_UNITS) {
765
+ this._updateUnits(
766
+ proposal.symbol,
767
+ proposal.payload.units,
768
+ proposal.payload.unitsMetaData,
769
+ );
770
+ }
771
+ if (proposal.action === DAOAction.UPDATE_FUNDING) {
772
+ this._updateFunding(proposal.symbol, proposal.payload.funding);
773
+ }
774
+ if (proposal.action === DAOAction.UPDATE_VESTING) {
775
+ this._updateVesting(proposal.symbol, proposal.payload.vestings);
776
+ }
777
+ // todo other actions
778
+ }
779
+ }
780
+
781
+ /** OFF-CHAIN only **/
782
+ /** @throws Error */
783
+ roadmap(symbol: string): IRoadmapItem[] {
784
+ const dao: IDAOData = this.getDAO(symbol);
785
+ const r: IRoadmapItem[] = [];
786
+ let tgeRun = 0;
787
+
788
+ for (const funding of dao.funding) {
789
+ if (funding.type === FundingType.SEED) {
790
+ r.push({
791
+ phase: LifecyclePhase.SEED,
792
+ start: funding.start,
793
+ end: funding.end,
794
+ });
795
+ }
796
+ if (funding.type === FundingType.TGE) {
797
+ // if SEED was done
798
+ if (r.length > 0) {
799
+ r.push({
800
+ phase: LifecyclePhase.DEVELOPMENT,
801
+ start: (r[0].end as number) + 1,
802
+ end: funding.start - 1,
803
+ });
804
+ }
805
+
806
+ tgeRun = funding.claim || funding.end;
807
+ r.push({
808
+ phase: LifecyclePhase.TGE,
809
+ start: funding.start,
810
+ end: tgeRun,
811
+ });
812
+ }
813
+ }
814
+
815
+ if (dao.vesting.length > 0) {
816
+ let vestingStart = this.blockTimestamp;
817
+ let vestingEnd = this.blockTimestamp;
818
+ for (const vesting of dao.vesting) {
819
+ if (vesting.start < vestingStart) {
820
+ vestingStart = vesting.start;
821
+ }
822
+ if (vesting.end > vestingEnd) {
823
+ vestingEnd = vesting.end;
824
+ }
825
+ }
826
+ r.push({
827
+ phase: LifecyclePhase.LIVE_CLIFF,
828
+ start: tgeRun + 1,
829
+ end: vestingStart - 1,
830
+ });
831
+ r.push({
832
+ phase: LifecyclePhase.LIVE_VESTING,
833
+ start: vestingStart,
834
+ end: vestingEnd,
835
+ });
836
+ r.push({
837
+ phase: LifecyclePhase.LIVE,
838
+ start: vestingEnd + 1,
839
+ });
840
+ }
841
+
842
+ return r;
843
+ }
844
+
845
+ /** @throws Error */
846
+ tasks(symbol: string): ITask[] {
847
+ const dao: IDAOData = this.getDAO(symbol);
848
+ const r: ITask[] = [];
849
+
850
+ if (dao.phase === LifecyclePhase.DRAFT) {
851
+ // images
852
+ if (!dao.images.seedToken || !dao.images.token) {
853
+ r.push({
854
+ name: "Need images of token and seedToken",
855
+ });
856
+ }
857
+
858
+ // socials
859
+ if (dao.socials.length < 2) {
860
+ r.push({
861
+ name: "Need at least 2 socials",
862
+ });
863
+ }
864
+
865
+ // units projected
866
+ if (dao.units.length === 0) {
867
+ r.push({
868
+ name: "Need at least 1 projected unit",
869
+ });
870
+ }
871
+ } else if (dao.phase === LifecyclePhase.SEED) {
872
+ const seedIndex = this.getFundingIndex(symbol, FundingType.SEED);
873
+ if (
874
+ dao.funding[seedIndex].raised < dao.funding[seedIndex].minRaise &&
875
+ dao.funding[seedIndex].end > this.blockTimestamp
876
+ ) {
877
+ r.push({
878
+ name: "Need attract minimal seed funding",
879
+ });
880
+ }
881
+ } else if (dao.phase === LifecyclePhase.DEVELOPMENT) {
882
+ // check funding
883
+ const tgeExist =
884
+ dao.funding.filter((f) => f.type === FundingType.TGE).length === 1;
885
+ if (!tgeExist) {
886
+ r.push({
887
+ name: "Need add pre-TGE funding",
888
+ });
889
+ }
890
+
891
+ // images
892
+ if (!dao.images.tgeToken || !dao.images.xToken || !dao.images.daoToken) {
893
+ r.push({
894
+ name: "Need images of all DAO tokens",
895
+ });
896
+ }
897
+
898
+ // setup vesting allocations
899
+ if (!dao.vesting?.length) {
900
+ r.push({
901
+ name: "Need vesting allocations",
902
+ });
903
+ }
904
+
905
+ if (
906
+ dao.unitsMetaData?.filter(
907
+ (unitMetaData) => unitMetaData.status === UnitStatus.LIVE,
908
+ ).length === 0
909
+ ) {
910
+ r.push({
911
+ name: "Run revenue generating units",
912
+ });
913
+ }
914
+ } else if (dao.phase === LifecyclePhase.TGE) {
915
+ const tgeIndex = this.getFundingIndex(symbol, FundingType.TGE);
916
+ if (
917
+ dao.funding[tgeIndex].raised < dao.funding[tgeIndex].minRaise &&
918
+ dao.funding[tgeIndex].end > this.blockTimestamp
919
+ ) {
920
+ r.push({
921
+ name: "Need attract minimal TGE funding",
922
+ });
923
+ }
924
+ } else if (dao.phase === LifecyclePhase.LIVE_CLIFF) {
925
+ // establish and improve
926
+ // build money markets
927
+ // bridge to chains
928
+ } else if (dao.phase === LifecyclePhase.LIVE_VESTING) {
929
+ // distribute vesting funds to leverage token
930
+ }
931
+
932
+ /*if (dao.phase === LifecyclePhase.LIVE)*/
933
+ // lifetime revenue generating for DAO holders (till ABSORBED proposed feature)
934
+
935
+ return r;
936
+ }
937
+
938
+ /** Strict on-chain validation */
939
+ /** @throws Error */
940
+ validate(dao: IDAOData) {
941
+ this._validateName(dao.name);
942
+ this._validateSymbol(dao.symbol);
943
+ if (
944
+ dao.params.vePeriod < this.settings.minVePeriod ||
945
+ dao.params.vePeriod > this.settings.maxVePeriod
946
+ ) {
947
+ throw new Error(`VePeriod(${dao.params.vePeriod})`);
948
+ }
949
+ this._validatePvpFee(dao.params.pvpFee);
950
+ if (!dao.funding.length) {
951
+ throw new Error("NeedFunding");
952
+ }
953
+
954
+ // todo: check activity are correct
955
+ // todo: check funding array has unique funding types
956
+ // todo: check funding dates
957
+ // todo: check funding raise goals
958
+ }
959
+
960
+ /** @throws Error */
961
+ getDAO(symbol: string): IDAOData {
962
+ if (this.daos[symbol]) {
963
+ return this.daos[symbol];
964
+ }
965
+ throw new Error("DAONotFound");
966
+ }
967
+
968
+ getDaoOwner(symbol: string): string {
969
+ const dao = this.getDAO(symbol);
970
+
971
+ if (dao.phase === LifecyclePhase.DRAFT) {
972
+ return dao.deployer;
973
+ }
974
+
975
+ if (
976
+ [
977
+ LifecyclePhase.SEED,
978
+ LifecyclePhase.DEVELOPMENT,
979
+ LifecyclePhase.TGE,
980
+ ].includes(dao.phase)
981
+ ) {
982
+ return dao.deployments[getChainByName(dao.initialChain).chainId]
983
+ .seedToken as string;
984
+ }
985
+
986
+ return dao.deployments[this.chainId]?.daoToken as string;
987
+ }
988
+
989
+ getFundingIndex(symbol: string, type: FundingType) {
990
+ const dao = this.getDAO(symbol);
991
+ for (let i = 0; i < dao.funding.length; i++) {
992
+ if (type === dao.funding[i].type) {
993
+ return i;
994
+ }
995
+ }
996
+ throw new Error("FundingNotFound");
997
+ }
998
+
999
+ warpDays(days: number = 7) {
1000
+ this.blockTimestamp += days * 86400;
1001
+ }
1002
+
1003
+ /** @throws Error */
1004
+ private _onlyOwnerOf(symbol: string) {
1005
+ if (this.from != this.getDaoOwner(symbol)) {
1006
+ throw new Error(`YouAreNotOwnerOf(${symbol})`);
1007
+ }
1008
+ }
1009
+
1010
+ private _emit(event: string) {
1011
+ this.events.push(event);
1012
+ }
1013
+
1014
+ private _validateName(name: string) {
1015
+ if (
1016
+ name.length < this.settings.minNameLength ||
1017
+ name.length > this.settings.maxNameLength
1018
+ ) {
1019
+ throw new Error(`NameLength(${name.length})`);
1020
+ }
1021
+ }
1022
+
1023
+ private _validateSymbol(symbol: string) {
1024
+ if (
1025
+ symbol.length < this.settings.minSymbolLength ||
1026
+ symbol.length > this.settings.maxSymbolLength
1027
+ ) {
1028
+ throw new Error(`SymbolLength(${symbol.length})`);
1029
+ }
1030
+ if (this.usedSymbols[symbol]) {
1031
+ throw new Error(`SymbolNotUnique(${symbol})`);
1032
+ }
1033
+ }
1034
+
1035
+ private _validatePvpFee(pvpFee: number) {
1036
+ if (pvpFee < this.settings.minPvPFee || pvpFee > this.settings.maxPvPFee) {
1037
+ throw new Error(`PvPFee(${pvpFee})`);
1038
+ }
1039
+ }
1040
+
1041
+ private _validateFunding(daoPhase: LifecyclePhase, fundings: IFunding[]) {
1042
+ for (const funding of fundings) {
1043
+ if (
1044
+ funding.type === FundingType.SEED &&
1045
+ daoPhase !== LifecyclePhase.DRAFT
1046
+ ) {
1047
+ throw new Error("TooLateToUpdateSuchFunding");
1048
+ }
1049
+ if (
1050
+ funding.type === FundingType.TGE &&
1051
+ ![
1052
+ LifecyclePhase.DRAFT,
1053
+ LifecyclePhase.SEED,
1054
+ LifecyclePhase.DEVELOPMENT,
1055
+ ].includes(daoPhase)
1056
+ ) {
1057
+ throw new Error("TooLateToUpdateSuchFunding");
1058
+ }
1059
+
1060
+ // todo check min round duration
1061
+ // todo check max round duration
1062
+ // todo check start date delay
1063
+ // todo check min amount
1064
+ // todo check max amount
1065
+ }
1066
+ }
1067
+
1068
+ private _validateVesting(daoPhase: LifecyclePhase, vestings: IVesting[]) {
1069
+ if (
1070
+ [
1071
+ LifecyclePhase.LIVE_CLIFF,
1072
+ LifecyclePhase.LIVE_VESTING,
1073
+ LifecyclePhase.LIVE,
1074
+ ].includes(daoPhase)
1075
+ ) {
1076
+ throw new Error("TooLateToUpdateVesting");
1077
+ }
1078
+ for (const vesting of vestings) {
1079
+ // todo check vesting consistency
1080
+ }
1081
+ }
1082
+
1083
+ private _sendCrossChainMessage(type: CROSS_CHAIN_MESSAGE, payload: any) {
1084
+ // todo some stub
1085
+ }
1086
+
1087
+ private _proposeAction(
1088
+ symbol: string,
1089
+ action: DAOAction,
1090
+ payload: any,
1091
+ ): string {
1092
+ const dao = this.getDAO(symbol);
1093
+
1094
+ // todo check for initial chain
1095
+ // todo get user power
1096
+ // todo check proposalThreshold
1097
+ // todo validate payload
1098
+
1099
+ const proposalId = Math.round(Math.random() * Math.random()).toString();
1100
+
1101
+ this.proposals[proposalId] = {
1102
+ id: proposalId,
1103
+ created: this.blockTimestamp,
1104
+ action,
1105
+ symbol,
1106
+ payload,
1107
+ status: VotingStatus.VOTING,
1108
+ };
1109
+
1110
+ return proposalId;
1111
+ }
1112
+
1113
+ private _updateImages(symbol: string, images: IDAOImages) {
1114
+ this.daos[symbol].images = images;
1115
+ this._emit(`Action ${DAOAction.UPDATE_IMAGES}`);
1116
+ }
1117
+
1118
+ private _updateVesting(symbol: string, vestings: IVesting[]) {
1119
+ this.daos[symbol].vesting = vestings;
1120
+ this._emit(`Action ${DAOAction.UPDATE_VESTING}`);
1121
+ }
1122
+ }
1123
+
1124
+ export enum DAOAction {
1125
+ UPDATE_IMAGES = 0,
1126
+ UPDATE_SOCIALS,
1127
+ UPDATE_NAMING,
1128
+ UPDATE_UNITS,
1129
+ UPDATE_FUNDING,
1130
+ UPDATE_VESTING,
1131
+ }
1132
+
1133
+ interface IOSSettings {
1134
+ priceDao: number;
1135
+ priceUnit: number;
1136
+ priceOracle: number;
1137
+ priceBridge: number;
1138
+ minNameLength: number;
1139
+ maxNameLength: number;
1140
+ minSymbolLength: number;
1141
+ maxSymbolLength: number;
1142
+ minVePeriod: number;
1143
+ maxVePeriod: number;
1144
+ minPvPFee: number;
1145
+ maxPvPFee: number;
1146
+ minFundingDuration: number;
1147
+ maxFundingDuration: number;
1148
+ }
1149
+
1150
+ enum VotingStatus {
1151
+ VOTING = 0,
1152
+ APPROVED,
1153
+ REJECTED,
1154
+ }
1155
+
1156
+ enum CROSS_CHAIN_MESSAGE {
1157
+ NEW_DAO_SYMBOL = 0,
1158
+ DAO_RENAME_SYMBOL,
1159
+ DAO_BRIDGED,
1160
+ }
1161
+
1162
+ interface ITask {
1163
+ name: string;
1164
+ }
1165
+
1166
+ interface IProposal {
1167
+ id: string;
1168
+ created: number;
1169
+ symbol: string;
1170
+ action: DAOAction;
1171
+ payload: any;
1172
+ status: VotingStatus;
1173
+ }
1174
+
1175
+ interface IRoadmapItem {
1176
+ phase: LifecyclePhase;
1177
+ start: number;
1178
+ end?: number;
1179
+ }
1180
+
1181
+ export function getDAOUnit(
1182
+ daos: IDAOData[],
1183
+ symbol: string,
1184
+ unitId: string,
1185
+ ): IUnit | undefined {
1186
+ for (const dao of daos) {
1187
+ if (dao.symbol.toLowerCase() === symbol.toLowerCase()) {
1188
+ for (const unit of dao.units) {
1189
+ if (unit.unitId === unitId) {
1190
+ return unit;
1191
+ }
1192
+ }
1193
+ }
1194
+ }
1195
+ }
1196
+
1197
+ export function getDAOUnitMetaData(
1198
+ daos: IDAOData[],
1199
+ symbol: string,
1200
+ unitId: string,
1201
+ ): IUnitMetaData | undefined {
1202
+ for (const dao of daos) {
1203
+ if (dao.symbol.toLowerCase() === symbol.toLowerCase()) {
1204
+ for (let i = 0; i < dao.units.length; i++) {
1205
+ const unit = dao.units[i];
1206
+ if (unit.unitId === unitId) {
1207
+ return dao.unitsMetaData[i];
1208
+ }
1209
+ }
1210
+ }
1211
+ }
1212
+ }