Contract Interaction

This guide explains how to programmatically interact with CreateDAO contracts using ethers.js or viem. It covers reading data from contracts, writing transactions, handling events, and implementing common use cases.

Prerequisites

Before you begin, make sure you have:

  • Basic knowledge of JavaScript/TypeScript
  • Familiarity with Ethereum and smart contracts
  • Node.js and npm/yarn installed

Setting Up

1. Install Dependencies

# Using npm
npm install ethers

# Using yarn
yarn add ethers

2. Configure Provider

Set up a provider to connect to the blockchain:

import { ethers } from 'ethers';

// For browser environments with MetaMask
const provider = new ethers.providers.Web3Provider(window.ethereum);

// For Node.js environments
// const provider = new ethers.providers.JsonRpcProvider('https://mainnet.infura.io/v3/YOUR_INFURA_KEY');

3. Load Contract ABIs

Create a file to store your contract ABIs:

// abis.js
export const DAO_FACTORY_ABI = [...]; // DAO Factory ABI
export const DAO_ABI = [...]; // DAO ABI
export const DAO_TOKEN_ABI = [...]; // DAO Token ABI
export const DAO_TREASURY_ABI = [...]; // DAO Treasury ABI
export const DAO_STAKING_ABI = [...]; // DAO Staking ABI
export const DAO_PRESALE_ABI = [...]; // DAO Presale ABI

Reading Contract Data

Connecting to Contracts

import { ethers } from 'ethers';
import { DAO_FACTORY_ABI, DAO_ABI } from './abis';

// Connect to DAO Factory
const daoFactoryAddress = '0x...'; // Replace with actual address
const daoFactory = new ethers.Contract(daoFactoryAddress, DAO_FACTORY_ABI, provider);

// Connect to a specific DAO
const daoAddress = '0x...'; // Replace with actual address
const dao = new ethers.Contract(daoAddress, DAO_ABI, provider);

Reading Basic DAO Information

async function getDAOInfo(daoAddress) {
  const dao = new ethers.Contract(daoAddress, DAO_ABI, provider);
  
  // Get DAO name
  const name = await dao.name();
  
  // Get associated contract addresses
  const tokenAddress = await dao.upgradeableContracts(2); // 2 is the index for Token in the UpgradeableContract enum
  const treasuryAddress = await dao.upgradeableContracts(1); // 1 is the index for Treasury
  const stakingAddress = await dao.upgradeableContracts(0); // 0 is the index for Staking
  
  // Get governance parameters
  const votingPeriod = await dao.votingPeriod();
  const minProposalStake = await dao.minProposalStake();
  const quorum = await dao.quorum();
  
  return {
    name,
    tokenAddress,
    treasuryAddress,
    stakingAddress,
    governance: {
      votingPeriod: votingPeriod.toString(),
      minProposalStake: ethers.utils.formatEther(minProposalStake),
      quorum: quorum.toNumber() / 100, // Convert basis points to percentage
    }
  };
}

Reading Proposal Data

async function getProposalDetails(daoAddress, proposalId) {
  const dao = new ethers.Contract(daoAddress, DAO_ABI, provider);
  
  // Get basic proposal info
  const proposal = await dao.getProposal(proposalId);
  const [proposalType, forVotes, againstVotes, endTime, executed] = proposal;
  
  // Get proposal-specific data based on type
  let specificData = {};
  
  if (proposalType.toNumber() === 0) { // Transfer proposal
    const transferData = await dao.getTransferData(proposalId);
    specificData = {
      token: transferData.token,
      recipient: transferData.recipient,
      amount: ethers.utils.formatEther(transferData.amount),
    };
  } else if (proposalType.toNumber() === 1) { // Upgrade proposal
    const upgradeData = await dao.getUpgradeData(proposalId);
    specificData = {
      newVersion: upgradeData.newVersion,
    };
  }
  // Add other proposal types as needed
  
  return {
    id: proposalId,
    type: proposalType.toNumber(),
    votes: {
      for: ethers.utils.formatEther(forVotes),
      against: ethers.utils.formatEther(againstVotes),
    },
    endTime: new Date(endTime.toNumber() * 1000),
    executed,
    specificData,
  };
}

