Skip to main content
Create a decentralized autonomous organization (DAO) on NEAR that can control assets and execute transactions across multiple blockchains using Chain Signatures.

Overview

This tutorial demonstrates how to build a DAO that:
  • Votes on proposals using NEAR
  • Executes actions on Ethereum, Bitcoin, and other chains
  • Manages multi-chain treasuries
  • Coordinates cross-chain DeFi strategies

GitHub Repository

View the complete Multi-chain DAO example

Architecture

The Multi-chain DAO consists of three main components:
1

DAO Contract (NEAR)

Handles governance, voting, and proposal management
#[near(contract_state)]
pub struct MultiChainDao {
    proposals: UnorderedMap<u64, Proposal>,
    members: UnorderedSet<AccountId>,
    voting_threshold: u8,
}
2

Chain Signatures Integration

Connects to NEAR’s MPC network to sign transactions for other chains
pub fn execute_proposal(&mut self, proposal_id: u64) -> Promise {
    let proposal = self.proposals.get(&proposal_id).unwrap();
    require!(proposal.approved, "Proposal not approved");
    
    // Sign transaction for target chain
    ext_mpc::ext("v1.signer-prod.testnet".parse().unwrap())
        .sign(proposal.payload, proposal.path, 0)
}
3

Transaction Broadcaster

Sends signed transactions to target blockchains
// After getting signature from NEAR
const signedTx = attachSignature(transaction, signature);
const txHash = await targetChain.broadcastTransaction(signedTx);

Creating proposals

// Propose sending ETH
const ethTx = {
  to: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
  value: ethers.parseEther("1.0"),
  gasLimit: 21000,
};

await dao.call("create_proposal", {
  title: "Send 1 ETH to treasury manager",
  description: "Funding for Q1 operations",
  chain: "ethereum",
  transaction: ethTx,
  path: "dao-eth-treasury"
});

Voting on proposals

1

DAO members vote

near contract call-function as-transaction dao.testnet \
  vote \
  json-args '{"proposal_id": 1, "vote": "approve"}' \
  prepaid-gas '30 TeraGas' \
  attached-deposit '0 NEAR' \
  network-config testnet
2

Proposal reaches threshold

Once enough members vote, the proposal is approved:
pub fn vote(&mut self, proposal_id: u64, vote: Vote) {
    let mut proposal = self.proposals.get(&proposal_id).unwrap();
    require!(!proposal.executed, "Already executed");
    
    proposal.votes.insert(env::predecessor_account_id(), vote);
    
    // Check if threshold reached
    let approvals = proposal.votes.values()
        .filter(|v| **v == Vote::Approve)
        .count();
        
    if approvals * 100 / self.members.len() >= self.voting_threshold as usize {
        proposal.approved = true;
    }
    
    self.proposals.insert(proposal_id, proposal);
}
3

Execute approved proposal

Anyone can trigger execution once approved:
near contract call-function as-transaction dao.testnet \
  execute_proposal \
  json-args '{"proposal_id": 1}' \
  prepaid-gas '300 TeraGas' \
  attached-deposit '0 NEAR' \
  network-config testnet

Complete example

DAO Contract

use near_sdk::{
    near, env, require, AccountId, Promise,
    collections::{UnorderedMap, UnorderedSet},
    borsh::{BorshDeserialize, BorshSerialize},
};

#[near(serializers = [json, borsh])]
pub struct Proposal {
    pub id: u64,
    pub title: String,
    pub description: String,
    pub chain: String,
    pub transaction: Vec<u8>,
    pub path: String,
    pub votes: UnorderedMap<AccountId, Vote>,
    pub approved: bool,
    pub executed: bool,
}

#[near(serializers = [json, borsh])]
pub enum Vote {
    Approve,
    Reject,
    Abstain,
}

