diff --git a/api/v2.go b/api/v2.go
index 727b0f76..7df4e1a6 100644
--- a/api/v2.go
+++ b/api/v2.go
@@ -1,4 +1,4 @@
-// Copyright (C) 2022, Chain4Travel AG. All rights reserved.
+// Copyright (C) 2022-2023, Chain4Travel AG. All rights reserved.
 //
 // This file is a derived work, based on ava-labs code whose
 // original notices appear below.
@@ -153,7 +153,9 @@ func AddV2Routes(ctx *Context, router *web.Router, path string, indexBytes []byt
 		Get("/cacheassetaggregates", (*V2Context).CacheAssetAggregates).
 		Get("/cacheaggregates/:id", (*V2Context).CacheAggregates).
 		Get("/multisigalias/:owners", (*V2Context).GetMultisigAlias).
-		Post("/rewards", (*V2Context).GetRewardPost)
+		Post("/rewards", (*V2Context).GetRewardPost).
+		Get("/proposals", (*V2Context).ListDACProposals).
+		Get("/proposals/:id", (*V2Context).GetDACProposalWithVotes)
 }
 
 // AVAX
@@ -1145,3 +1147,46 @@ func (c *V2Context) CacheAggregates(w web.ResponseWriter, r *web.Request) {
 
 	WriteJSON(w, b)
 }
+
+func (c *V2Context) ListDACProposals(w web.ResponseWriter, r *web.Request) {
+	collectors := utils.NewCollectors(
+		utils.NewCounterObserveMillisCollect(MetricMillis),
+		utils.NewCounterIncCollect(MetricCount),
+	)
+	defer func() {
+		_ = collectors.Collect()
+	}()
+
+	params := &params.ListDACProposalsParams{}
+	if err := params.ForValues(c.version, r.URL.Query()); err != nil {
+		c.WriteErr(w, 400, err)
+		return
+	}
+
+	c.WriteCacheable(w, caching.Cacheable{
+		TTL: 5 * time.Second,
+		Key: c.cacheKeyForParams("list_dac_proposals", params),
+		CacheableFn: func(ctx context.Context) (interface{}, error) {
+			return c.avaxReader.ListDACProposals(ctx, params)
+		},
+	})
+}
+
+func (c *V2Context) GetDACProposalWithVotes(w web.ResponseWriter, r *web.Request) {
+	collectors := utils.NewCollectors(
+		utils.NewCounterObserveMillisCollect(MetricMillis),
+		utils.NewCounterIncCollect(MetricCount),
+	)
+	defer func() {
+		_ = collectors.Collect()
+	}()
+
+	proposalID := r.PathParams["id"]
+	c.WriteCacheable(w, caching.Cacheable{
+		TTL: 5 * time.Second,
+		Key: c.cacheKeyForID("get_dac_proposal", proposalID),
+		CacheableFn: func(ctx context.Context) (interface{}, error) {
+			return c.avaxReader.GetDACProposalWithVotes(ctx, proposalID)
+		},
+	})
+}
diff --git a/db/dbmodel.go b/db/dbmodel.go
index 68170c95..6ea88fee 100644
--- a/db/dbmodel.go
+++ b/db/dbmodel.go
@@ -1,4 +1,4 @@
-// Copyright (C) 2022, Chain4Travel AG. All rights reserved.
+// Copyright (C) 2022-2023, Chain4Travel AG. All rights reserved.
 //
 // This file is a derived work, based on ava-labs code whose
 // original notices appear below.
@@ -21,6 +21,7 @@ import (
 	"github.com/ava-labs/avalanchego/ids"
 	"github.com/ava-labs/avalanchego/utils/hashing"
 	"github.com/chain4travel/magellan/models"
+	"github.com/chain4travel/magellan/services/indexes/params"
 	"github.com/chain4travel/magellan/utils"
 	"github.com/gocraft/dbr/v2"
 )