Reading Treasury Data

async function getTreasuryInfo(treasuryAddress) {
  const treasury = new ethers.Contract(treasuryAddress, DAO_TREASURY_ABI, provider);
  
  // Get ETH balance
  const ethBalance = await treasury.getETHBalance();
  
  // Get token balance (example for a specific token)
  const tokenAddress = '0x...'; // Replace with actual token address
  const tokenBalance = await treasury.getERC20Balance(tokenAddress);
  
  return {
    ethBalance: ethers.utils.formatEther(ethBalance),
    tokenBalance: ethers.utils.formatEther(tokenBalance),
  };
}

Reading Staking Data

async function getStakingInfo(stakingAddress, userAddress) {
  const staking = new ethers.Contract(stakingAddress, DAO_STAKING_ABI, provider);
  
  // Get total staked
  const totalStaked = await staking.totalStaked();
  
  // Get user's staked amount
  const userStaked = await staking.stakedAmount(userAddress);
  
  // Get user's voting power
  const votingPower = await staking.getVotingPower(userAddress);
  
  // Get staking multipliers
  const multipliers = [];
  for (let i = 0; i < 4; i++) {
    multipliers.push((await staking.multipliers(i)).toNumber() / 10000);
  }
  
  // Get staking thresholds
  const thresholds = [];
  for (let i = 0; i < 3; i++) {
    thresholds.push((await staking.thresholds(i)).toNumber());
  }
  
  return {
    totalStaked: ethers.utils.formatEther(totalStaked),
    userStaked: ethers.utils.formatEther(userStaked),
    votingPower: ethers.utils.formatEther(votingPower),
    multipliers,
    thresholds: thresholds.map(t => t / (24 * 60 * 60) + ' days'),
  };
}

Writing Transactions

Setting Up a Signer

// Browser environment with MetaMask
const provider = new ethers.providers.Web3Provider(window.ethereum);
await provider.send("eth_requestAccounts", []);
const signer = provider.getSigner();

// Node.js environment with private key
// const privateKey = 'your-private-key';
// const signer = new ethers.Wallet(privateKey, provider);

Creating a DAO

async function createDAO(
  daoFactoryAddress,
  versionId,
  name,
  tokenName,
  tokenSymbol,
  initialSupply
) {
  const daoFactory = new ethers.Contract(daoFactoryAddress, DAO_FACTORY_ABI, signer);
  
  // Convert initialSupply to wei (assuming 18 decimals)
  const initialSupplyWei = ethers.utils.parseEther(initialSupply.toString());
  
  // Estimate gas
  const gasEstimate = await daoFactory.estimateGas.createDAO(
    versionId,
    name,
    tokenName,
    tokenSymbol,
    initialSupplyWei
  );
  
  // Add 20% buffer to gas estimate
  const gasLimit = gasEstimate.mul(120).div(100);
  
  // Create DAO
  const tx = await daoFactory.createDAO(
    versionId,
    name,
    tokenName,
    tokenSymbol,
    initialSupplyWei,
    { gasLimit }
  );
  
  console.log(`Transaction submitted: ${tx.hash}`);
  
  // Wait for transaction to be mined
  const receipt = await tx.wait();
  console.log(`Transaction confirmed in block ${receipt.blockNumber}`);
  
  // Parse event logs to get deployed addresses
  const daoCreatedEvent = receipt.events.find(e => e.event === 'DAOCreated');
  
  if (daoCreatedEvent) {
    const { daoAddress, tokenAddress, treasuryAddress, stakingAddress } = daoCreatedEvent.args;
    
    return {
      daoAddress,
      tokenAddress,
      treasuryAddress,
      stakingAddress,
      transactionHash: tx.hash,
    };
  }
  
  throw new Error('Failed to parse DAOCreated event');
}

Creating a Proposal