#[near(contract_state)]
pub struct MultiChainDao {
    proposals: UnorderedMap<u64, Proposal>,
    members: UnorderedSet<AccountId>,
    voting_threshold: u8,  // Percentage needed to pass
    next_proposal_id: u64,
}

#[near]
impl MultiChainDao {
    #[init]
    pub fn new(members: Vec<AccountId>, voting_threshold: u8) -> Self {
        require!(voting_threshold <= 100, "Invalid threshold");
        
        let mut member_set = UnorderedSet::new(b"m");
        for member in members {
            member_set.insert(member);
        }
        
        Self {
            proposals: UnorderedMap::new(b"p"),
            members: member_set,
            voting_threshold,
            next_proposal_id: 0,
        }
    }
    
    pub fn create_proposal(
        &mut self,
        title: String,
        description: String,
        chain: String,
        transaction: Vec<u8>,
        path: String,
    ) -> u64 {
        require!(
            self.members.contains(&env::predecessor_account_id()),
            "Only members can create proposals"
        );
        
        let proposal = Proposal {
            id: self.next_proposal_id,
            title,
            description,
            chain,
            transaction,
            path,
            votes: UnorderedMap::new(b"v"),
            approved: false,
            executed: false,
        };
        
        self.proposals.insert(self.next_proposal_id, proposal);
        self.next_proposal_id += 1;
        
        self.next_proposal_id - 1
    }
    
    pub fn vote(&mut self, proposal_id: u64, vote: Vote) {
        require!(
            self.members.contains(&env::predecessor_account_id()),
            "Only members can vote"
        );
        
        let mut proposal = self.proposals.get(&proposal_id)
            .expect("Proposal not found");
        require!(!proposal.executed, "Proposal already executed");
        
        proposal.votes.insert(env::predecessor_account_id(), vote);
        
        // Calculate approval percentage
        let approvals = proposal.votes.iter()
            .filter(|(_, v)| matches!(v, Vote::Approve))
            .count();
        
        let approval_pct = (approvals * 100) / self.members.len();
        if approval_pct >= self.voting_threshold as usize {
            proposal.approved = true;
        }
        
        self.proposals.insert(proposal_id, proposal);
    }
    
    pub fn execute_proposal(&mut self, proposal_id: u64) -> Promise {
        let mut proposal = self.proposals.get(&proposal_id)
            .expect("Proposal not found");
        require!(proposal.approved, "Proposal not approved");
        require!(!proposal.executed, "Already executed");
        
        proposal.executed = true;
        self.proposals.insert(proposal_id, proposal.clone());
        
        // Request signature from Chain Signatures MPC
        ext_mpc::ext("v1.signer-prod.testnet".parse().unwrap())
            .with_static_gas(Gas::from_tgas(250))
            .sign(proposal.transaction, proposal.path, 0)
    }
}

Benefits of Multi-chain DAOs

Unified governance

Manage assets across multiple chains from a single DAO interface

Cost efficiency

Vote on NEAR with low transaction fees, execute anywhere

Security

MPC-based signatures eliminate single points of failure

Flexibility

Add support for new chains without deploying new contracts

Use cases

  • Hold assets across multiple chains
  • Diversify investments in different DeFi protocols
  • Execute rebalancing strategies
  • Manage NFT collections on various chains
  • Control protocol upgrades on multiple chains
  • Manage parameter changes across deployments
  • Coordinate cross-chain protocol migrations
  • Unified fee collection and distribution
  • Pool funds across chains
  • Execute trades on different DEXs
  • Manage LP positions on various protocols
  • Coordinate yield farming strategies
  • Distribute funds in recipient’s preferred chain/token
  • Pay contributors across ecosystems
  • Fund development on multiple chains
  • Track spending across all chains

Next steps

Chain Signatures

Deep dive into multi-chain signatures

DAO primitives

Learn about NEAR DAO frameworks

Cross-chain transfers

Control accounts on other chains

NEAR Intents

Simplify cross-chain operations

Build docs developers (and LLMs) love