@@ -56,6 +57,8 @@ const (
 	TableMultisigAliases                = "multisig_aliases"
 	TableReward                         = "reward"
 	TableRewardOwner                    = "reward_owner"
+	TableDACProposals                   = "dac_proposals"
+	TableDACVotes                       = "dac_votes"
 )
 
 type Persist interface {
@@ -64,6 +67,11 @@ type Persist interface {
 		dbr.SessionRunner,
 		*Transactions,
 	) (*Transactions, error)
+	QueryMultipleTransactions(
+		context.Context,
+		dbr.SessionRunner,
+		[]string,
+	) (*[]Transactions, error)
 	InsertTransactions(
 		context.Context,
 		dbr.SessionRunner,
@@ -440,6 +448,62 @@ type Persist interface {
 		dbr.SessionRunner,
 		*Reward,
 	) error
+
+	// DAC Proposals
+	InsertDACProposal(
+		ctx context.Context,
+		session dbr.SessionRunner,
+		proposal *DACProposal,
+	) error
+	UpdateDACProposal(
+		ctx context.Context,
+		session dbr.SessionRunner,
+		proposalID string,
+		updatedProposal []byte,
+	) error
+	FinishDACProposals(
+		ctx context.Context,
+		session dbr.SessionRunner,
+		proposalIDs []string,
+		finishedAt time.Time,
+		proposalStatus models.ProposalStatus,
+	) error
+	FinishDACProposalWithOutcome(
+		ctx context.Context,
+		session dbr.SessionRunner,
+		proposalID string,
+		finishedAt time.Time,
+		proposalStatus models.ProposalStatus,
+		outcome []byte,
+	) error
+	GetDACProposals(
+		ctx context.Context,
+		session dbr.SessionRunner,
+		proposalIDs []string,
+	) ([]DACProposal, error)
+	QueryDACProposals(
+		ctx context.Context,
+		session dbr.SessionRunner,
+		params *params.ListDACProposalsParams,
+	) ([]DACProposal, error)
+
+	// DAC Votes
+	InsertDACVote(
+		ctx context.Context,
+		session dbr.SessionRunner,
+		vote *DACVote,
+	) error
+	QueryDACProposalVotes(
+		ctx context.Context,
+		session dbr.SessionRunner,
+		proposalID string,
+	) ([]DACVote, error)
+
+	GetTxHeight(
+		ctx context.Context,
+		session dbr.SessionRunner,
+		txID string,
+	) (uint64, error)
 }
 
 type persist struct{}
@@ -500,6 +564,29 @@ func (p *persist) QueryTransactions(
 	return v, err
 }
 
+func (p *persist) QueryMultipleTransactions(
+	ctx context.Context,
+	sess dbr.SessionRunner,
+	txIDs []string,
+) (*[]Transactions, error) {
+	v := &[]Transactions{}
+	err := sess.Select(
+		"id",
+		"chain_id",
+		"type",
+		"memo",
+		"created_at",
+		"canonical_serialization",
+		"txfee",
+		"genesis",
+		"network_id",
+		"status",
+	).From(TableTransactions).
+		Where("id IN ?", txIDs).
+		LoadOneContext(ctx, v)
+	return v, err
+}
+
 func (p *persist) InsertTransactions(
 	ctx context.Context,
 	sess dbr.SessionRunner,
@@ -2390,3 +2477,246 @@ func (p *persist) DeactivateReward(ctx context.Context, session dbr.SessionRunne
 
 	return p.InsertReward(ctx, session, v)
 }
+
+type DACProposal struct {
+	ID              string                `db:"id"`               // proposal id, also addProposalTx id
+	ProposerAddr    string                `db:"proposer_addr"`    // address which authorized proposal
+	StartTime       time.Time             `db:"start_time"`       // time when proposal will become votable
+	EndTime         time.Time             `db:"end_time"`         // time when proposal will become non-votable and will be executed if its successful
+	Type            models.ProposalType   `db:"type"`             // proposal type
+	IsAdminProposal bool                  `db:"admin_proposal"`   // true if it is admin proposal
+	Options         []byte                `db:"options"`          // proposal votable options
+	Data            []byte                `db:"data"`             // arbitrary proposal data
+	SerializedBytes []byte                `db:"serialized_bytes"` // serialized dac.ProposalState
+	Memo            []byte                `db:"memo"`             // addProposalTx memo
+	FinishedAt      *time.Time            `db:"finished_at"`      // time when proposal was finished
+	Outcome         []byte                `db:"outcome"`          // outcome of successful proposal, usually is one or multiple options indexes
+	Status          models.ProposalStatus `db:"status"`           // current status of proposal
+}
+
+func (p *persist) InsertDACProposal(ctx context.Context, session dbr.SessionRunner, proposal *DACProposal) error {
+	_, err := session.
+		InsertInto(TableDACProposals).
+		Pair("id", proposal.ID).
+		Pair("proposer_addr", proposal.ProposerAddr).
+		Pair("start_time", proposal.StartTime).
+		Pair("end_time", proposal.EndTime).
+		Pair("type", proposal.Type).
+		Pair("admin_proposal", proposal.IsAdminProposal).
+		Pair("serialized_bytes", proposal.SerializedBytes).
+		Pair("options", proposal.Options).
+		Pair("data", proposal.Data).
+		Pair("status", proposal.Status).
+		ExecContext(ctx)
+	if err != nil {
+		return EventErr(TableDACProposals, false, err)
+	}
+	return nil
+}
+
+func (p *persist) UpdateDACProposal(
+	ctx context.Context,
+	session dbr.SessionRunner,
+	proposalID string,
+	updatedProposal []byte,
+) error {
+	_, err := session.
+		Update(TableDACProposals).
+		Set("serialized_bytes", updatedProposal).
+		Where("id=?", proposalID).
+		ExecContext(ctx)
+	if err != nil {
+		return EventErr(TableDACProposals, true, err)
+	}
+	return nil
+}
+
+func (p *persist) FinishDACProposals(
+	ctx context.Context,
+	session dbr.SessionRunner,
+	proposalIDs []string,
+	finishedAt time.Time,
+	proposalStatus models.ProposalStatus,
+) error {
+	result, err := session.
+		Update(TableDACProposals).
+		Set("status", proposalStatus).
+		Set("finished_at", finishedAt).
+		Where("id IN ?", proposalIDs).
+		ExecContext(ctx)
+	if err != nil {
+		return EventErr(TableDACProposals, true, err)
+	}
+
+	if rowsAffected, err := result.RowsAffected(); err != nil {
+		return EventErr(TableDACProposals, true, err)
+	} else if rowsAffected == 0 {
+		return EventErr(TableDACProposals, true, dbr.ErrNotFound)
+	}
+
+	return nil
+}
+
+func (p *persist) FinishDACProposalWithOutcome(
+	ctx context.Context,
+	session dbr.SessionRunner,
+	proposalID string,
+	finishedAt time.Time,
+	proposalStatus models.ProposalStatus,
+	outcome []byte,
+) error {
+	result, err := session.
+		Update(TableDACProposals).
+		Set("status", proposalStatus).
+		Set("finished_at", finishedAt).
+		Set("outcome", outcome).
+		Where("id = ?", proposalID).
+		ExecContext(ctx)
+	if err != nil {
+		return EventErr(TableDACProposals, true, err)
+	}
+
+	if rowsAffected, err := result.RowsAffected(); err != nil {
+		return EventErr(TableDACProposals, true, err)
+	} else if rowsAffected == 0 {
+		return EventErr(TableDACProposals, true, dbr.ErrNotFound)
+	}
+
+	return nil
+}
+
+func (p *persist) GetDACProposals(
+	ctx context.Context,
+	session dbr.SessionRunner,
+	proposalIDs []string,
+) ([]DACProposal, error) {
+	v := &[]DACProposal{}
+	query := session.Select(
+		"P.id",
+		"P.proposer_addr",
+		"P.start_time",
+		"P.end_time",
+		"P.type",
+		"P.admin_proposal",
+		"P.serialized_bytes",
+		"P.options",
+		"P.data",
+		"T.memo",
+		"P.outcome",
+		"P.status",
+	).From(dbr.I(TableDACProposals).As("P")).
+		Join(dbr.I(TableTransactions).As("T"), "T.id=P.id").
+		Where("P.id in ?", proposalIDs)
+
+	_, err := query.LoadContext(ctx, v)
+	return *v, err
+}
+
+func (p *persist) QueryDACProposals(
+	ctx context.Context,
+	session dbr.SessionRunner,
+	params *params.ListDACProposalsParams,
+) ([]DACProposal, error) {
+	v := &[]DACProposal{}
+	query := session.Select(
+		"P.id",
+		"P.proposer_addr",
+		"P.start_time",
+		"P.end_time",
+		"P.type",
+		"P.admin_proposal",
+		"P.serialized_bytes",
+		"P.options",
+		"P.data",
+		"T.memo",
+		"P.finished_at",
+		"P.outcome",
+		"P.status",
+	).From(dbr.I(TableDACProposals).As("P")).
+		Join(dbr.I(TableTransactions).As("T"), "T.id=P.id")
+
+	if params.Offset > 0 {
+		query.Offset(uint64(params.Offset))
+	}
+	if params.Limit > 0 {
+		query.Limit(uint64(params.Limit))
+	}
+	if params.MinStartTimeProvided {
+		query.Where("P.start_time >= ?", params.MinStartTime)
+	}
+	if params.MaxStartTimeProvided {
+		query.Where("P.start_time <= ?", params.MaxStartTime)
+	}
+	if params.EndTimeProvided {
+		query.Where("P.end_time >= ?", params.EndTime)
+	}
+	if params.ProposalType != nil {
+		query.Where("P.type = ?", params.ProposalType)
+	}
+	if params.ProposalStatus != nil {
+		if *params.ProposalStatus == models.ProposalStatusCompleted {
+			query.Where("P.status IN ?", []models.ProposalStatus{models.ProposalStatusSuccess, models.ProposalStatusFailed})
+		} else {
+			query.Where("P.status = ?", params.ProposalStatus)
+		}
+	}
+
+	_, err := query.LoadContext(ctx, v)
+	return *v, err
+}
+
+type DACVote struct {
+	VoteTxID     string    `db:"id"`            // addVoteTx id
+	VoterAddr    string    `db:"voter_addr"`    // address which authorized this vote
+	VotedAt      time.Time `db:"voted_at"`      // timestamp when this vote happened
+	ProposalID   string    `db:"proposal_id"`   // id of proposal that was voted on
+	VotedOptions []byte    `db:"voted_options"` // proposal options that was voted by this vote, usually one or multiple option indexes
+}
+
+func (p *persist) InsertDACVote(ctx context.Context, session dbr.SessionRunner, vote *DACVote) error {
+	_, err := session.
+		InsertInto(TableDACVotes).
+		Pair("id", vote.VoteTxID).
+		Pair("voter_addr", vote.VoterAddr).
+		Pair("voted_at", vote.VotedAt).
+		Pair("proposal_id", vote.ProposalID).
+		Pair("voted_options", vote.VotedOptions).
+		ExecContext(ctx)
+	if err != nil {
+		return EventErr(TableDACVotes, false, err)
+	}
+	return nil
+}
+
+func (p *persist) QueryDACProposalVotes(
+	ctx context.Context,
+	session dbr.SessionRunner,
+	proposalID string,
+) ([]DACVote, error) {
+	v := &[]DACVote{}
+	_, err := session.Select(
+		"id",
+		"voter_addr",
+		"voted_at",
+		"voted_options",
+	).From(TableDACVotes).
+		Where("proposal_id = ?", proposalID).
+		LoadContext(ctx, v)
+	return *v, err
+}
+
+func (p *persist) GetTxHeight(
+	ctx context.Context,
+	session dbr.SessionRunner,
+	txID string,
+) (uint64, error) {
+	v := uint64(0)
+	err := session.SelectBySql(`
+		SELECT PB.height
+		FROM magellan.pvm_blocks AS PB
+		WHERE PB.id IN (
+			SELECT TB.tx_block_id
+			FROM magellan.transactions_block AS TB
+			WHERE TB.id = ?);`, txID).LoadOneContext(ctx, &v)
+	return v, err
+}
diff --git a/db/dbmodel_mock.go b/db/dbmodel_mock.go
index e70dcd8e..814bd559 100644
--- a/db/dbmodel_mock.go
+++ b/db/dbmodel_mock.go
@@ -1,3 +1,13 @@
+// Copyright (C) 2022-2023, Chain4Travel AG. All rights reserved.
+//
+// This file is a derived work, based on ava-labs code whose
+// original notices appear below.
+//
+// It is distributed under the same license conditions as the
+// original code from which it is derived.
+//
+// Much love to the original authors for their work.
+// **********************************************************
 // (c) 2021, Ava Labs, Inc. All rights reserved.
 // See the file LICENSE for licensing terms.
 
@@ -6,7 +16,10 @@ package db
 import (
 	"context"
 	"sync"
+	"time"
 
+	"github.com/chain4travel/magellan/models"
+	"github.com/chain4travel/magellan/services/indexes/params"
 	"github.com/gocraft/dbr/v2"
 )
 
@@ -43,6 +56,8 @@ type MockPersist struct {
 	MultisigAlias                  map[string]*MultisigAlias
 	RewardOwner                    map[string]*RewardOwner
 	Reward                         map[string]*Reward
+	DACProposals                   map[string]*DACProposal
+	DACVotes                       map[string]*DACVote
 }
 
 func NewPersistMock() *MockPersist {
@@ -75,6 +90,8 @@ func NewPersistMock() *MockPersist {
 		KeyValueStore:                  make(map[string]*KeyValueStore),
 		NodeIndex:                      make(map[string]*NodeIndex),
 		MultisigAlias:                  make(map[string]*MultisigAlias),
+		DACProposals:                   make(map[string]*DACProposal),
+		DACVotes:                       make(map[string]*DACVote),
 	}
 }
 
@@ -87,6 +104,18 @@ func (m *MockPersist) QueryTransactions(ctx context.Context, runner dbr.SessionR
 	return nil, nil
 }
 
+func (m *MockPersist) QueryMultipleTransactions(ctx context.Context, runner dbr.SessionRunner, txIDs []string) (*[]Transactions, error) {
+	m.lock.RLock()
+	defer m.lock.RUnlock()
+	var txs []Transactions
+	for _, txID := range txIDs {
+		if tx, ok := m.Transactions[txID]; ok {
+			txs = append(txs, *tx)
+		}
+	}
+	return &txs, nil
+}
+
 func (m *MockPersist) InsertTransactions(ctx context.Context, runner dbr.SessionRunner, v *Transactions, b bool) error {
 	m.lock.Lock()
 	defer m.lock.Unlock()
@@ -665,3 +694,123 @@ func (m *MockPersist) InsertReward(ctx context.Context, session dbr.SessionRunne
 	m.Reward[nv.RewardOwnerHash] = nv
 	return nil
 }
+
+func (m *MockPersist) InsertDACProposal(ctx context.Context, session dbr.SessionRunner, proposal *DACProposal) error {
+	m.lock.Lock()
+	defer m.lock.Unlock()
+	nv := &DACProposal{}
+	*nv = *proposal
+	m.DACProposals[proposal.ID] = nv
+	return nil
+}
+
+func (m *MockPersist) UpdateDACProposal(ctx context.Context, session dbr.SessionRunner, proposalID string, updatedProposal []byte) error {
+	m.lock.Lock()
+	defer m.lock.Unlock()
+	proposal, ok := m.DACProposals[proposalID]
+	if !ok {
+		return nil
+	}
+	proposal.SerializedBytes = updatedProposal
+	return nil
+}
+
+// QueryDACProposals is not deterministic about return result, cause of random mapping order
+func (m *MockPersist) QueryDACProposals(ctx context.Context, session dbr.SessionRunner, params *params.ListDACProposalsParams) ([]DACProposal, error) {
+	m.lock.Lock()
+	defer m.lock.Unlock()
+	proposals := make([]DACProposal, 0, len(m.DACProposals))
+	offset := 0
+	if params.Offset != 0 {
+		offset = params.Offset
+	}
+
+	for _, v := range m.DACProposals {
+		if len(proposals) == params.Limit {
+			break
+		}
+
+		if offset > 0 {
+			offset--
+			continue
+		}
+
+		if (!params.StartTimeProvided || !params.StartTime.Before(v.StartTime)) &&
+			(!params.EndTimeProvided || !params.EndTime.After(v.EndTime)) &&
+			(params.ProposalType == nil || *params.ProposalType == v.Type) &&
+			(params.ProposalStatus == nil || *params.ProposalStatus == v.Status) {
+			proposals = append(proposals, *v)
+		}
+	}
+	return proposals, nil
+}
+
+func (m *MockPersist) FinishDACProposals(ctx context.Context, session dbr.SessionRunner, proposalIDs []string, finishedAt time.Time, proposalStatus models.ProposalStatus) error {
+	m.lock.Lock()
+	defer m.lock.Unlock()
+	for _, proposalID := range proposalIDs {
+		if proposal, ok := m.DACProposals[proposalID]; ok {
+			proposal.FinishedAt = &finishedAt
+			proposal.Status = proposalStatus
+		}
+	}
+	return nil
+}
+
+func (m *MockPersist) FinishDACProposalWithOutcome(ctx context.Context, session dbr.SessionRunner, proposalID string, finishedAt time.Time, proposalStatus models.ProposalStatus, outcome []byte) error {
+	m.lock.Lock()
+	defer m.lock.Unlock()
+	proposal, ok := m.DACProposals[proposalID]
+	if !ok {
+		return nil
+	}
+	proposal.FinishedAt = &finishedAt
+	proposal.Status = proposalStatus
+	proposal.Outcome = outcome
+	return nil
+}
+
+func (m *MockPersist) GetDACProposals(ctx context.Context, session dbr.SessionRunner, proposalIDs []string) ([]DACProposal, error) {
+	proposals := make([]DACProposal, 0, len(proposalIDs))
+	for i := range proposalIDs {
+		if proposal, ok := m.DACProposals[proposalIDs[i]]; ok {
+			proposals = append(proposals, *proposal)
+		}
+	}
+	return proposals, nil
+}
+
+func (m *MockPersist) InsertDACVote(ctx context.Context, session dbr.SessionRunner, vote *DACVote) error {
+	m.lock.Lock()
+	defer m.lock.Unlock()
+	nv := &DACVote{}
+	*nv = *vote
+	m.DACVotes[vote.VoterAddr] = nv
+	return nil
+}
+
+func (m *MockPersist) QueryDACProposalVotes(ctx context.Context, session dbr.SessionRunner, voterAddr string) ([]DACVote, error) {
+	m.lock.Lock()
+	defer m.lock.Unlock()
+	votes := make([]DACVote, 0, len(m.DACVotes))
+	for _, v := range m.DACVotes {
+		if v.VoterAddr == voterAddr {
+			votes = append(votes, *v)
+		}
+	}
+	return votes, nil
+}
+
+func (m *MockPersist) GetTxHeight(ctx context.Context, session dbr.SessionRunner, txID string) (uint64, error) {
+	m.lock.Lock()
+	defer m.lock.Unlock()
+	txBlock, ok := m.TransactionsBlock[txID]
+	if !ok {
+		return 0, dbr.ErrNotFound
+	}
+	pvmBlock, ok := m.PvmBlocks[txBlock.TxBlockID]
+	if !ok {
+		return 0, dbr.ErrNotFound
+	}
+	return pvmBlock.Height, nil
+}
diff --git a/db/dbmodel_test.go b/db/dbmodel_test.go
index 51f898d1..2a3d1b3c 100644
--- a/db/dbmodel_test.go
+++ b/db/dbmodel_test.go
@@ -1,4 +1,4 @@
-// Copyright (C) 2022, Chain4Travel AG. All rights reserved.
+// Copyright (C) 2022-2023, Chain4Travel AG. All rights reserved.
 //
 // This file is a derived work, based on ava-labs code whose
 // original notices appear below.
@@ -20,7 +20,9 @@ import (
 	"time"
 
 	"github.com/chain4travel/magellan/models"
+	"github.com/chain4travel/magellan/services/indexes/params"
 	"github.com/gocraft/dbr/v2"
+	"github.com/stretchr/testify/require"
 )
 
 const (
@@ -1384,3 +1386,540 @@ func TestInsertMultisigAlias(t *testing.T) {
 		t.Fatal("delete fail", err)
 	}
 }
+
+func TestInsertDACProposal(t *testing.T) {
+	p := NewPersist()
+	ctx := context.Background()
+	stream := &dbr.NullEventReceiver{}
+	rawDBConn, err := dbr.Open(TestDB, TestDSN, stream)
+	require.NoError(t, err)
+	_, err = rawDBConn.NewSession(stream).DeleteFrom(TableTransactions).Exec()
+	require.NoError(t, err)
+	_, err = rawDBConn.NewSession(stream).DeleteFrom(TableDACVotes).Exec()
+	require.NoError(t, err)
+	_, err = rawDBConn.NewSession(stream).DeleteFrom(TableDACProposals).Exec()
+	require.NoError(t, err)
+	now := time.Now().UTC().Truncate(time.Second)
+
+	baseTxMemo := []byte("serialized base tx memo")
+	proposal := DACProposal{
+		ID:              "1111111111111111111111111111111111111111111111111",
+		ProposerAddr:    "222222222222222222222222222222222",
+		StartTime:       now.Add(1 * time.Second),
+		EndTime:         now.Add(10 * time.Second),
+		Type:            models.ProposalTypeBaseFee,
+		IsAdminProposal: false,
+		SerializedBytes: []byte("serialized proposal bytes"),
+		Options:         []byte("serialized proposal options"),
+		Data:            []byte("serialized proposal data"),
+		Memo:            []byte("should be ignored"),           // should be ingored and not inserted
+		Outcome:         []byte("serialized proposal outcome"), // should be ingored and not inserted
+		Status:          models.ProposalStatusInProgress,
+	}
+
+	require.NoError(t, p.InsertTransactions(ctx, rawDBConn.NewSession(stream), &Transactions{
+		ID:        proposal.ID,
+		Memo:      baseTxMemo,
+		CreatedAt: now,
+	}, false))
+	require.NoError(t, p.InsertDACProposal(ctx, rawDBConn.NewSession(stream), &proposal))
+
+	expectedProposal := proposal
+	expectedProposal.Memo = baseTxMemo
+	expectedProposal.Outcome = nil
+	resultProposals, err := p.QueryDACProposals(ctx, rawDBConn.NewSession(stream), &params.ListDACProposalsParams{})
+	require.NoError(t, err)
+	require.Equal(t, []DACProposal{expectedProposal}, resultProposals)
+}
+
+func TestUpdateDACProposal(t *testing.T) {
+	p := NewPersist()
+	ctx := context.Background()
+	stream := &dbr.NullEventReceiver{}
+	rawDBConn, err := dbr.Open(TestDB, TestDSN, stream)
+	require.NoError(t, err)
+	_, err = rawDBConn.NewSession(stream).DeleteFrom(TableTransactions).Exec()
+	require.NoError(t, err)
+	_, err = rawDBConn.NewSession(stream).DeleteFrom(TableDACVotes).Exec()
+	require.NoError(t, err)
+	_, err = rawDBConn.NewSession(stream).DeleteFrom(TableDACProposals).Exec()
+	require.NoError(t, err)
+	now := time.Now().UTC().Truncate(time.Second)
+
+	proposal := DACProposal{
+		ID:              "1111111111111111111111111111111111111111111111111",
+		ProposerAddr:    "222222222222222222222222222222222",
+		StartTime:       now.Add(1 * time.Second),
+		EndTime:         now.Add(10 * time.Second),
+		Type:            models.ProposalTypeBaseFee,
+		IsAdminProposal: false,
+		SerializedBytes: []byte("serialized proposal bytes"),
+		Options:         []byte("serialized proposal options"),
+		Data:            []byte("serialized proposal data"),
+		Memo:            []byte("serialized base tx memo"),
+		Status:          models.ProposalStatusInProgress,
+	}
+	require.NoError(t, p.InsertTransactions(ctx, rawDBConn.NewSession(stream), &Transactions{
+		ID:        proposal.ID,
+		Memo:      proposal.Memo,
+		CreatedAt: now,
+	}, false))
+	require.NoError(t, p.InsertDACProposal(ctx, rawDBConn.NewSession(stream), &proposal))
+
+	updatedProposalBytes := []byte("updated serialized proposal bytes")
+	require.NoError(t, p.UpdateDACProposal(
+		ctx,
+		rawDBConn.NewSession(stream),
+		proposal.ID,
+		updatedProposalBytes,
+	))
+
+	expectedProposal := proposal
+	expectedProposal.SerializedBytes = updatedProposalBytes
+	resultProposals, err := p.QueryDACProposals(ctx, rawDBConn.NewSession(stream), &params.ListDACProposalsParams{})
+	require.NoError(t, err)
+	require.Equal(t, []DACProposal{expectedProposal}, resultProposals)
+}
+
+func TestFinishDACProposals(t *testing.T) {
+	p := NewPersist()
+	ctx := context.Background()
+	stream := &dbr.NullEventReceiver{}
+	rawDBConn, err := dbr.Open(TestDB, TestDSN, stream)
+	require.NoError(t, err)
+	_, err = rawDBConn.NewSession(stream).DeleteFrom(TableTransactions).Exec()
+	require.NoError(t, err)
+	_, err = rawDBConn.NewSession(stream).DeleteFrom(TableDACVotes).Exec()
+	require.NoError(t, err)
+	_, err = rawDBConn.NewSession(stream).DeleteFrom(TableDACProposals).Exec()
+	require.NoError(t, err)
+	now := time.Now().UTC().Truncate(time.Second)
+
+	proposals := []*DACProposal{
+		{ // 0
+			ID:              "1111111111111111111111111111111111111111111111111",
+			ProposerAddr:    "111111111111111111111111111111111",
+			StartTime:       now.Add(1 * time.Second),
+			EndTime:         now.Add(10 * time.Second),
+			Type:            models.ProposalTypeBaseFee,
+			IsAdminProposal: false,
+			SerializedBytes: []byte("1 serialized proposal bytes 1"),
+			Options:         []byte("1 serialized proposal options 1"),
+			Data:            []byte("1 serialized proposal data 1"),
+			Memo:            []byte("1 serialized proposal memo 1"),
+			Status:          models.ProposalStatusInProgress,
+		},
+		{ // 1
+			ID:              "2222222222222222222222222222222222222222222222222",
+			ProposerAddr:    "222222222222222222222222222222222",
+			StartTime:       now.Add(1 * time.Second),
+			EndTime:         now.Add(10 * time.Second),
+			Type:            models.ProposalTypeBaseFee,
+			IsAdminProposal: false,
+			SerializedBytes: []byte("2 serialized proposal bytes 2"),
+			Options:         []byte("2 serialized proposal options 2"),
+			Data:            []byte("2 serialized proposal data 2"),
+			Memo:            []byte("2 serialized proposal memo 2"),
+			Status:          models.ProposalStatusInProgress,
+		},
+	}
+
+	for _, proposal := range proposals {
+		require.NoError(t, p.InsertTransactions(ctx, rawDBConn.NewSession(stream), &Transactions{
+			ID:        proposal.ID,
+			Memo:      proposal.Memo,
+			CreatedAt: now,
+		}, false))
+		require.NoError(t, p.InsertDACProposal(ctx, rawDBConn.NewSession(stream), proposal))
+	}
+
+	finishTime := now.Add(5 * time.Second)
+
+	require.Error(t, p.FinishDACProposals(
+		ctx,
+		rawDBConn.NewSession(stream),
+		[]string{"3333333333333333333333333333333333333333333333333"}, // non-existing
+		finishTime,
+		models.ProposalStatusFailed,
+	))
+
+	require.NoError(t, p.FinishDACProposals(
+		ctx,
+		rawDBConn.NewSession(stream),
+		[]string{proposals[0].ID, proposals[1].ID},
+		finishTime,
+		models.ProposalStatusFailed,
+	))
+
+	expectedProposal0 := *proposals[0]
+	expectedProposal0.Status = models.ProposalStatusFailed
+	expectedProposal0.FinishedAt = &finishTime
+	expectedProposal1 := *proposals[1]
+	expectedProposal1.Status = models.ProposalStatusFailed
+	expectedProposal1.FinishedAt = &finishTime
+	resultProposals, err := p.QueryDACProposals(ctx, rawDBConn.NewSession(stream), &params.ListDACProposalsParams{})
+	require.NoError(t, err)
+	require.Equal(t, []DACProposal{expectedProposal0, expectedProposal1}, resultProposals)
+}
+
+func TestFinishDACProposalWithOutcome(t *testing.T) {
+	p := NewPersist()
+	ctx := context.Background()
+	stream := &dbr.NullEventReceiver{}
+	rawDBConn, err := dbr.Open(TestDB, TestDSN, stream)
+	require.NoError(t, err)
+	_, err = rawDBConn.NewSession(stream).DeleteFrom(TableTransactions).Exec()
+	require.NoError(t, err)
+	_, err = rawDBConn.NewSession(stream).DeleteFrom(TableDACVotes).Exec()
+	require.NoError(t, err)
+	_, err = rawDBConn.NewSession(stream).DeleteFrom(TableDACProposals).Exec()
+	require.NoError(t, err)
+	now := time.Now().UTC().Truncate(time.Second)
+
+	proposal := DACProposal{
+		ID:              "1111111111111111111111111111111111111111111111111",
+		ProposerAddr:    "222222222222222222222222222222222",
+		StartTime:       now.Add(1 * time.Second),
+		EndTime:         now.Add(10 * time.Second),
+		Type:            models.ProposalTypeBaseFee,
+		IsAdminProposal: false,
+		SerializedBytes: []byte("serialized proposal bytes"),
+		Options:         []byte("serialized proposal options"),
+		Data:            []byte("serialized proposal data"),
+		Memo:            []byte("serialized base tx memo"),
+		Status:          models.ProposalStatusInProgress,
+	}
+	require.NoError(t, p.InsertTransactions(ctx, rawDBConn.NewSession(stream), &Transactions{
+		ID:        proposal.ID,
+		Memo:      proposal.Memo,
+		CreatedAt: now,
+	}, false))
+	require.NoError(t, p.InsertDACProposal(ctx, rawDBConn.NewSession(stream), &proposal))
+
+	finishTime := now.Add(5 * time.Second)
+	outcome := []byte{}
+
+	require.Error(t, p.FinishDACProposalWithOutcome(
+		ctx,
+		rawDBConn.NewSession(stream),
+		"3333333333333333333333333333333333333333333333333",
+		finishTime,
+		models.ProposalStatusSuccess,
+		outcome,
+	))
+
+	require.NoError(t, p.FinishDACProposalWithOutcome(
+		ctx,
+		rawDBConn.NewSession(stream),
+		proposal.ID,
+		finishTime,
+		models.ProposalStatusSuccess,
+		outcome,
+	))
+
+	expectedProposal := proposal
+	expectedProposal.Status = models.ProposalStatusSuccess
+	expectedProposal.Outcome = outcome
+	expectedProposal.FinishedAt = &finishTime
+	resultProposals, err := p.QueryDACProposals(ctx, rawDBConn.NewSession(stream), &params.ListDACProposalsParams{})
+	require.NoError(t, err)
+	require.Equal(t, []DACProposal{expectedProposal}, resultProposals)
+}
+
+func TestGetDACProposals(t *testing.T) {
+	p := NewPersist()
+	ctx := context.Background()
+	stream := &dbr.NullEventReceiver{}
+	rawDBConn, err := dbr.Open(TestDB, TestDSN, stream)
+	require.NoError(t, err)
+	_, err = rawDBConn.NewSession(stream).DeleteFrom(TableTransactions).Exec()
+	require.NoError(t, err)
+	_, err = rawDBConn.NewSession(stream).DeleteFrom(TableDACVotes).Exec()
+	require.NoError(t, err)
+	_, err = rawDBConn.NewSession(stream).DeleteFrom(TableDACProposals).Exec()
+	require.NoError(t, err)
+	now := time.Now().UTC().Truncate(time.Second)
+
+	proposals := []*DACProposal{
+		{ // 0
+			ID:              "1111111111111111111111111111111111111111111111111",
+			ProposerAddr:    "111111111111111111111111111111111",
+			StartTime:       now.Add(100 * time.Second),
+			EndTime:         now.Add(100 * time.Second).Add(time.Hour),
+			Type:            models.ProposalTypeBaseFee,
+			IsAdminProposal: false,
+			SerializedBytes: []byte("1 serialized proposal bytes 1"),
+			Options:         []byte("1 serialized proposal options 1"),
+			Data:            []byte("1 serialized proposal data 1"),
+			Memo:            []byte("1 serialized proposal memo 1"),
+			Status:          models.ProposalStatusInProgress,
+		},
+		{ // 1
+			ID:              "2222222222222222222222222222222222222222222222222",
+			ProposerAddr:    "222222222222222222222222222222222",
+			StartTime:       now.Add(100 * time.Second),
+			EndTime:         now.Add(100 * time.Second).Add(time.Hour),
+			Type:            models.ProposalType(100), // different proposal type
+			IsAdminProposal: false,
+			SerializedBytes: []byte("2 serialized proposal bytes 2"),
+			Options:         []byte("2 serialized proposal options 2"),
+			Data:            []byte("2 serialized proposal data 2"),
+			Memo:            []byte("2 serialized proposal memo 2"),
+			Status:          models.ProposalStatusInProgress,
+		},
+		{ // 2
+			ID:              "3333333333333333333333333333333333333333333333333",
+			ProposerAddr:    "333333333333333333333333333333333",
+			StartTime:       now.Add(100 * time.Second),
+			EndTime:         now.Add(100 * time.Second).Add(time.Hour),
+			Type:            models.ProposalTypeBaseFee,
+			IsAdminProposal: false,
+			SerializedBytes: []byte("3 serialized proposal bytes 3"),
+			Options:         []byte("3 serialized proposal options 3"),
+			Data:            []byte("3 serialized proposal data 3"),
+			Memo:            []byte("3 serialized proposal memo 3"),
+			Status:          models.ProposalStatusInProgress,
+		},
+		{ // 3
+			ID:              "4444444444444444444444444444444444444444444444444",
+			ProposerAddr:    "444444444444444444444444444444444",
+			StartTime:       now.Add(100 * time.Second),
+			EndTime:         now.Add(100 * time.Second).Add(time.Hour),
+			Type:            models.ProposalTypeBaseFee,
+			IsAdminProposal: false,
+			SerializedBytes: []byte("4 serialized proposal bytes 4"),
+			Options:         []byte("4 serialized proposal options 4"),
+			Data:            []byte("4 serialized proposal data 4"),
+			Memo:            []byte("4 serialized proposal memo 4"),
+			Status:          models.ProposalStatusInProgress,
+		},
+	}
+
+	for _, proposal := range proposals {
+		require.NoError(t, p.InsertTransactions(ctx, rawDBConn.NewSession(stream), &Transactions{
+			ID:        proposal.ID,
+			Memo:      proposal.Memo,
+			CreatedAt: now,
+		}, false))
+		require.NoError(t, p.InsertDACProposal(ctx, rawDBConn.NewSession(stream), proposal))
+	}
+
+	resultProposals, err := p.GetDACProposals(ctx, rawDBConn.NewSession(stream), []string{proposals[1].ID, proposals[2].ID})
+	require.NoError(t, err)
+	require.Equal(t, []DACProposal{*proposals[1], *proposals[2]}, resultProposals)
+}
+
+func TestQueryDACProposals(t *testing.T) {
+	p := NewPersist()
+	ctx := context.Background()
+	stream := &dbr.NullEventReceiver{}
+	rawDBConn, err := dbr.Open(TestDB, TestDSN, stream)
+	require.NoError(t, err)
+	_, err = rawDBConn.NewSession(stream).DeleteFrom(TableTransactions).Exec()
+	require.NoError(t, err)
+	_, err = rawDBConn.NewSession(stream).DeleteFrom(TableDACVotes).Exec()
+	require.NoError(t, err)
+	_, err = rawDBConn.NewSession(stream).DeleteFrom(TableDACProposals).Exec()
+	require.NoError(t, err)
+	now := time.Now().UTC().Truncate(time.Second)
+
+	baseFeeProposalType := models.ProposalTypeBaseFee
+	proposalStatusSuccess := models.ProposalStatusSuccess
+	queryParams := &params.ListDACProposalsParams{
+		ListParams: params.ListParams{
+			Limit:  3,
+			Offset: 1,
+		},
+		MinStartTime:         now.Add(100 * time.Second),
+		MaxStartTime:         now.Add(105 * time.Second),
+		MinStartTimeProvided: true,
+		MaxStartTimeProvided: true,
+		ProposalType:         &baseFeeProposalType,
+		ProposalStatus:       &proposalStatusSuccess,
+	}
+
+	proposals := []*DACProposal{
+		{ // 0
+			ID:              "1111111111111111111111111111111111111111111111111",
+			ProposerAddr:    "111111111111111111111111111111111",
+			StartTime:       queryParams.MinStartTime,
+			EndTime:         queryParams.MinStartTime.Add(time.Hour),
+			Type:            *queryParams.ProposalType,
+			IsAdminProposal: false,
+			SerializedBytes: []byte("1 serialized proposal bytes 1"),
+			Options:         []byte("1 serialized proposal options 1"),
+			Data:            []byte("1 serialized proposal data 1"),
+			Memo:            []byte("1 serialized proposal memo 1"),
+			Status:          models.ProposalStatusInProgress, // different proposal status
+		},
+		{ // 1
+			ID:              "2222222222222222222222222222222222222222222222222",
+			ProposerAddr:    "222222222222222222222222222222222",
+			StartTime:       queryParams.MinStartTime,
+			EndTime:         queryParams.MinStartTime.Add(time.Hour),
+			Type:            models.ProposalType(100), // different proposal type
+			IsAdminProposal: false,
+			SerializedBytes: []byte("2 serialized proposal bytes 2"),
+			Options:         []byte("2 serialized proposal options 2"),
+			Data:            []byte("2 serialized proposal data 2"),
+			Memo:            []byte("2 serialized proposal memo 2"),
+			Status:          *queryParams.ProposalStatus,
+		},
+		{ // 2
+			ID:              "3333333333333333333333333333333333333333333333333",
+			ProposerAddr:    "333333333333333333333333333333333",
+			StartTime:       queryParams.MinStartTime.Add(-time.Second), // starttime is before
+			EndTime:         queryParams.MinStartTime.Add(time.Hour),
+			Type:            *queryParams.ProposalType,
+			IsAdminProposal: false,
+			SerializedBytes: []byte("3 serialized proposal bytes 3"),
+			Options:         []byte("3 serialized proposal options 3"),
+			Data:            []byte("3 serialized proposal data 3"),
+			Memo:            []byte("3 serialized proposal memo 3"),
+			Status:          *queryParams.ProposalStatus,
+		},
+		{ // 3
+			ID:              "4444444444444444444444444444444444444444444444444",
+			ProposerAddr:    "444444444444444444444444444444444",
+			StartTime:       queryParams.MaxStartTime.Add(time.Second), // starttime is after
+			EndTime:         queryParams.MaxStartTime.Add(time.Hour),
+			Type:            *queryParams.ProposalType,
+			IsAdminProposal: false,
+			SerializedBytes: []byte("4 serialized proposal bytes 4"),
+			Options:         []byte("4 serialized proposal options 4"),
+			Data:            []byte("4 serialized proposal data 4"),
+			Memo:            []byte("4 serialized proposal memo 4"),
+			Status:          *queryParams.ProposalStatus,
+		},
+		{ // 4 // cut by offset
+			ID:              "5555555555555555555555555555555555555555555555555",
+			ProposerAddr:    "555555555555555555555555555555555",
+			StartTime:       queryParams.MinStartTime,
+			EndTime:         queryParams.MinStartTime.Add(time.Hour),
+			Type:            *queryParams.ProposalType,
+			IsAdminProposal: false,
+			SerializedBytes: []byte("5 serialized proposal bytes 5"),
+			Options:         []byte("5 serialized proposal options 5"),
+			Data:            []byte("5 serialized proposal data 5"),
+			Memo:            []byte("5 serialized proposal memo 5"),
+			Status:          *queryParams.ProposalStatus,
+		},
+		{ // 5
+			ID:              "6666666666666666666666666666666666666666666666666",
+			ProposerAddr:    "666666666666666666666666666666666",
+			StartTime:       queryParams.MinStartTime,
+			EndTime:         queryParams.MinStartTime.Add(time.Hour),
+			Type:            *queryParams.ProposalType,
+			SerializedBytes: []byte("6 serialized proposal bytes 6"),
+			Options:         []byte("6 serialized proposal options 6"),
+			Data:            []byte("6 serialized proposal data 6"),
+			Memo:            []byte("6 serialized proposal memo 6"),
+			Status:          *queryParams.ProposalStatus,
+		},
+		{ // 6
+			ID:              "7777777777777777777777777777777777777777777777777",
+			ProposerAddr:    "777777777777777777777777777777777",
+			StartTime:       queryParams.MinStartTime,
+			EndTime:         queryParams.MinStartTime.Add(time.Hour),
+			Type:            *queryParams.ProposalType,
+			IsAdminProposal: false,
+			SerializedBytes: []byte("7 serialized proposal bytes 7"),
+			Options:         []byte("7 serialized proposal options 7"),
+			Data:            []byte("7 serialized proposal data 7"),
+			Memo:            []byte("7 serialized proposal memo 7"),
+			Status:          *queryParams.ProposalStatus,
+		},
+		{ // 7
+			ID:              "8888888888888888888888888888888888888888888888888",
+			ProposerAddr:    "888888888888888888888888888888888",
+			StartTime:       queryParams.MinStartTime,
+			EndTime:         queryParams.MinStartTime.Add(time.Hour),
+			Type:            *queryParams.ProposalType,
+			IsAdminProposal: true,
+			SerializedBytes: []byte("8 serialized proposal bytes 8"),
+			Options:         []byte("8 serialized proposal options 8"),
+			Data:            []byte("8 serialized proposal data 8"),
+			Memo:            []byte("8 serialized proposal memo 8"),
+			Status:          *queryParams.ProposalStatus,
+		},
+		{ // 8 // cut by limit
+			ID:              "9999999999999999999999999999999999999999999999999",
+			ProposerAddr:    "999999999999999999999999999999999",
+			StartTime:       queryParams.MinStartTime,
+			EndTime:         queryParams.MinStartTime.Add(time.Hour),
+			Type:            *queryParams.ProposalType,
+			IsAdminProposal: false,
+			SerializedBytes: []byte("9 serialized proposal bytes 9"),
+			Options:         []byte("9 serialized proposal options 9"),
+			Data:            []byte("9 serialized proposal data 9"),
+			Memo:            []byte("9 serialized proposal memo 9"),
+			Status:          *queryParams.ProposalStatus,
+		},
+		{ // 9
+			ID:              "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
+			ProposerAddr:    "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
+			StartTime:       queryParams.MinStartTime,
+			EndTime:         queryParams.MinStartTime.Add(time.Hour),
+			Type:            *queryParams.ProposalType,
+			IsAdminProposal: true,
+			SerializedBytes: []byte("A serialized proposal bytes A"),
+			Options:         []byte("A serialized proposal options A"),
+			Data:            []byte("A serialized proposal data A"),
+			Memo:            []byte("A serialized proposal memo A"),
+			Status:          models.ProposalStatusFailed, // different proposal status, but will be included in "completed" status query
+		},
+	}
+
+	for _, proposal := range proposals {
+		require.NoError(t, p.InsertTransactions(ctx, rawDBConn.NewSession(stream), &Transactions{
+			ID:        proposal.ID,
+			Memo:      proposal.Memo,
+			CreatedAt: now,
+		}, false))
+		require.NoError(t, p.InsertDACProposal(ctx, rawDBConn.NewSession(stream), proposal))
+	}
+
+	resultProposals, err := p.QueryDACProposals(ctx, rawDBConn.NewSession(stream), queryParams)
+	require.NoError(t, err)
+	require.Equal(t, []DACProposal{*proposals[5], *proposals[6], *proposals[7]}, resultProposals)
+
+	proposalStatusCompleted := models.ProposalStatusCompleted
+	queryParams = &params.ListDACProposalsParams{
+		ProposalStatus: &proposalStatusCompleted,
+	}
+	resultProposals, err = p.QueryDACProposals(ctx, rawDBConn.NewSession(stream), queryParams)
+	require.NoError(t, err)
+	require.Equal(t,
+		[]DACProposal{*proposals[1], *proposals[2], *proposals[3], *proposals[4], *proposals[5], *proposals[6], *proposals[7], *proposals[8], *proposals[9]},
+		resultProposals)
+}
+
+// TestInsertDACVote also tests QueryDACProposalVotes
+func TestInsertDACVote(t *testing.T) {
+	p := NewPersist()
+	ctx := context.Background()
+	stream := &dbr.NullEventReceiver{}
+	rawDBConn, err := dbr.Open(TestDB, TestDSN, stream)
+	require.NoError(t, err)
+	_, err = rawDBConn.NewSession(stream).DeleteFrom(TableTransactions).Exec()
+	require.NoError(t, err)
+	_, err = rawDBConn.NewSession(stream).DeleteFrom(TableDACVotes).Exec()
+	require.NoError(t, err)
+	now := time.Now().UTC().Truncate(time.Second)
+
+	vote := DACVote{
+		VoteTxID:     "1111111111111111111111111111111111111111111111111",
+		VoterAddr:    "222222222222222222222222222222222",
+		VotedAt:      now,
+		ProposalID:   "3333333333333333333333333333333333333333333333333",
+		VotedOptions: []byte("serialized voted options"),
+	}
+
+	require.NoError(t, p.InsertDACVote(ctx, rawDBConn.NewSession(stream), &vote))
+
+	expectedVote := vote
+	expectedVote.ProposalID = ""
+	resultVotes, err := p.QueryDACProposalVotes(ctx, rawDBConn.NewSession(stream), vote.ProposalID)
+	require.NoError(t, err)
+	require.Equal(t, []DACVote{expectedVote}, resultVotes)
+}
diff --git a/dependencies/caminoethvm b/dependencies/caminoethvm
index c70cf2d6..f525ec79 160000
--- a/dependencies/caminoethvm
+++ b/dependencies/caminoethvm
@@ -1 +1 @@
-Subproject commit c70cf2d6a4363a0cb6eadf85720c918c3ee708cb
+Subproject commit f525ec79dd22f2eaafce8e81ae9186eac06d3ef6
diff --git a/go.mod b/go.mod
index 12784e84..1f015b73 100644
--- a/go.mod
+++ b/go.mod
@@ -23,6 +23,7 @@ require (
 	github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect
 	github.com/btcsuite/btcd/btcutil v1.1.3 // indirect
 	github.com/cenkalti/backoff/v4 v4.1.3 // indirect
+	github.com/cespare/cp v1.0.0 // indirect
 	github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
 	github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 // indirect
 	github.com/dop251/goja v0.0.0-20220405120441-9037c2b61cbf // indirect
diff --git a/go.sum b/go.sum
index 2db6d24f..0af6c57b 100644
--- a/go.sum
+++ b/go.sum
@@ -187,6 +187,7 @@ github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl
 github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
 github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
 github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
+github.com/btcsuite/btcd v0.23.0 h1:V2/ZgjfDFIygAX3ZapeigkVBoVUtOJKSwrhZdlpSvaA=
 github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY=
 github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
 github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
@@ -221,7 +222,8 @@ github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA
 github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
 github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
-github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk=
+github.com/cespare/cp v1.0.0 h1:47QuPGrUwHTJLdv2MeejqLT29EfhvKzfH+OMBvayz80=
+github.com/cespare/cp v1.0.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s=
 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
diff --git a/models/collections.go b/models/collections.go
index 2e5eceb4..9f7e1905 100644
--- a/models/collections.go
+++ b/models/collections.go
@@ -1,4 +1,4 @@
-// Copyright (C) 2022, Chain4Travel AG. All rights reserved.
+// Copyright (C) 2022-2023, Chain4Travel AG. All rights reserved.
 //
 // This file is a derived work, based on ava-labs code whose
 // original notices appear below.
@@ -21,6 +21,57 @@ import (
 	"github.com/chain4travel/magellan/modelsc"
 )
 
+type DACProposalWithVotes struct {
+	DACProposal DACProposal `json:"dacProposal"`
+	DACVotes    []DACVote   `json:"dacVotes"`
+}
+
+type DACProposalsList struct {
+	DACProposals []DACProposalWithVotes `json:"dacProposals"`
+}
+
+type ProposalStatus int
+
+const (
+	ProposalStatusInProgress ProposalStatus = iota
+	ProposalStatusSuccess
+	ProposalStatusFailed
+	ProposalStatusCompleted // both success and failed
+)
+
+type ProposalType int
+
+const (
+	ProposalTypeBaseFee ProposalType = iota
+	ProposalTypeAddMember
+	ProposalTypeExcludeMember
+	ProposalTypeGeneral
+	ProposalTypeFeeDistribution
+)
+
+type DACProposal struct {
+	ID              string         `json:"id"`                   // proposal id, also addProposalTx id
+	ProposerAddr    string         `json:"proposerAddr"`         // address which authorized proposal
+	StartTime       time.Time      `json:"startTime"`            // time when proposal will become votable
+	EndTime         time.Time      `json:"endTime"`              // time when proposal will become non-votable and will be executed if its successful
+	FinishedAt      *time.Time     `json:"finishedAt,omitempty"` // time when proposal was finished
+	Type            ProposalType   `json:"type"`                 // proposal type
+	IsAdminProposal bool           `json:"admin_proposal"`       // true if it is admin proposal
+	Options         []byte         `json:"options"`              // proposal votable options
+	Data            []byte         `json:"data,omitempty"`       // arbitrary proposal data
+	Memo            []byte         `json:"memo"`                 // addProposalTx memo
+	Outcome         []byte         `json:"outcome,omitempty"`    // outcome of successful proposal, usually is one or multiple options indexes
+	Status          ProposalStatus `json:"status"`               // current status of proposal
+	BlockHeight     uint64         `json:"blockHeight"`          // height of proposal block
+}
+
+type DACVote struct {
+	VoteTxID     string    `json:"voteTxID"`     // addVoteTx id
+	VoterAddr    string    `json:"voterAddr"`    // address which authorized this vote
+	VotedAt      time.Time `json:"votedAt"`      // timestamp when this vote happened
+	VotedOptions []byte    `json:"votedOptions"` // proposal options that was voted by this vote, usually one or multiple option indexes
+}
+
 type MultisigAliasList struct {
 	Alias []string `json:"alias"`
 }
diff --git a/models/types.go b/models/types.go
index c1b9b1f7..5f287b7e 100644
--- a/models/types.go
+++ b/models/types.go
@@ -1,3 +1,13 @@
+// Copyright (C) 2022-2023, Chain4Travel AG. All rights reserved.
+//
+// This file is a derived work, based on ava-labs code whose
+// original notices appear below.
+//
+// It is distributed under the same license conditions as the
+// original code from which it is derived.
+//
+// Much love to the original authors for their work.
+// **********************************************************
 // (c) 2021, Ava Labs, Inc. All rights reserved.
 // See the file LICENSE for licensing terms.
 
@@ -46,8 +56,6 @@ var (
 	TransactionTypeTransformSubnet            TransactionType = 0x16
 	TransactionTypeAddPermissionlessValidator TransactionType = 0x17
 	TransactionTypeAddPermissionlessDelegator TransactionType = 0x18
-	TransactionTypeAddDaoProposal             TransactionType = 0x19
-	TransactionTypeAddDaoVote                 TransactionType = 0x20
 
 	// Camino Custom Datatypes
 
@@ -73,6 +81,9 @@ var (
 	TransactionTypeClaimReward           TransactionType = RegisterTransactionTypeCustom + 10
 	TransactionTypeRewardsImport         TransactionType = RegisterTransactionTypeCustom + 11
 	TransactionTypeAddDepositOffer       TransactionType = RegisterTransactionTypeCustom + 15
+	TransactionTypeAddDACProposal        TransactionType = RegisterTransactionTypeCustom + 16
+	TransactionTypeAddDACVote            TransactionType = RegisterTransactionTypeCustom + 17
+	TransactionTypeFinishDACProposals    TransactionType = RegisterTransactionTypeCustom + 18
 
 	ResultTypeTransaction SearchResultType = "transaction"
 	ResultTypeAsset       SearchResultType = "asset"
@@ -136,10 +147,12 @@ func (t TransactionType) String() string {
 		return "add_permissionless_validator"
 	case TransactionTypeAddPermissionlessDelegator:
 		return "add_permissionless_delegator"
-	case TransactionTypeAddDaoProposal:
-		return "add_dao_proposal"
-	case TransactionTypeAddDaoVote:
-		return "add_dao_vote"
+	case TransactionTypeAddDACProposal:
+		return "add_dac_proposal"
+	case TransactionTypeAddDACVote:
+		return "add_dac_vote"
+	case TransactionTypeFinishDACProposals:
+		return "finish_dac_proposals"
 	case TransactionTypeAddAddressState:
 		return "address_state"
 	case TransactionTypeDeposit:
diff --git a/services/db/migrations/054_dac_proposals_and_votes.down.sql b/services/db/migrations/054_dac_proposals_and_votes.down.sql
new file mode 100644
index 00000000..abfb95b1
--- /dev/null
+++ b/services/db/migrations/054_dac_proposals_and_votes.down.sql
@@ -0,0 +1,5 @@
+DROP INDEX dac_votes_by_proposal_id;
+DROP TABLE dac_votes;
+
+DROP INDEX dac_proposals_by_id;
+DROP TABLE dac_proposals;
diff --git a/services/db/migrations/054_dac_proposals_and_votes.up.sql b/services/db/migrations/054_dac_proposals_and_votes.up.sql
new file mode 100644
index 00000000..1c154e54
--- /dev/null
+++ b/services/db/migrations/054_dac_proposals_and_votes.up.sql
@@ -0,0 +1,29 @@
+CREATE TABLE `dac_proposals` (
+  id                VARCHAR(50)      NOT NULL PRIMARY KEY,
+  proposer_addr     VARCHAR(50)      NOT NULL,
+  start_time        TIMESTAMP        NOT NULL,
+  end_time          TIMESTAMP        NOT NULL,
+  type              TINYINT          NOT NULL,
+  admin_proposal    BOOLEAN          NOT NULL,
+  serialized_bytes  VARBINARY(1024)  NOT NULL,
+  finished_at       TIMESTAMP,
+  options           VARBINARY(1024)  NOT NULL,
+  data              VARBINARY(1024),
+  outcome           VARBINARY(1024),
+  status            TINYINT          NOT NULL
+);
+
+CREATE UNIQUE INDEX dac_proposals_by_id ON dac_proposals (id);
+
+CREATE TABLE `dac_votes` (
+  id             VARCHAR(50)      NOT NULL PRIMARY KEY,
+  voter_addr     VARCHAR(50)      NOT NULL,
+  voted_at       TIMESTAMP        NOT NULL,
+  proposal_id    VARCHAR(50)      NOT NULL,
+  voted_options  VARBINARY(1024)  NOT NULL,
+
+  FOREIGN KEY    (proposal_id)    REFERENCES dac_proposals(id)
+);
+ 
+ 
+CREATE INDEX dac_votes_by_proposal_id ON dac_votes (proposal_id);
\ No newline at end of file
diff --git a/services/indexes/avax/reader.go b/services/indexes/avax/reader.go
index 0c01839b..d1db6529 100644
--- a/services/indexes/avax/reader.go
+++ b/services/indexes/avax/reader.go
@@ -1,4 +1,4 @@
-// Copyright (C) 2022, Chain4Travel AG. All rights reserved.
+// Copyright (C) 2022-2023, Chain4Travel AG. All rights reserved.
 //
 // This file is a derived work, based on ava-labs code whose
 // original notices appear below.
@@ -25,6 +25,8 @@ import (
 	"time"
 
 	"github.com/ava-labs/avalanchego/ids"
+	"github.com/ava-labs/avalanchego/utils/constants"
+	"github.com/ava-labs/avalanchego/utils/formatting/address"
 	"github.com/chain4travel/magellan/caching"
 	"github.com/chain4travel/magellan/cfg"
 	"github.com/chain4travel/magellan/db"
@@ -1040,3 +1042,150 @@ func (r *Reader) ActiveAddresses(ctx context.Context, p *params.ListParams) (*mo
 	addressStatistics.AddressInfo = ActiveAddresses
 	return addressStatistics, err
 }
+
+func (r *Reader) ListDACProposals(ctx context.Context, p *params.ListDACProposalsParams) (*models.DACProposalsList, error) {
+	dbRunner, err := r.conns.DB().NewSession("list_dac_proposals", cfg.RequestTimeout)
+	if err != nil {
+		return nil, err
+	}
+
+	proposals, err := r.sc.Persist.QueryDACProposals(ctx, dbRunner, p)
+	if err != nil {
+		return nil, err
+	}
+
+	proposalsList := make([]models.DACProposalWithVotes, len(proposals))
+	for i := range proposals {
+		height, err := r.sc.Persist.GetTxHeight(ctx, dbRunner, proposals[i].ID)
+		if err != nil {
+			return nil, err
+		}
+
+		votes, err := r.sc.Persist.QueryDACProposalVotes(ctx, dbRunner, proposals[i].ID)
+		if err != nil {
+			return nil, err
+		}
+
+		proposalModel, err := r.dacProposalFromDB(&proposals[i])
+		if err != nil {
+			return nil, err
+		}
+		proposalModel.BlockHeight = height
+
+		votesModel, err := r.dacVotesFromDB(votes)
+		if err != nil {
+			return nil, err
+		}
+
+		proposalsList[i] = models.DACProposalWithVotes{
+			DACProposal: proposalModel,
+			DACVotes:    votesModel,
+		}
+	}
+
+	return &models.DACProposalsList{DACProposals: proposalsList}, nil
+}
+
+func (r *Reader) GetDACProposalWithVotes(ctx context.Context, proposalID string) (*models.DACProposalWithVotes, error) {
+	dbRunner, err := r.conns.DB().NewSession("get_dac_proposal", cfg.RequestTimeout)
+	if err != nil {
+		return nil, err
+	}
+
+	proposals, err := r.sc.Persist.GetDACProposals(ctx, dbRunner, []string{proposalID})
+	if err != nil {
+		return nil, err
+	}
+	switch {
+	case err != nil:
+		return nil, err
+	case len(proposals) == 0:
+		return nil, dbr.ErrNotFound
+	case len(proposals) != 1:
+		return nil, errors.New("db returned multiple proposals for one proposalID") // should never happen
+	}
+
+	height, err := r.sc.Persist.GetTxHeight(ctx, dbRunner, proposalID)
+	if err != nil {
+		return nil, err
+	}
+
+	votes, err := r.sc.Persist.QueryDACProposalVotes(ctx, dbRunner, proposalID)
+	if err != nil {
+		return nil, err
+	}
+
+	proposalModel, err := r.dacProposalFromDB(&proposals[0])
+	if err != nil {
+		return nil, err
+	}
+	proposalModel.BlockHeight = height
+
+	votesModel, err := r.dacVotesFromDB(votes)
+	if err != nil {
+		return nil, err
+	}
+
+	return &models.DACProposalWithVotes{
+		DACVotes:    votesModel,
+		DACProposal: proposalModel,
+	}, nil
+}
+
+func (r *Reader) dacProposalFromDB(proposal *db.DACProposal) (models.DACProposal, error) {
+	id, err := ids.ShortFromString(proposal.ProposerAddr)
+	if err != nil {
+		return models.DACProposal{}, err
+	}
+
+	proposerAddr, err := address.Format("P", constants.GetHRP(r.networkID), id[:])
+	if err != nil {
+		return models.DACProposal{}, err
+	}
+
+	return models.DACProposal{
+		ID:              proposal.ID,
+		ProposerAddr:    proposerAddr,
+		StartTime:       proposal.StartTime,
+		EndTime:         proposal.EndTime,
+		FinishedAt:      proposal.FinishedAt,
+		Type:            proposal.Type,
+		IsAdminProposal: proposal.IsAdminProposal,
+		Options:         proposal.Options,
+		Data:            proposal.Data,
+		Memo:            proposal.Memo,
+		Outcome:         proposal.Outcome,
+		Status:          proposal.Status,
+	}, nil
+}
+
+func (r *Reader) dacVotesFromDB(votes []db.DACVote) ([]models.DACVote, error) {
+	dacVotes := make([]models.DACVote, len(votes))
+	for i := range votes {
+		dacVote, err := r.dacVoteFromDB(&votes[i])
+		if err != nil {
+			return nil, err
+		}
+		dacVotes[i] = dacVote
+	}
+	return dacVotes, nil
+}
+
+func (r *Reader) dacVoteFromDB(vote *db.DACVote) (models.DACVote, error) {
+	id, err := ids.ShortFromString(vote.VoterAddr)
+	if err != nil {
+		return models.DACVote{}, err
+	}
+
+	voterAddr, err := address.Format("P", constants.GetHRP(r.networkID), id[:])
+	if err != nil {
+		return models.DACVote{}, err
+	}
+
+	return models.DACVote{
+		VoteTxID:     vote.VoteTxID,
+		VoterAddr:    voterAddr,
+		VotedAt:      vote.VotedAt,
+		VotedOptions: vote.VotedOptions,
+	}, nil
+}
diff --git a/services/indexes/params/collections.go b/services/indexes/params/collections.go
index 2816a0d3..01c92554 100644
--- a/services/indexes/params/collections.go
+++ b/services/indexes/params/collections.go
@@ -1,4 +1,4 @@
-// Copyright (C) 2022, Chain4Travel AG. All rights reserved.
+// Copyright (C) 2022-2023, Chain4Travel AG. All rights reserved.
 //
 // This file is a derived work, based on ava-labs code whose
 // original notices appear below.
@@ -21,7 +21,6 @@ import (
 	"time"
 
 	"github.com/ava-labs/avalanchego/ids"
-	"github.com/chain4travel/magellan/db"
 	"github.com/chain4travel/magellan/models"
 	"github.com/gocraft/dbr/v2"
 )
@@ -45,6 +44,7 @@ var (
 	_ Param = &ListOutputsParams{}
 	_ Param = &ListCTransactionsParams{}
 	_ Param = &ListBlocksParams{}
+	_ Param = &ListDACProposalsParams{}
 )
 
 type SearchParams struct {
@@ -384,7 +384,7 @@ func (p *ListCTransactionsParams) CacheKey() []string {
 }
 
 func (p *ListCTransactionsParams) Apply(b *dbr.SelectBuilder) *dbr.SelectBuilder {
-	p.ListParams.ApplyPk(db.TableCvmTransactionsTxdata, b, "hash", false)
+	p.ListParams.ApplyPk("cvm_transactions_txdata", b, "hash", false)
 
 	return b
 }
@@ -801,3 +801,51 @@ func (p *ValidatorParams) SetParamInfo(v uint8, rpc string) error {
 func (p *ValidatorParams) CacheKey() []string {
 	return p.ListParams.CacheKey()
 }
+
+type ListDACProposalsParams struct {
+	ListParams
+
+	MinStartTime         time.Time
+	MaxStartTime         time.Time
+	MinStartTimeProvided bool
+	MaxStartTimeProvided bool
+
+	ProposalType   *models.ProposalType
+	ProposalStatus *models.ProposalStatus
+}
+
+func (p *ListDACProposalsParams) ForValues(v uint8, q url.Values) error {
+	val, err := GetQueryInt(q, KeyProposalType, -1)
+	if err != nil {
+		return err
+	}
+	if val != -1 {
+		p.ProposalType = new(models.ProposalType)
+		*p.ProposalType = models.ProposalType(val)
+	}
+
+	val, err = GetQueryInt(q, KeyProposalStatus, -1)
+	if err != nil {
+		return err
+	}
+	if val != -1 {
+		p.ProposalStatus = new(models.ProposalStatus)
+		*p.ProposalStatus = models.ProposalStatus(val)
+	}
+
+	p.MinStartTimeProvided, p.MinStartTime, err = GetQueryTime(q, KeyProposalMinStartTime)
+	if err != nil {
+		return err
+	}
+
+	p.MaxStartTimeProvided, p.MaxStartTime, err = GetQueryTime(q, KeyProposalMaxStartTime)
+	if err != nil {
+		return err
+	}
+
+	return p.ListParams.ForValuesAllowOffset(v, q)
+}
+
+func (p *ListDACProposalsParams) CacheKey() []string {
+	return p.ListParams.CacheKey()
+}
diff --git a/services/indexes/params/params.go b/services/indexes/params/params.go
index afd8b1bb..73ea2103 100644
--- a/services/indexes/params/params.go
+++ b/services/indexes/params/params.go
@@ -1,4 +1,4 @@
-// Copyright (C) 2022, Chain4Travel AG. All rights reserved.
+// Copyright (C) 2022-2023, Chain4Travel AG. All rights reserved.
 //
 // This file is a derived work, based on ava-labs code whose
 // original notices appear below.
@@ -24,31 +24,35 @@ import (
 )
 
 const (
-	KeyID               = "id"
-	KeyChainID          = "chainID"
-	KeyAddress          = "address"
-	KeyToAddress        = "toAddress"
-	KeyFromAddress      = "fromAddress"
-	KeyBlockStart       = "blockStart"
-	KeyBlockEnd         = "blockEnd"
-	KeyHash             = "hash"
-	KeyAlias            = "alias"
-	KeyAssetID          = "assetID"
-	KeySearchQuery      = "query"
-	KeySortBy           = "sort"
-	KeyLimit            = "limit"
-	KeyOffset           = "offset"
-	KeySpent            = "spent"
-	KeyStartTime        = "startTime"
-	KeyEndTime          = "endTime"
-	KeyIntervalSize     = "intervalSize"
-	KeyDisableCount     = "disableCount"
-	KeyDisableGenesis   = "disableGenesis"
-	KeyOutputOutputType = "outputOutputType"
-	KeyOutputGroupID    = "outputGroupId"
-	KeyTransactionID    = "transactionId"
-	KeyRPC              = "rpc"
-	KeyRaw              = "raw"
+	KeyID                   = "id"
+	KeyChainID              = "chainID"
+	KeyAddress              = "address"
+	KeyToAddress            = "toAddress"
+	KeyFromAddress          = "fromAddress"
+	KeyBlockStart           = "blockStart"
+	KeyBlockEnd             = "blockEnd"
+	KeyHash                 = "hash"
+	KeyAlias                = "alias"
+	KeyAssetID              = "assetID"
+	KeySearchQuery          = "query"
+	KeySortBy               = "sort"
+	KeyLimit                = "limit"
+	KeyOffset               = "offset"
+	KeySpent                = "spent"
+	KeyStartTime            = "startTime"
+	KeyEndTime              = "endTime"
+	KeyIntervalSize         = "intervalSize"
+	KeyDisableCount         = "disableCount"
+	KeyDisableGenesis       = "disableGenesis"
+	KeyOutputOutputType     = "outputOutputType"
+	KeyOutputGroupID        = "outputGroupId"
+	KeyTransactionID        = "transactionId"
+	KeyProposalType         = "proposalType"
+	KeyProposalStatus       = "proposalStatus"
+	KeyProposalMinStartTime = "minStartTime"
+	KeyProposalMaxStartTime = "maxStartTime"
+	KeyRPC                  = "rpc"
+	KeyRaw                  = "raw"
 
 	PaginationMaxLimit      = 5000
 	PaginationDefaultOffset = 0
diff --git a/services/indexes/pvm/writer.go b/services/indexes/pvm/writer.go
index d8211ef2..028861ef 100644
--- a/services/indexes/pvm/writer.go
+++ b/services/indexes/pvm/writer.go
@@ -1,3 +1,13 @@
+// Copyright (C) 2022-2023, Chain4Travel AG. All rights reserved.
+//
+// This file is a derived work, based on ava-labs code whose
+// original notices appear below.
+//
+// It is distributed under the same license conditions as the
+// original code from which it is derived.
+//
+// Much love to the original authors for their work.
+// **********************************************************
 // (c) 2021, Ava Labs, Inc. All rights reserved.
 // See the file LICENSE for licensing terms.
 
@@ -26,7 +36,9 @@ import (
 	"github.com/ava-labs/avalanchego/vms/components/avax"
 	"github.com/ava-labs/avalanchego/vms/components/multisig"
 	"github.com/ava-labs/avalanchego/vms/components/verify"
+	as "github.com/ava-labs/avalanchego/vms/platformvm/addrstate"
 	"github.com/ava-labs/avalanchego/vms/platformvm/blocks"
+	"github.com/ava-labs/avalanchego/vms/platformvm/dac"
 	"github.com/ava-labs/avalanchego/vms/platformvm/txs"
 	"github.com/ava-labs/avalanchego/vms/proposervm/block"
 	"github.com/ava-labs/avalanchego/vms/secp256k1fx"
@@ -178,7 +190,7 @@ func (w *Writer) Bootstrap(ctx context.Context, conns *utils.Connections, persis
 	txDupCheck := set.NewSet[ids.ID](2*len(gc.Genesis.Camino.AddressStates) +
 		2*len(gc.Genesis.Camino.ConsortiumMembersNodeIDs))
 
-	addressStateTx := func(addr ids.ShortID, state txs.AddressStateBit) *txs.Tx {
+	addressStateTx := func(addr ids.ShortID, state as.AddressStateBit) *txs.Tx {
 		tx := &txs.Tx{
 			Unsigned: &txs.AddressStateTx{
 				BaseTx: txs.BaseTx{
@@ -242,22 +254,22 @@ func (w *Writer) Bootstrap(ctx context.Context, conns *utils.Connections, persis
 		}
 	}
 
-	for _, as := range gc.Genesis.Camino.AddressStates {
+	for _, addrState := range gc.Genesis.Camino.AddressStates {
 		select {
 		case <-ctx.Done():
 		default:
 		}
 
-		if as.State&txs.AddressStateKYCVerified != 0 {
-			if tx := addressStateTx(as.Address, txs.AddressStateBitKYCVerified); tx != nil {
+		if addrState.State&as.AddressStateKYCVerified != 0 {
+			if tx := addressStateTx(addrState.Address, as.AddressStateBitKYCVerified); tx != nil {
 				err := w.indexTransaction(cCtx, ChainID, tx, true)
 				if err != nil {
 					return err
 				}
 			}
 		}
-		if as.State&txs.AddressStateConsortiumMember != 0 {
-			if tx := addressStateTx(as.Address, txs.AddressStateBitConsortium); tx != nil {
+		if addrState.State&as.AddressStateConsortiumMember != 0 {
+			if tx := addressStateTx(addrState.Address, as.AddressStateBitConsortium); tx != nil {
 				err := w.indexTransaction(cCtx, ChainID, tx, true)
 				if err != nil {
 					return err
@@ -444,6 +456,8 @@ func (w *Writer) indexCommonBlock(
 
 //nolint:gocyclo
 func (w *Writer) indexTransaction(ctx services.ConsumerCtx, blkID ids.ID, tx *txs.Tx, genesis bool) error {
+	// ctx.Time() isn't strictly correct chaintime (block time) for Apricot blocks,
+	// but we assume we won't have any of them
 	var (
 		txID   = tx.ID()
 		baseTx avax.BaseTx
@@ -590,6 +604,32 @@ func (w *Writer) indexTransaction(ctx services.ConsumerCtx, blkID ids.ID, tx *tx
 	case *txs.AddDepositOfferTx:
 		baseTx = castTx.BaseTx.BaseTx
 		typ = models.TransactionTypeAddDepositOffer
+	case *txs.AddProposalTx:
+		baseTx = castTx.BaseTx.BaseTx
+		typ = models.TransactionTypeAddDACProposal
+		proposal, err := castTx.Proposal()
+		if err != nil {
+			return err
+		}
+		if err := w.InsertDACProposal(ctx, proposal, castTx.ProposerAddress, txID); err != nil {
+			return err
+		}
+	case *txs.AddVoteTx:
+		baseTx = castTx.BaseTx.BaseTx
+		typ = models.TransactionTypeAddDACVote
+		vote, err := castTx.Vote()
+		if err != nil {
+			return err
+		}
+		if err := w.InsertDACVote(ctx, txID, vote, castTx.VoterAddress, ctx.Time(), castTx.ProposalID); err != nil {
+			return err
+		}
+	case *txs.FinishProposalsTx:
+		baseTx = castTx.BaseTx.BaseTx
+		typ = models.TransactionTypeFinishDACProposals
+		if err := w.FinishDACProposals(ctx, castTx, ctx.Time()); err != nil {
+			return err
+		}
 	default:
 		return fmt.Errorf("unknown tx type %T", castTx)
 	}
@@ -778,3 +818,211 @@ func persistMultisigAliasAddresses(ctx services.ConsumerCtx, addr ids.ShortID, c
 
 	return nil
 }
+
+type dacProposalWrapper struct {
+	dac.ProposalState `serialize:"true"`
+}
+
+func (w *Writer) InsertDACProposal(
+	ctx services.ConsumerCtx,
+	proposal dac.Proposal,
+	proposerAddr ids.ShortID,
+	txID ids.ID,
+) error {
+	proposalType, proposalOptions, proposalData, isAdminProposal, err := parseDACProposal(proposal, w.networkID)
+	if err != nil {
+		return err
+	}
+
+	wrapper := dacProposalWrapper{}
+	if isAdminProposal {
+		proposalState, err := proposal.CreateFinishedProposalState(0)
+		if err != nil {
+			return err
+		}
+		wrapper.ProposalState = proposalState
+	} else {
+		wrapper.ProposalState = proposal.CreateProposalState([]ids.ShortID{})
+	}
+	proposalBytes, err := dac.Codec.Marshal(txs.Version, &wrapper)
+	if err != nil {
+		return err
+	}
+
+	return ctx.Persist().InsertDACProposal(ctx.Ctx(), ctx.DB(), &db.DACProposal{
+		ID:              txID.String(),
+		ProposerAddr:    proposerAddr.String(),
+		StartTime:       proposal.StartTime(),
+		EndTime:         proposal.EndTime(),
+		Type:            proposalType,
+		IsAdminProposal: isAdminProposal,
+		SerializedBytes: proposalBytes,
+		Options:         proposalOptions,
+		Data:            proposalData,
+		Status:          models.ProposalStatusInProgress,
+	})
+}
+
+func (w *Writer) FinishDACProposals(ctx services.ConsumerCtx, tx *txs.FinishProposalsTx, finishedAt time.Time) error {
+	// Finishing successful proposals
+	successfulProposalIDsStrs := make([]string, 0, len(tx.EarlyFinishedSuccessfulProposalIDs)+len(tx.ExpiredSuccessfulProposalIDs))
+	for _, proposalID := range tx.EarlyFinishedSuccessfulProposalIDs {
+		successfulProposalIDsStrs = append(successfulProposalIDsStrs, proposalID.String())
+	}
+	for _, proposalID := range tx.ExpiredSuccessfulProposalIDs {
+		successfulProposalIDsStrs = append(successfulProposalIDsStrs, proposalID.String())
+	}
+	if len(successfulProposalIDsStrs) > 0 {
+		successfulProposals, err := ctx.Persist().GetDACProposals(ctx.Ctx(), ctx.DB(), successfulProposalIDsStrs)
+		if err != nil {
+			return err
+		}
+		for _, dbProposal := range successfulProposals {
+			proposal := dacProposalWrapper{}
+			if _, err := dac.Codec.Unmarshal(dbProposal.SerializedBytes, &proposal); err != nil {
+				return err
+			}
+
+			outcomeBytes, err := json.Marshal(proposal.Outcome())
+			if err != nil {
+				return err
+			}
+
+			if err := ctx.Persist().FinishDACProposalWithOutcome(
+				ctx.Ctx(),
+				ctx.DB(),
+				dbProposal.ID,
+				finishedAt,
+				models.ProposalStatusSuccess,
+				outcomeBytes,
+			); err != nil {
+				return err
+			}
+		}
+	}
+
+	// Finishing failed proposals
+	failedProposalIDsStrs := make([]string, 0, len(tx.EarlyFinishedFailedProposalIDs)+len(tx.ExpiredFailedProposalIDs))
+	for _, proposalID := range tx.EarlyFinishedFailedProposalIDs {
+		failedProposalIDsStrs = append(failedProposalIDsStrs, proposalID.String())
+	}
+	for _, proposalID := range tx.ExpiredFailedProposalIDs {
+		failedProposalIDsStrs = append(failedProposalIDsStrs, proposalID.String())
+	}
+	if len(failedProposalIDsStrs) == 0 {
+		return nil
+	}
+	return ctx.Persist().FinishDACProposals(ctx.Ctx(), ctx.DB(), failedProposalIDsStrs, finishedAt, models.ProposalStatusFailed)
+}
+
+func (w *Writer) InsertDACVote(
+	ctx services.ConsumerCtx,
+	voteTxID ids.ID,
+	vote dac.Vote,
+	voterAddr ids.ShortID,
+	votedAt time.Time,
+	proposalID ids.ID,
+) error {
+	options, err := json.Marshal(vote.VotedOptions())
+	if err != nil {
+		return err
+	}
+
+	proposals, err := ctx.Persist().GetDACProposals(ctx.Ctx(), ctx.DB(), []string{proposalID.String()})
+	switch {
+	case err != nil:
+		return err
+	case len(proposals) == 0:
+		return dbr.ErrNotFound
+	case len(proposals) != 1:
+		return errors.New("db returned multiple proposals for one proposalID") // should never happen
+	}
+
+	wrapper := dacProposalWrapper{}
+	if _, err := dac.Codec.Unmarshal(proposals[0].SerializedBytes, &wrapper); err != nil {
+		return err
+	}
+
+	updatedProposal, err := wrapper.ForceAddVote(vote)
+	if err != nil {
+		return err
+	}
+
+	wrapper.ProposalState = updatedProposal
+	proposalBytes, err := dac.Codec.Marshal(txs.Version, &wrapper)
+	if err != nil {
+		return err
+	}
+
+	if err := ctx.Persist().UpdateDACProposal(
+		ctx.Ctx(),
+		ctx.DB(),
+		proposalID.String(),
+		proposalBytes,
+	); err != nil {
+		return err
+	}
+
+	return ctx.Persist().InsertDACVote(ctx.Ctx(), ctx.DB(), &db.DACVote{
+		VoterAddr:    voterAddr.String(),
+		VotedAt:      votedAt,
+		VoteTxID:     voteTxID.String(),
+		ProposalID:   proposalID.String(),
+		VotedOptions: options,
+	})
+}
+
+func parseDACProposal(
+	proposal dac.Proposal, networkID uint32,
+) (
+	proposalType models.ProposalType,
+	options []byte,
+	data []byte,
+	isAdminProposal bool,
+	err error,
+) {
+	adminProposal, isAdminProposal := proposal.(*dac.AdminProposal)
+	if isAdminProposal {
+		proposal = adminProposal.Proposal
+	}
+
+	var proposalData any
+	switch proposal := proposal.(type) {
+	case *dac.BaseFeeProposal:
+		proposalType = models.ProposalTypeBaseFee
+	case *dac.AddMemberProposal:
+		proposalType = models.ProposalTypeAddMember
+		applicantAddress, err := address.Format("P", constants.GetHRP(networkID), proposal.ApplicantAddress[:])
+		if err != nil {
+			return 0, nil, nil, false, err
+		}
+		proposalData = applicantAddress
+	case *dac.ExcludeMemberProposal:
+		proposalType = models.ProposalTypeExcludeMember
+		memberAddress, err := address.Format("P", constants.GetHRP(networkID), proposal.MemberAddress[:])
+		if err != nil {
+			return 0, nil, nil, false, err
+		}
+		proposalData = memberAddress
+	case *dac.GeneralProposal:
+		proposalType = models.ProposalTypeGeneral
+	case *dac.FeeDistributionProposal:
+		proposalType = models.ProposalTypeFeeDistribution
+	default:
+		return 0, nil, nil, false, fmt.Errorf("unknown proposal type: %T", proposal)
+	}
+
+	options, err = json.Marshal(proposal.GetOptions()) // always not nil
+	if err != nil {
+		return 0, nil, nil, false, err
+	}
+
+	if proposalData != nil {
+		data, err = json.Marshal(proposalData)
+		if err != nil {
+			return 0, nil, nil, false, err
+		}
+	}
+
+	return proposalType, options, data, isAdminProposal, nil
+}