async function createTransferProposal(daoAddress, tokenAddress, recipient, amount) {
  const dao = new ethers.Contract(daoAddress, DAO_ABI, signer);
  
  // Convert amount to wei (assuming 18 decimals)
  const amountWei = ethers.utils.parseEther(amount.toString());
  
  // Create proposal
  const tx = await dao.proposeTransfer(tokenAddress, recipient, amountWei);
  console.log(`Transaction submitted: ${tx.hash}`);
  
  // Wait for transaction to be mined
  const receipt = await tx.wait();
  console.log(`Transaction confirmed in block ${receipt.blockNumber}`);
  
  // Parse event logs to get proposal ID
  const proposalCreatedEvent = receipt.events.find(e => e.event === 'ProposalCreated');
  
  if (proposalCreatedEvent) {
    const proposalId = proposalCreatedEvent.args.proposalId;
    return {
      proposalId: proposalId.toString(),
      transactionHash: tx.hash,
    };
  }
  
  throw new Error('Failed to parse ProposalCreated event');
}

Voting on a Proposal

async function voteOnProposal(daoAddress, proposalId, support) {
  const dao = new ethers.Contract(daoAddress, DAO_ABI, signer);
  
  // Vote on proposal
  const tx = await dao.vote(proposalId, support);
  console.log(`Transaction submitted: ${tx.hash}`);
  
  // Wait for transaction to be mined
  const receipt = await tx.wait();
  console.log(`Transaction confirmed in block ${receipt.blockNumber}`);
  
  return {
    transactionHash: tx.hash,
    blockNumber: receipt.blockNumber,
  };
}

Executing a Proposal

async function executeProposal(daoAddress, proposalId) {
  const dao = new ethers.Contract(daoAddress, DAO_ABI, signer);
  
  // Execute proposal
  const tx = await dao.execute(proposalId);
  console.log(`Transaction submitted: ${tx.hash}`);
  
  // Wait for transaction to be mined
  const receipt = await tx.wait();
  console.log(`Transaction confirmed in block ${receipt.blockNumber}`);
  
  return {
    transactionHash: tx.hash,
    blockNumber: receipt.blockNumber,
  };
}

Staking Tokens

async function stakeTokens(stakingAddress, tokenAddress, amount) {
  const staking = new ethers.Contract(stakingAddress, DAO_STAKING_ABI, signer);
  const token = new ethers.Contract(tokenAddress, DAO_TOKEN_ABI, signer);
  
  // Convert amount to wei (assuming 18 decimals)
  const amountWei = ethers.utils.parseEther(amount.toString());
  
  // Approve tokens for staking
  const approveTx = await token.approve(stakingAddress, amountWei);
  console.log(`Approval transaction submitted: ${approveTx.hash}`);
  
  // Wait for approval transaction to be mined
  await approveTx.wait();
  console.log('Approval confirmed');
  
  // Stake tokens
  const stakeTx = await staking.stake(amountWei);
  console.log(`Stake transaction submitted: ${stakeTx.hash}`);
  
  // Wait for stake transaction to be mined
  const receipt = await stakeTx.wait();
  console.log(`Stake transaction confirmed in block ${receipt.blockNumber}`);
  
  return {
    transactionHash: stakeTx.hash,
    blockNumber: receipt.blockNumber,
  };
}

Unstaking Tokens

async function unstakeTokens(stakingAddress, amount) {
  const staking = new ethers.Contract(stakingAddress, DAO_STAKING_ABI, signer);
  
  // Convert amount to wei (assuming 18 decimals)
  const amountWei = ethers.utils.parseEther(amount.toString());
  
  // Unstake tokens
  const tx = await staking.unstake(amountWei);
  console.log(`Transaction submitted: ${tx.hash}`);
  
  // Wait for transaction to be mined
  const receipt = await tx.wait();
  console.log(`Transaction confirmed in block ${receipt.blockNumber}`);
  
  return {
    transactionHash: tx.hash,
    blockNumber: receipt.blockNumber,
  };
}

Handling Events

Listening for New Proposals

