Skip to content
DocsDevelopersLaunch from TypeScript

Launch a DAO from TypeScript

This guide walks through deploying a complete DAO — a Quai Vault treasury, a DAOShip governance module, and the SharesERC20 / LootERC20 tokens — in a single transaction using the quais SDK. Then you submit a proposal and attach metadata via Poster.

Everything targets Quai Cyprus-1, chain ID 15000. Use the canonical addresses from Contracts.

1. Encode the governance config

Governance parameters are ABI-encoded into a single bytes blob. All percentages are basis points (10000 = 100%).

import { quais } from "quais";
 
const governanceConfig = quais.AbiCoder.defaultAbiCoder().encode(
  ["uint32", "uint32", "uint256", "uint256", "uint256", "uint256"],
  [
    7 * 24 * 3600,          // votingPeriod: 7 days
    3 * 24 * 3600,          // gracePeriod: 3 days
    quais.parseQuai("0.1"), // proposalOffering
    2000,                   // quorumPercent: 20% (basis points)
    quais.parseQuai("1"),   // sponsorThreshold: 1 share
    6600,                   // minRetentionPercent: 66%
  ],
);

Basis points, not percentages

Pass 2000 for 20%, 10000 for 100%. Passing a raw percentage like 20 creates a near-zero threshold — a critical misconfiguration.

2. Encode the init params template

launchDAOShipAndVault takes an init-params template. You set the avatar (3rd field) to a placeholder — the launcher overwrites it with the real vault address before launch. The token addresses are likewise filled in by the launcher.

const MULTISEND = "0x002ae8A47C2da497fe569AfCF0486410aA1093E0"; // MultiSendCallOnly
const founder = "0xYourFounderAddress";
 
const initParamsTemplate = quais.AbiCoder.defaultAbiCoder().encode(
  ["address","address","address","address","bytes","address[]","uint256[]","address[]","uint256[]","uint256[]","address[]","bool","bool"],
  [
    quais.ZeroAddress,         // lootToken   (filled by launcher)
    quais.ZeroAddress,         // sharesToken (filled by launcher)
    quais.ZeroAddress,         // avatar      (placeholder -> vault)
    MULTISEND,                 // multisend library
    governanceConfig,
    [],                        // navigators
    [],                        // navigator permissions
    [founder],                 // initial members
    [quais.parseQuai("100")],  // initial shares
    [quais.parseQuai("0")],    // initial loot
    [],                        // guild tokens
    false,                     // pauseSharesOnLaunch
    false,                     // pauseLootOnLaunch
  ],
);

3. Mine CREATE2 salts for the 0x00 shard

Cyprus-1 requires every contract address to begin with the 0x00 shard prefix. Because the clones are deployed via CREATE2, you mine salts off-chain until calculateAllAddresses returns four addresses that all start with 0x00.

minExecutionDelay must be 0 here — launchDAOShipAndVault hardcodes 0, so any other value produces a vault prediction that will not match the deployed address.

const launcher = new quais.Contract(
  "0x0036B11eEC6aa17407b0e157fA9caa32b7EFC9D1", // DAOShipAndVaultLauncher
  DAOSHIP_AND_VAULT_LAUNCHER_ABI,
  signer,
);
 
const vaultOwners = [founder];
const vaultThreshold = 1;
 
function startsWith00(addr: string): boolean {
  return addr.toLowerCase().startsWith("0x00");
}
 
async function mineSalts(sender: string) {
  for (let i = 0; ; i++) {
    const sharesSalt = BigInt(i) * 4n + 0n;
    const lootSalt = BigInt(i) * 4n + 1n;
    const daoShipSalt = BigInt(i) * 4n + 2n;
    const vaultSalt = BigInt(i) * 4n + 3n;
 
    const [daoShip, shares, loot, vault] = await launcher.calculateAllAddresses(
      sender, sharesSalt, lootSalt, daoShipSalt, vaultSalt,
      vaultOwners, vaultThreshold, 0, // minExecutionDelay must be 0
    );
 
    if ([daoShip, shares, loot, vault].every(startsWith00)) {
      return { sharesSalt, lootSalt, daoShipSalt, vaultSalt, daoShip, shares, loot, vault };
    }
  }
}

Mine in a worker

The live app runs salt mining in a Web Worker so the UI stays responsive. For server scripts, prefer a local prediction routine over an RPC round-trip per attempt when you have the singleton addresses and creation bytecode.

4. Launch

Pass the mined salts to launchDAOShipAndVault. The sender you mined against must equal the transaction sender, or the predicted addresses will not match.

const salts = await mineSalts(await signer.getAddress());
 
const tx = await launcher.launchDAOShipAndVault(
  initParamsTemplate,
  "MyDAO Shares", "MDS",
  "MyDAO Loot", "MDL",
  vaultOwners, vaultThreshold,
  salts.vaultSalt, salts.sharesSalt, salts.lootSalt, salts.daoShipSalt,
);
const receipt = await tx.wait();
// Event: LaunchDAOShipAndVault(daoShip, vault, shares, loot, newVault, launcher)

After this transaction, DAOShip is already enabled as a module on the vault — there is no separate enableModule step.

5. Submit a proposal

Proposals carry a MultiSend-encoded batch of vault actions. Here we send 10 QUAI from the treasury:

const daoShip = new quais.Contract(salts.daoShip, DAOSHIP_ABI, signer);
 
const proposalData = encodeMultiSend([{
  operation: 0,                 // Call
  to: recipientAddress,
  value: quais.parseQuai("10"),
  data: "0x",
}]);
 
await daoShip.submitProposal(proposalData, 0, "Fund community event", {
  value: await daoShip.proposalOffering(),
});

6. Post metadata via Poster

Poster stores no state — it emits NewPost for indexers and frontends. Attach a DAO profile under the recognized tag so the indexer picks it up:

const poster = new quais.Contract(
  "0x005C3957b8f612BBcdCFCbeDb8C53C3d3b3FEEdc", // Poster
  [
    "function post(string content, string tag) external",
    "event NewPost(address indexed user, string content, string indexed tag)",
  ],
  signer,
);
 
await poster.post(
  JSON.stringify({ name: "My DAO", description: "A test ship", logo: "ipfs://Qm..." }),
  "daoships.launcher.daoProfile",
);

The indexer stores these posts in ds_records, keyed by tag and trust level. See Indexer for how to query them, and Build a Navigator to add onboarding extensions.