function listenForProposals(daoAddress) {
  const dao = new ethers.Contract(daoAddress, DAO_ABI, provider);
  
  // Listen for ProposalCreated events
  dao.on('ProposalCreated', (proposalId, proposer, description, event) => {
    console.log(`New proposal created: #${proposalId}`);
    console.log(`Proposer: ${proposer}`);
    console.log(`Description: ${description}`);
    console.log(`Block: ${event.blockNumber}`);
    
    // You can fetch additional details about the proposal here
  });
  
  return () => {
    // Return a cleanup function to remove the listener
    dao.removeAllListeners('ProposalCreated');
  };
}

Listening for Votes

function listenForVotes(daoAddress) {
  const dao = new ethers.Contract(daoAddress, DAO_ABI, provider);
  
  // Listen for Voted events
  dao.on('Voted', (voter, proposalId, support, weight, event) => {
    console.log(`New vote on proposal #${proposalId}`);
    console.log(`Voter: ${voter}`);
    console.log(`Support: ${support ? 'For' : 'Against'}`);
    console.log(`Weight: ${ethers.utils.formatEther(weight)}`);
    console.log(`Block: ${event.blockNumber}`);
  });
  
  return () => {
    // Return a cleanup function to remove the listener
    dao.removeAllListeners('Voted');
  };
}

Listening for Proposal Execution

function listenForExecution(daoAddress) {
  const dao = new ethers.Contract(daoAddress, DAO_ABI, provider);
  
  // Listen for ProposalExecuted events
  dao.on('ProposalExecuted', (proposalId, event) => {
    console.log(`Proposal #${proposalId} executed`);
    console.log(`Block: ${event.blockNumber}`);
  });
  
  return () => {
    // Return a cleanup function to remove the listener
    dao.removeAllListeners('ProposalExecuted');
  };
}

Common Use Cases

Fetching All DAOs Created by a Factory

async function getAllDAOs(daoFactoryAddress) {
  const daoFactory = new ethers.Contract(daoFactoryAddress, DAO_FACTORY_ABI, provider);
  
  // Get past DAOCreated events
  const filter = daoFactory.filters.DAOCreated();
  const events = await daoFactory.queryFilter(filter);
  
  // Parse events to get DAO addresses
  const daos = events.map(event => {
    const { daoAddress, tokenAddress, treasuryAddress, stakingAddress, name, versionId } = event.args;
    
    return {
      daoAddress,
      tokenAddress,
      treasuryAddress,
      stakingAddress,
      name,
      versionId,
      blockNumber: event.blockNumber,
    };
  });
  
  return daos;
}

Fetching All Proposals for a DAO

async function getAllProposals(daoAddress) {
  const dao = new ethers.Contract(daoAddress, DAO_ABI, provider);
  
  // Get proposal count
  const proposalCount = await dao.proposalCount();
  
  // Fetch all proposals
  const proposals = [];
  for (let i = 1; i <= proposalCount; i++) {
    try {
      const proposal = await getProposalDetails(daoAddress, i);
      proposals.push(proposal);
    } catch (error) {
      console.error(`Error fetching proposal #${i}:`, error);
    }
  }
  
  return proposals;
}

Checking if a User Can Create a Proposal

async function canCreateProposal(daoAddress, stakingAddress, userAddress) {
  const dao = new ethers.Contract(daoAddress, DAO_ABI, provider);
  const staking = new ethers.Contract(stakingAddress, DAO_STAKING_ABI, provider);
  
  // Get minimum proposal stake
  const minProposalStake = await dao.minProposalStake();
  
  // Get user's voting power
  const votingPower = await staking.getVotingPower(userAddress);
  
  // Check if user has enough voting power
  const hasEnoughStake = votingPower.gte(minProposalStake);
  
  return {
    hasEnoughStake,
    votingPower: ethers.utils.formatEther(votingPower),
    minProposalStake: ethers.utils.formatEther(minProposalStake),
  };
}

Calculating Proposal Status

async function getProposalStatus(daoAddress, proposalId) {
  const dao = new ethers.Contract(daoAddress, DAO_ABI, provider);
  
  // Get proposal data
  const proposal = await dao.getProposal(proposalId);
  const [proposalType, forVotes, againstVotes, endTime, executed] = proposal;
  
  // Get quorum
  const quorum = await dao.quorum();
  
  // Get total staked
  const stakingAddress = await dao.upgradeableContracts(0); // 0 is the index for Staking
  const staking = new ethers.Contract(stakingAddress, DAO_STAKING_ABI, provider);
  const totalStaked = await staking.totalStaked();
  
  // Calculate status
  const now = Math.floor(Date.now() / 1000);
  const votingEnded = now > endTime.toNumber();
  
  // Calculate quorum percentage
  const quorumPercentage = quorum.toNumber() / 10000; // Convert basis points to decimal
  const quorumAmount = totalStaked.mul(quorum).div(10000);
  
  // Calculate total votes
  const totalVotes = forVotes.add(againstVotes);
  
  // Check if quorum is reached
  const quorumReached = totalVotes.gte(quorumAmount);
  
  // Check if proposal passed
  const passed = forVotes.gt(againstVotes);
  
  // Determine status
  let status;
  if (executed) {
    status = 'Executed';
  } else if (!votingEnded) {
    status = 'Active';
  } else if (!quorumReached) {
    status = 'Failed (Quorum not reached)';
  } else if (!passed) {
    status = 'Failed (Majority not reached)';
  } else {
    status = 'Ready for execution';
  }
  
  return {
    status,
    votingEnded,
    quorumReached,
    passed,
    canExecute: votingEnded && quorumReached && passed && !executed,
    stats: {
      forVotes: ethers.utils.formatEther(forVotes),
      againstVotes: ethers.utils.formatEther(againstVotes),
      totalVotes: ethers.utils.formatEther(totalVotes),
      quorumAmount: ethers.utils.formatEther(quorumAmount),
      quorumPercentage: quorumPercentage * 100,
      totalStaked: ethers.utils.formatEther(totalStaked),
      votingEndTime: new Date(endTime.toNumber() * 1000),
    }
  };
}

Error Handling

async function safeContractCall(contractCall) {
  try {
    return await contractCall();
  } catch (error) {
    // Parse error message
    let errorMessage = error.message;
    
    // Check for revert reason
    if (error.data) {
      try {
        // Try to parse error data
        const errorData = error.data;
        const decodedError = ethers.utils.toUtf8String(
          '0x' + errorData.substring(138)
        );
        errorMessage = decodedError;
      } catch (e) {
        // If parsing fails, use original error message
        console.error('Failed to parse error data:', e);
      }
    }
    
    throw new Error(`Contract call failed: ${errorMessage}`);
  }
}

// Example usage
async function safeVote(daoAddress, proposalId, support) {
  const dao = new ethers.Contract(daoAddress, DAO_ABI, signer);
  
  return safeContractCall(async () => {
    const tx = await dao.vote(proposalId, support);
    return tx.wait();
  });
}

Gas Optimization

async function optimizedContractCall(contractMethod, args, gasMultiplier = 1.2) {
  try {
    // Estimate gas
    const gasEstimate = await contractMethod.estimateGas(...args);
    
    // Add buffer to gas estimate
    const gasLimit = gasEstimate.mul(Math.floor(gasMultiplier * 100)).div(100);
    
    // Execute transaction with gas limit
    const tx = await contractMethod(...args, { gasLimit });
    return tx.wait();
  } catch (error) {
    console.error('Transaction failed:', error);
    throw error;
  }
}

// Example usage
async function optimizedVote(daoAddress, proposalId, support) {
  const dao = new ethers.Contract(daoAddress, DAO_ABI, signer);
  
  return optimizedContractCall(dao.vote, [proposalId, support]);
}

Conclusion

This guide provides a foundation for programmatically interacting with CreateDAO contracts. You can use these patterns to build applications that create, manage, and interact with DAOs.

For more information on building a complete DAO management platform, refer to the Building a Management Platform guide.