WEB3DEV

Cover image for Como configurar um contrato inteligente de Votação na Flow
Isabela Curado Nehme
Isabela Curado Nehme

Posted on

Como configurar um contrato inteligente de Votação na Flow

06 de março de 2023

O seguinte artigo faz parte de uma série de artigos para ajudar você a construir uma DAO na blockchain Flor usando a linguagem Cadence.

As organizações autônomas descentralizadas (DAOs) são um elemento básico da Web3. Nativas da internet e baseadas em blockchain, as DAOs destinam-se a fornecer uma nova e democratizada estrutura de gerenciamento para negócios, projetos e comunidades, na qual qualquer membro pode votar em propostas apenas comprando uma participação no projeto.

Em artigos anteriores, expliquei como criar um token de governança e uma venda pública inicial. Neste artigo, vou guiá-lo passo a passo sobre como construir um contrato inteligente de governança simples na blockchain Flow.

Para acompanhar este tutorial, você precisará do seguinte:

NodeJs e NPM;
Linguagem Go para os testes;
A interface de linha de comando da Flow (Flow CLI - Command Line Interface);
Seu IDE (Integrated Development Environment - ambiente de desenvolvimento integrado) favorito.

Vamos começar!

Primeiro, vamos definir o que queremos realizar: desenvolver um contrato inteligente onde os membros de uma DAO possam criar e votar em propostas. É isso. Simples o suficiente, certo? Bem, para conseguir isso, precisaremos concluir algumas etapas, a primeira será definir a estrutura do nosso objeto de proposta (Proposal).

Então, o que cada objeto Proposal precisa? Ele precisa de um Id, um Proposer (proponente), um Title (título), uma Description (descrição), uma lista de Options (opções), uma lista que conta os Votes (votos) para cada opção e outras variáveis em torno do tempo de criação e conclusão. Além disso, seria necessário especificar o requisito para votação, neste caso, é a quantidade mínima de tokens de governança que precisam ser mantidos pelo voter (eleitor) para votar. Vamos também adicionar um countIndex e uma variável que nos permite saber se a proposta ainda é válida ou não.

import NonFungibleToken from "../utility/NonFungibleToken.cdc"
import FungibleToken from "../utility/FungibleToken.cdc"
import GorvernanceToken from "../GovernanceToken.cdc"

pub contract ExampleDAO {

  pub struct Proposal {
    pub let id: Int;
    pub let proposer: Address
    pub var title: String
    pub var description: String
    pub var options: [String]
    // options index <-> result mapping
    pub var votesCountActual: [UInt64]
    pub let createdAt: UFix64
    pub var updatedAt: UFix64
    pub var startAt: UFix64
    pub var endAt: UFix64
    pub var sealed: Bool
    pub var countIndex: Int
    pub var voided: Bool
    pub let minHoldedGVTAmount: UFix64

    init(proposer: Address, title: String, description: String, options: [String], startAt: UFix64?, endAt: UFix64?, minHoldedGVTAmount: UFix64?) {
      pre {
        title.length <= 1000: "New title too long"
        description.length <= 1000: "New description too long"
      }

      self.proposer = proposer
      self.title = title
      self.options = options
      self.description = description
      self.votesCountActual = []
      self.minHoldedGVTAmount = minHoldedGVTAmount != nil ? minHoldedGVTAmount! : 0.0
      for option in options {
        self.votesCountActual.append(0)
      }
      self.id = ExampleDAO.totalProposals
      self.sealed = false
      self.countIndex = 0
      self.createdAt = getCurrentBlock().timestamp
      self.updatedAt = getCurrentBlock().timestamp
      self.startAt = startAt != nil ? startAt! : getCurrentBlock().timestamp
      self.endAt = endAt != nil ? endAt! : self.createdAt + 86400.0 * 14.0 // Around a year
      self.voided = false
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
# Estrutura básica do objeto Proposal

Aí está! Nosso objeto Proposal tem todos os elementos que precisamos para acompanhar, mas é bastante inútil agora; não temos nenhuma funcionalidade em torno dele. Precisamos adicionar uma maneira de votar nele, certo? E se alguém cometeu um erro? Também precisamos de uma maneira de atualizar as propostas!

pub fun update(title: String?, description: String?, startAt: UFix64?, endAt: UFix64?, voided: Bool?) {
      pre {
        title?.length ?? 0 <= 1000: "Title too long"
        description?.length ?? 0 <= 1000: "Description too long"
        voided != true: "Can't update after started"
        getCurrentBlock().timestamp < self.startAt: "Can't update after started"
      }

      self.title = title != nil ? title! : self.title
      self.description = description != nil ? description! : self.description
      self.endAt = endAt != nil ? endAt! : self.endAt
      self.startAt = startAt != nil ? startAt! : self.startAt
      self.voided = voided != nil ? voided! : self.voided
      self.updatedAt = getCurrentBlock().timestamp
    }

    pub fun vote(voterAddr: Address, optionIndex: Int) {
      pre {
        self.isStarted(): "Vote not started"
        !self.isEnded(): "Vote ended"
        ExampleDAO.votedRecords[self.id][voterAddr] == nil: "Already voted"
      }

      ExampleDAO.votedRecords[self.id][voterAddr] = optionIndex
    }
Enter fullscreen mode Exit fullscreen mode
# Funcionalidade básica da proposta (vai dentro da estrutura da proposta).

Agora, sei o que você está pensando: parece que qualquer um pode votar na nossa proposta, mesmo que não seja um membro da DAO! E é verdade, até agora não implementamos nenhum requisito para votar nas propostas da nossa DAO, mas chegaremos a isso em breve. Por enquanto, queremos apenas desenvolver a fundação do nosso contrato e adicionaremos alguma complexidade mais tarde.

Além disso, existem algumas variáveis de contrato que ainda não definimos, você notou? São votedRecords e totalProposals e precisamos delas para acompanhar os votos de todas as propostas já feitas. Também precisamos que nosso contrato acompanhe todos os registros de votação, então vamos adicioná-los e inicializá-los!

pub contract ExampleDAO {
  access(contract) var Proposals: [Proposal]
  access(contract) var votedRecords: [{ Address: Int }]
  access(contract) var totalProposals: Int

  pub let AdminStoragePath: StoragePath;

  // O administrador proprietário do recurso pode criar Proposers (propostas)
  pub resource Admin {
    pub fun createProposer(): @ExampleDAO.Proposer {
      return <- create Proposer()
    }
  }

  //// Estrutura Proposal ////

  init () {
  self.Proposals = []
  self.votedRecords = []
  self.totalProposals = 0

  self.AdminStoragePath = /storage/ExampleDAOAdmin
  self.account.save(<-create Admin(), to: self.AdminStoragePath)

}
Enter fullscreen mode Exit fullscreen mode
# Inicializando as variáveis de contrato.

Nós passamos à frente e também adicionamos o caminho para o recurso admin e sua funcionalidade, que é criar proponentes; um recurso que ainda não definimos, mas estamos prestes a fazê-lo!

Precisamos de um recurso Proposer para limitar quem pode criar propostas em nossa DAO para apenas aqueles que detêm o referido recurso. Isso faz sentido, certo? Mas aqui está o problema: se apenas o admin pode criar proposers, como podemos transferir esse recurso para outra pessoa da conta de administrador? E uma vez feito isso, o que acontece se, mais tarde, esse membro se tornar um agente mal-intencionado e a DAO não quiser mais que ele possa criar mais propostas? Precisamos de uma maneira de transferir esse recurso com segurança, sem usar multiassinaturas, e também precisamos de alguma funcionalidade que nos permita remover esse poder de um agente mal-intencionado. Para resolver isso, eu vim com uma solução de proxy.

Vamos criar um objeto de recurso proposerProxy contendo uma capacidade que pode ser usada para criar novas propostas. O recurso que essa capacidade representa pode ser excluído pelo admin a fim de revogar unilateralmente a capacidade do proposer, se necessário. Vamos dar uma olhada no código.

 // Proposer
    //
    // Objeto de recurso que pode criar novas propostas.
    // O administrador armazena isso e passa para a conta do Proponente como um recurso de empacotador (wrapper) de capacidade.
    //
    pub resource Proposer {

        // Função que cria novas propostas.
        //
      pub fun addProposal(
        title: String,
        description: String,
        options: [String],
        startAt: UFix64?,
        endAt: UFix64?,
        minHoldedGVTAmount: UFix64?
        ) {

        ExampleDAO.Proposals.append(Proposal(
         proposer: self.owner!.address,
         title: title,
         description: description,
         options: options,
         startAt: startAt,
         endAt: endAt,
         minHoldedGVTAmount: minHoldedGVTAmount
        ))

        ExampleDAO.votedRecords.append({})
        ExampleDAO.totalProposals = ExampleDAO.totalProposals + 1
      }

      pub fun updateProposal(
        id: Int,
        title: String?,
        description: String?,
        startAt: UFix64?,
        endAt: UFix64?,
        voided: Bool?
        ) {

        pre {
          ExampleDAO.Proposals[id].proposer == self.owner!.address: "Only original proposer can update"
        }

        BlockVersityDAO.Proposals[id].update(
          title: title,
          description: description,
          startAt: startAt,
          endAt: endAt,
          voided: voided
        )
      }
     }

    pub resource interface ProposerProxyPublic {
        pub fun setProposerCapability(capability: Capability<&Proposer>)
    }
    // ProposerProxy
    //
    // Objeto de recurso contendo a capacidade que pode ser usada para criar novas propostas.
    // O recurso que essa capacidade representa pode ser excluído pelo administrador (admin)
    // a fim de revogar unilateralmente a capacidade do proponente, se necessário.
    pub resource ProposerProxy: ProposerProxyPublic {
        // access(self) para que ninguém mais possa copiar a capacidade e usá-la.
        access(self) var ProposerCapability: Capability<&Proposer>?
        // Qualquer um pode chamar isto, mas apenas o administrador (admin) pode criar capacidades de proponente,
        // então o sistema de tipos restringe isso a ser chamado pelo administrador.
        pub fun setProposerCapability(capability: Capability<&Proposer>) {
            self.ProposerCapability = capability
        }

        pub fun addProposal(
          _title: String,
          _description: String,
          _options: [String],
          _startAt: UFix64?,
          _endAt: UFix64?,
          _minHoldedGVTAmount: UFix64?
          ): Void? {

            return self.ProposerCapability
            ?.borrow()!
            ?.addProposal(
              title: _title,
              description: _description,
              options: _options,
              startAt: _startAt,
              endAt: _endAt,
              minHoldedGVTAmount: _minHoldedGVTAmount
              )
        }

        pub fun updateProposal(
          id: Int,
          title: String?,
          description: String?,
          startAt: UFix64?,
          endAt: UFix64?,
          voided: Bool?
          ) {

          return self.ProposerCapability!
          .borrow()!
          .updateProposal(
            id: id,
            title: title,
            description: description,
            startAt: startAt,
            endAt: endAt,
            voided: voided
            )
        }

        init () {
          self.ProposerCapability = nil
        }

    }
    // createProposerProxy
    //
    // Função que cria um ProposerProxy.
    // Qualquer um pode chamar isso, mas o ProposerProxy não pode
    // criar propostas sem uma capacidade de proponente,
    // e apenas o administrador pode fornecer isso.
    //
    pub fun createProposerProxy(): @ProposerProxy {
        return <- create ProposerProxy()
    }
Enter fullscreen mode Exit fullscreen mode
# Funcionalidade de recurso do proponente.

Ufa, tem muita coisa acontecendo aqui. Deixamos alguns comentários no código para guiá-lo e ajudá-lo a entender como tudo está conectado. Até este ponto, devemos ser capazes de testar a funcionalidade do nosso contrato. Se você veio deste artigo, já possui os requisitos mínimos para começar a testar os contratos inteligentes da Cadence. Caso contrário, basta seguir estas etapas simples para começar:

  1. A primeira coisa que faremos é criar um diretório de teste com mkdir blockversity-overflow && cd blockversity-overflow.. Em seguida, iniciaremos o ambiente da Flow com flow init.
  2. Você precisará configurar seu arquivo flow.json com os contratos inteligentes que quer testar. Para resolver isso rapidamente, copie e cole as configurações neste repositório: DAO - Example branch flow.json.
  3. Depois, iniciamos o módulo Go e instalamos o overflow nele. Aqui está o exemplo de código Bash (substitua “blockversity/dao-overflow” pelo nome do arquivo):
  • go mod init blockversity/dao-overflow
  • go get github.com/bjartek/overflow

Em seguida, criamos um arquivo de teste chamado blockVersityDAO_test.go e importamos os pacotes que usaremos nele. Dessa maneira:

package main

import (
"fmt"
"testing"

. "github.com/bjartek/overflow"
"github.com/fatih/color"
"github.com/stretchr/testify/assert"
)

func TestAdminICO(t *testing.T) {

o, err := OverflowTesting()
assert.NoError(t, err)

}
Enter fullscreen mode Exit fullscreen mode

Agora, queremos testar os scripts e transações da Cadence com o overflow para testar as funcionalidades de nossos recursos Admin e Proposer, mas primeiro adicionaremos os scripts e transações que usaremos. O primeiro passo é configurar o proposerProxy na sua conta de membro da DAO.

Essa transação cria um novo recurso de proxy do “Proposer” e armazena-o na conta do signatário. Depois de executar essa transação, o administrador da DAO deve executar depositProposer.cdc para depositar um recurso de “Proposer” dentro do proxy do “Proposer”.

import ExampleDAO from "../../contracts/DAO/DaoTest.cdc"

transaction {

    prepare(Member: AuthAccount) {

        let proposerProxy <- ExampleDAO.createProposerProxy()

        Member.save(
            <- proposerProxy,
            to: ExampleDAO.ProposerProxyStoragePath,
        )

        Member.link<&ExampleDAO.ProposerProxy{ExampleDAO.ProposerProxyPublic}>(
            ExampleDAO.ProposerProxyPublicPath,
            target: ExampleDAO.ProposerProxyStoragePath
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

Agora, escrevemos a transação depositProposer, que só pode ser executada pelo admin da DAO. Essa transação cria um novo “DAO Proposer” (proponente da DAO) e o deposita em um recurso de proxy do “Proposer” existente na conta especificada. Isso leva um proposerAddress como um parâmetro. No exemplo atual, essa transação falhará se authAccount não tiver o recurso BlockVersityDAO.Administrator. Essa transação também falhará se a conta Proposer não tiver um recurso BlockVersityDAO.ProposerProxy.

import ExampleDAO from "../../contracts/DAO/DaoTest.cdc"

transaction(proposerAddress: Address) {

    let resourceStoragePath: StoragePath
    let capabilityPrivatePath: CapabilityPath
    let proposerCapability: Capability<&ExampleDAO.Proposer>

    prepare(adminAccount: AuthAccount) {

        // Esses caminhos devem ser exclusivos dentro do armazenamento da conta do contrato ExampleDAO
        self.resourceStoragePath = /storage/proposer_01     // e.g. /storage/proposer_01
        self.capabilityPrivatePath = /private/proposer_01 // e.g. private/proposer_01
        // Cria uma referência ao recurso do administrador no armazenamento.
        let tokenAdmin = adminAccount.borrow<&ExampleDAO.Admin>(from: ExampleDAO.AdminStoragePath)
            ?? panic("Could not borrow a reference to the admin resource")

        // Cria um novo recurso Proposer e um link privado para uma capacidade para ele no armazenamento do administrador.
        let proposer <- tokenAdmin.createProposer()
        adminAccount.save(<- proposer, to: self.resourceStoragePath)
        self.proposerCapability = adminAccount.link<&ExampleDAO.Proposer>(
            self.capabilityPrivatePath,
            target: self.resourceStoragePath
        ) ?? panic("Could not link Proposer")

    }

    execute {
        // Esta é a conta para a qual a capacidade será dada.
        let proposerAccount = getAccount(proposerAddress)

        let capabilityReceiver = proposerAccount.getCapability
            <&ExampleDAO.ProposerProxy{BlockVersityDAO.ProposerProxyPublic}>
            (ExampleDAO.ProposerProxyPublicPath)
            .borrow() ?? panic("Could not borrow capability receiver reference")

        capabilityReceiver.setProposerCapability(capability: self.proposerCapability)
    }

}
Enter fullscreen mode Exit fullscreen mode

E é isso! Esta conta está finalmente pronta para fazer algumas propostas na DAO. Claro, para fazer isso, teremos que escrever mais uma transação e essa é a transação makeProposal. Vamos deixá-la bem aqui:

import ExampleDAO from "../../contracts/DAO/DaoTest.cdc"

transaction(
  title: String,
  description: String,
  options: [String],
  startAt: UFix64,
  endAt: UFix64,
  minHoldedGVTAmount: UFix64?
  ) {
  let proposer: &ExampleDAO.ProposerProxy?
  let minHoldedGVTAmount:UFix64?

  prepare(signer: AuthAccount) {
    self.proposer = signer
        .borrow<&BlockVersityDAO.ProposerProxy>(from: ExampleDAO.ProposerProxyStoragePath)
        ?? panic("No Proposer Proxy available")


    self.minHoldedGVTAmount = minHoldedGVTAmount != nil ? minHoldedGVTAmount! : 0.0
  }



  execute {
    self.proposer?.addProposal(
      _title: title,
      _description: description,
      _options: options,
      _startAt: startAt,
      _endAt: endAt,
      _minHoldedGVTAmount: self.minHoldedGVTAmount
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

E com isso estamos prontos para começar a construir nosso conjunto de teste para o recurso Proposer, que é a primeira parte principal do nosso contrato DAO. É assim que as coisas deveriam funcionar:

  • Bob deposita um ProposerProxy dentro de sua conta.
  • Bob tenta fazer uma proposta sem o recurso Proposer e falha.
  • O administrador deposita um recurso Proposer dentro da conta de Bob.
  • Bob cria uma proposta com sucesso.
package main

import (
    "fmt"
    "testing"

    . "github.com/bjartek/overflow"
    "github.com/fatih/color"
    "github.com/stretchr/testify/assert"
)

func TestProposers(t *testing.T) {

    o, err := OverflowTesting()
    assert.NoError(t, err)

    fmt.Println("Testing Proposer creation and interactions")
    fmt.Println("Press any key to continue")
    fmt.Scanln()

    color.Red("Should be able setup a ProposerProxy inside Bob's account")
    o.Tx("DAO/setupProposerProxy",
        WithSigner("bob")).AssertSuccess(t).Print()
    color.Green("Pass")

    color.Red("Bob will attempt to make a proposal with the Proxy, but fails")
    o.Tx("DAO/createProposal",
        WithSigner("bob"),
        WithArg("title", "How much $BVT tokens grant should the BlockVersity ecosystem fund allocate in support for Ukraine?"),
        WithArg("description", "BlockVersity is dedicated to stop the doomsday clock from moving any closer to midnight, at any cost."),
        WithArg("options", `["200K $BVT", "600K $BVT", "1000K $BVT"]`),
        WithArg("startAt", "1641373200.0"),
        WithArg("endAt", "1759546000.0"),
        WithArg("minHoldedBVTAmount", "100.0"),
    ).AssertFailure(t, "unexpectedly found nil while forcing an Optional value")
    color.Green("Pass")

    color.Red("Admin should be able deposit a Proposer resource inside Bob's account")
    o.Tx("DAO/depositProposer",
        WithSigner("account"),
        WithArg("proposerAddress", "bob"),
    ).AssertSuccess(t).Print()
    color.Green("Pass")

    color.Red("Bob will attempt to make a proposal again, and succeeds")
    o.Tx("DAO/createProposal",
        WithSigner("bob"),
        WithArg("title", "How much $BVT tokens grant should the BlockVersity ecosystem fund allocate in support for Ukraine?"),
        WithArg("description", "BlockVersity is dedicated to stop the doomsday clock from moving any closer to midnight, at any cost."),
        WithArg("options", `["200K $BVT", "600K $BVT", "1000K $BVT"]`),
        WithArg("startAt", "1641373200.0"),
        WithArg("endAt", "1759546000.0"),
        WithArg("minHoldedBVTAmount", "100.0"),
    ).AssertSuccess(t).Print()
    color.Green("Pass")
}
Enter fullscreen mode Exit fullscreen mode

Depois de executar este teste no VSCode, devemos esperar as seguintes mensagens em nosso terminal:

# Passou!

Todos os testes passam, o que significa que nosso contrato, transações e conjuntos de teste são um sucesso! E agora que completamos a funcionalidade do recurso Proposer, é hora de continuar o trabalho no contrato. É assim que tudo deve parecer até agora:

import NonFungibleToken from "../utility/NonFungibleToken.cdc"
import FungibleToken from "../utility/FungibleToken.cdc"
import GovernanceToken from "../GovernanceToken.cdc"

pub contract ExampleDAO {
  access(contract) var Proposals: [Proposal]
  access(contract) var votedRecords: [{ Address: Int }]
  access(contract) var totalProposals: Int

  pub let AdminStoragePath: StoragePath;
  pub let ProposerStoragePath: StoragePath;
  // O caminho de armazenamento para o ProposerProxy do Proposer
  pub let ProposerProxyStoragePath: StoragePath

  // O caminho público para a capacidade ProposerProxy do Proposer
  pub let ProposerProxyPublicPath: PublicPath

  // O administrador titular do recurso pode criar propostas
  pub resource Admin {
    pub fun createProposer(): @ExampleDAO.Proposer {
      return <- create Proposer()
    }
  }

  // Proposer 
    //
    // Objeto de recurso que pode criar novas propostas.
    // O administrador armazena-o e passa para a conta do Proposer como um recurso de empacotamento de capacidade.
    //
    pub resource Proposer {

        // Função que cria novas propostas.
        //
      pub fun addProposal(
        title: String,
        description: String,
        options: [String],
        startAt: UFix64?,
        endAt: UFix64?,
        minHoldedGVTAmount: UFix64?
        ) {

        ExampleDAO.Proposals.append(Proposal(
         proposer: self.owner!.address,
         title: title,
         description: description,
         options: options,
         startAt: startAt,
         endAt: endAt,
         minHoldedGVTAmount: minHoldedGVTAmount
        ))

        ExampleDAO.votedRecords.append({})
        ExampleDAO.totalProposals = ExampleDAO.totalProposals + 1
      }

      pub fun updateProposal(
        id: Int,
        title: String?,
        description: String?,
        startAt: UFix64?,
        endAt: UFix64?,
        voided: Bool?
        ) {

        pre {
          ExampleDAO.Proposals[id].proposer == self.owner!.address: "Only original proposer can update"
        }

        ExampleDAO.Proposals[id].update(
          title: title,
          description: description,
          startAt: startAt,
          endAt: endAt,
          voided: voided
        )
      }
     }

    pub resource interface ProposerProxyPublic {
        pub fun setProposerCapability(capability: Capability<&Proposer>)
    }
    // ProposerProxy
    //
    // Objeto de recurso contendo a capacidade que pode ser usada para criar novas propostas.
    // O recurso que essa capacidade representa pode ser excluído pelo administrador.
    // a fim de que a capacidade de Proposer (proponente) possa ser unilateralmente revogada caso necessário.
    pub resource ProposerProxy: ProposerProxyPublic {
        // access(self) para que ninguém possa copiar a capacidade e usá-la.
        access(self) var ProposerCapability: Capability<&Proposer>?
        // Qualquer um pode chamar isso, mas apenas o administrador pode criar capacidades de Proposer,
        // para que o sistema de tipo o restrinja a ser chamado pelo administrador.
        pub fun setProposerCapability(capability: Capability<&Proposer>) {
            self.ProposerCapability = capability
        }

        pub fun addProposal(
          _title: String,
          _description: String,
          _options: [String],
          _startAt: UFix64?,
          _endAt: UFix64?,
          _minHoldedGVTAmount: UFix64?
          ): Void? {

            return self.ProposerCapability
            ?.borrow()!
            ?.addProposal(
              title: _title,
              description: _description,
              options: _options,
              startAt: _startAt,
              endAt: _endAt,
              minHoldedGVTAmount: _minHoldedGVTAmount
              )
        }

        pub fun updateProposal(
          id: Int,
          title: String?,
          description: String?,
          startAt: UFix64?,
          endAt: UFix64?,
          voided: Bool?
          ) {

          return self.ProposerCapability!
          .borrow()!
          .updateProposal(
            id: id,
            title: title,
            description: description,
            startAt: startAt,
            endAt: endAt,
            voided: voided
            )
        }

        init () {
          self.ProposerCapability = nil
        }

    }
    // createProposerProxy
    //
    // Função que cria um ProposerProxy.
    // Qualquer um pode chama isso, mas o ProposerProxy não pode
    // criar propostas sem a capacidade Proposer,
    // e apenas o administrador pode fornecer isso.
    //
    pub fun createProposerProxy(): @ProposerProxy {
        return <- create ProposerProxy()
    }

  pub struct Proposal {
  pub let id: Int;
  pub let proposer: Address
  pub var title: String
  pub var description: String
  pub var options: [String]
  // options index <-> result mapping
  pub var votesCountActual: [UInt64]
  pub let createdAt: UFix64
  pub var updatedAt: UFix64
  pub var startAt: UFix64
  pub var endAt: UFix64
  pub var sealed: Bool
  pub var countIndex: Int
  pub var voided: Bool
  pub let minHoldedBVTAmount: UFix64

  init(proposer: Address, title: String, description: String, options: [String], startAt: UFix64?, endAt: UFix64?, minHoldedGVTAmount: UFix64?) {
    pre {
      title.length <= 1000: "New title too long"
      description.length <= 1000: "New description too long"
    }

    self.proposer = proposer
    self.title = title
    self.options = options
    self.description = description
    self.votesCountActual = []
    self.minHoldedGVTAmount = minHoldedGVTAmount != nil ? minHoldedGVTAmount! : 0.0

    for option in options {
      self.votesCountActual.append(0)
    }

    self.id =  ExampleDAO.totalProposals

    self.sealed = false
    self.countIndex = 0

    self.createdAt = getCurrentBlock().timestamp
    self.updatedAt = getCurrentBlock().timestamp

    self.startAt = startAt != nil ? startAt! : getCurrentBlock().timestamp
    self.endAt = endAt != nil ? endAt! : self.createdAt + 86400.0 * 14.0 // Around a year

    self.voided = false
  }

    pub fun update(title: String?, description: String?, startAt: UFix64?, endAt: UFix64?, voided: Bool?) {
      pre {
        title?.length ?? 0 <= 1000: "Title too long"
        description?.length ?? 0 <= 1000: "Description too long"
        voided != true: "Can't update after started"
        getCurrentBlock().timestamp < self.startAt: "Can't update after started"
      }

      self.title = title != nil ? title! : self.title
      self.description = description != nil ? description! : self.description
      self.endAt = endAt != nil ? endAt! : self.endAt
      self.startAt = startAt != nil ? startAt! : self.startAt
      self.voided = voided != nil ? voided! : self.voided
      self.updatedAt = getCurrentBlock().timestamp
    }

  }

  init () {
    self.Proposals = []
    self.votedRecords = []
    self.totalProposals = 0

    self.AdminStoragePath = /storage/ExampleDAOAdmin
    self.ProposerStoragePath = /storage/ExampleDAOProposer
    self.ProposerProxyPublicPath = /public/ExampleDAOProposerProxy
    self.ProposerProxyStoragePath = /storage/ExampleDAOProposerProxy

    self.account.save(<-create Admin(), to: self.AdminStoragePath)
    self.account.save(<-create Proposer(), to: self.ProposerStoragePath)
  }
}
Enter fullscreen mode Exit fullscreen mode
# O código DAO completo até agora.

O que este contrato precisa agora é uma maneira de permitir que as pessoas votem nas propostas e um sistema para contas esses votos. Assim como no recurso Proposer, criaremos um recurso Voter, mas não será tão complexo quanto o Proposer. Para este exemplo, qualquer um pode ser eleitor! Desde que detenham a quantidade necessária de ** tokens de governança** (BVT, neste caso) dentro de suas contas. Vamos fazer uma pequena lista do que o nosso recurso Eleitor precisa ser capaz de realizar:

  • Ele precisa ser capaz de votar em uma proposta específica.
  • Ele precisa ser capaz de buscar as opções nas quais esta conta votou (em uma proposta específica).
  • Uma função pública genérica para buscar todas as opções nas quais um eleitor já votou.

Parece bastante simples, mas definir a funcionalidade voting dentro do recurso Voter não seria muito eficiente e, na verdade, não há como ela interagir com nosso objeto Proposal. Por esse motivo, definiremos uma função vote dentro da própria estrutura Proposal.

Até agora, ela tem apenas duas funções públicas: uma para criar propostas e outra para atualizá-las. Agora, adicionaremos mais cinco funções públicas, que são: vote, count, isEnded, isStarted e getTotalVoted. Vamos dar uma olhada no código.

    pub fun vote(voterAddr: Address, optionIndex: Int) {
      pre {
        self.isStarted(): "Vote not started"
        !self.isEnded(): "Vote ended"
        ExampleDAO.votedRecords[self.id][voterAddr] == nil: "Already voted"
      }

      let voterGVT = ExampleDAO.getHoldedGVT(address: voterAddr)

      assert(voterGVT >= self.minHoldedGVTAmount, message: "Not enought GVT in your Vault to vote")

      ExampleDAO.votedRecords[self.id][voterAddr] = optionIndex
    }

Enter fullscreen mode Exit fullscreen mode
# Função para votar na proposta.

Nossa função vote é bem auto-explicativa: estamos nos certificando de que a proposta ainda está em andamento, que esta conta não votou ainda e então buscamos quantos tokens de governança (BVT, neste caso) que ela detém (com uma função que adicionaremos em breve). Se o usuário não tiver tokens de governança suficientes, ela para o processo. Se o usuário tem a quantidade mínima de tokens de governança requerida pela proposta, então o voto passa. Simples!

Vamos verificar a função count (de contagem).

   pub fun count(size: Int): [UInt64] {
      if self.isEnded() == false {
        return self.votesCountActual
      }
      if self.sealed {
        return self.votesCountActual
      }
      // Busca as chaves de todos que votaram nesta proposta
      let votedList = ExampleDAO.votedRecords[self.id].keys
      // Conta a partir da última vez que você contou
      var batchEnd = self.countIndex + size
      // Se o índice da contagem for maior que o número de eleitores
      // define o índice de contagem para o número de eleitores
      if batchEnd > votedList.length {
        batchEnd = votedList.length
      }

      while self.countIndex != batchEnd {
        let address = votedList[self.countIndex]
        let votedOptionIndex = ExampleDAO.votedRecords[self.id][address]!
    self.votesCountActual[votedOptionIndex] = self.votesCountActual[votedOptionIndex] + 1

        self.countIndex = self.countIndex + 1
      }

      self.sealed = self.countIndex == votedList.length

      return self.votesCountActual
    }
Enter fullscreen mode Exit fullscreen mode
# Função para contar os votos na proposta.

Iremos passo a passo sobre o que está acontecendo aqui.

  1. Verificamos se a proposta está finalizada ou está selada. Se sim, retornaremos a última variável votesCountActual.
  2. Buscamos uma lista de todos os endereços que votaram nesta proposta e criamos um contador para acompanhar seu comprimento.
  3. Usando um loop while, examinamos cada endereço e escolhemos em qual opção eles votaram e adicionamos à variável votesCountActual.
  4. Retornamos a contagem total para cada opção.

E é isso! Isso é tudo que essa função faz. Agora, vamos adicionar o resto, que é bem simples de desenvolver.

   pub fun isEnded(): Bool {
      return getCurrentBlock().timestamp >= self.endAt
    }

    pub fun isStarted(): Bool {
      return getCurrentBlock().timestamp >= self.startAt
    }

    pub fun getTotalVoted(): Int {
      return ExampleDAO.votedRecords[self.id].keys.length
    }
Enter fullscreen mode Exit fullscreen mode

Aqui vamos nós. Agora, nossa estrutura proposal está completa e pronta para começar a trabalhar! É hora de voltar ao recurso Voter (eleitor) do qual estávamos falando há pouco. Você se lembra do que ele precisava fazer? Precisava ser capaz de votar! E, para isso, será necessário utilizar nossa funcionalidade de voto Proposal. Aqui está o código do recurso Voter completo.

 // Detentor do recurso Voter pode votar nas propostas
  pub resource Voter: VoterPublic {
    access(self) var records: { UInt64: Int }

    pub fun vote(ProposalId: UInt64, optionIndex: Int) {
      pre {
        self.records[ProposalId] == nil: "Already voted"
        optionIndex < ExampleDAO.Proposals[ProposalId].options.length: "Invalid option"
      }
      ExampleDAO.Proposals[ProposalId].vote(
        voterAddr: self.owner!.address,
        optionIndex: optionIndex
        )
      self.records[ProposalId] = optionIndex
    };

    pub fun getVotedOption(ProposalId: UInt64): Int? {
      return self.records[ProposalId]
    }

    pub fun getVotedOptions(): { UInt64: Int } {
      return self.records
    }

    init() {
      self.records = {}
    }
  }
Enter fullscreen mode Exit fullscreen mode
# Recurso de Eleitor da DAO

É simples o suficiente para realizar o que se destina a fazer. Agora só falta adicionar uma função para criar esse recurso Voter dentro das contas e tem que ser público, para que qualquer um possa chamá-lo. Também precisamos adicionar aquela função getHolderBVT para buscar os tokens de governança que o eleitor detém. Além disso, adicionaremos os caminhos de armazenamento, interfaces e outras funcionalidades públicas necessárias para buscar dados da DAO.

Como esta é a última parte e o restante do código é coisa bem simples, iremos em frente e compartilharemos o código do contrato inteligente da DAO completo, para que você possa contemplar e estudar.

import NonFungibleToken from "../utility/NonFungibleToken.cdc"
import FungibleToken from "../utility/FungibleToken.cdc"
import GovernanceToken from "../GovernanceToken.cdc"

pub contract ExampleDAO {
  access(contract) var Proposals: [Proposal]
  access(contract) var votedRecords: [{ Address: Int }]
  access(contract) var totalProposals: Int

  pub let AdminStoragePath: StoragePath;
  pub let ProposerStoragePath: StoragePath;
  // O caminho de armazenamento para o ProposerProxy dos proponentes
  pub let ProposerProxyStoragePath: StoragePath
  // O caminho público para a capacidade ProposerProxy dos proponentes
  pub let ProposerProxyPublicPath: PublicPath
  // O detentor do recurso administrativo pode criar proponentes
  pub let VoterStoragePath: StoragePath;
  pub let VoterPublicPath: PublicPath;
  pub let VoterPath: PrivatePath;

    // Eventos
  pub event ContractInitialized()
  pub event ProposalCreated(Title: String, Proposer: Address, MinHoldedGVTAmount: UFix64?)
  pub event VoteSubmitted(Voter: Address, ProposalId: Int, OptionIndex: Int)

  pub resource Admin {
    pub fun createProposer(): @ExampleDAO.Proposer {
      return <- create Proposer()
    }
  }

  // Proposer (proponente)
    //
    // Objeto de recurso que pode criar novas propostas.
    // O administrador armazena isso e passa para a conta do Proposer (proponente) como um recurso de empacotador (wrapper) de capacidade. 
    //
    pub resource Proposer {

        // Função que cria novas propostas.
        //
      pub fun addProposal(
        title: String,
        description: String,
        options: [String],
        startAt: UFix64?,
        endAt: UFix64?,
        minHoldedGVTAmount: UFix64?
        ) {

        ExampleDAO.Proposals.append(Proposal(
         proposer: self.owner!.address,
         title: title,
         description: description,
         options: options,
         startAt: startAt,
         endAt: endAt,
         minHoldedGVTAmount: minHoldedGVTAmount
        ))

        ExampleDAO.votedRecords.append({})
        ExampleDAO.totalProposals = ExampleDAO.totalProposals + 1
      }

      pub fun updateProposal(
        id: Int,
        title: String?,
        description: String?,
        startAt: UFix64?,
        endAt: UFix64?,
        voided: Bool?
        ) {

        pre {
          ExampleDAO.Proposals[id].proposer == self.owner!.address: "Only original proposer can update"
        }

        ExampleDAO.Proposals[id].update(
          title: title,
          description: description,
          startAt: startAt,
          endAt: endAt,
          voided: voided
        )
      }
     }

    pub resource interface ProposerProxyPublic {
        pub fun setProposerCapability(capability: Capability<&Proposer>)
    }
    // ProposerProxy
    //
    // O objeto de recurso contendo uma capacidade que pode ser usada para criar novas propostas.
    // O recurso que essa capacidade representa pode ser excluído pelo administrador
    // a fim de revogar unilateralmente a capacidade do proponente, se necessário.
    pub resource ProposerProxy: ProposerProxyPublic {
        // access(self) para que ninguém mais possa copiar a capacidade e usá-la.
        access(self) var ProposerCapability: Capability<&Proposer>?
        // Qualquer um pode chamar isso, mas apenas o administrador pode criar capacidades de proponente,
        // então o sistema de tipos restringe isso para ser chamado pelo administrador.
        pub fun setProposerCapability(capability: Capability<&Proposer>) {
            self.ProposerCapability = capability
        }

        pub fun addProposal(
          _title: String,
          _description: String,
          _options: [String],
          _startAt: UFix64?,
          _endAt: UFix64?,
          _minHoldedGVTAmount: UFix64?
          ): Void? {

            return self.ProposerCapability
            ?.borrow()!
            ?.addProposal(
              title: _title,
              description: _description,
              options: _options,
              startAt: _startAt,
              endAt: _endAt,
              minHoldedGVTAmount: _minHoldedGVTAmount
              )
        }

        pub fun updateProposal(
          id: Int,
          title: String?,
          description: String?,
          startAt: UFix64?,
          endAt: UFix64?,
          voided: Bool?
          ) {

          return self.ProposerCapability!
          .borrow()!
          .updateProposal(
            id: id,
            title: title,
            description: description,
            startAt: startAt,
            endAt: endAt,
            voided: voided
            )
        }

        init () {
          self.ProposerCapability = nil
        }

    }
    // createProposerProxy
    //
    // Função que cria um ProposerProxy.
    // Qualquer um pode chamar isso, mas o ProposerProxy não pode
    // criar propostas sem uma capacidade de proponente (Proposer),
    // e apenas o administrador pode fornecer isso.
    //
    pub fun createProposerProxy(): @ProposerProxy {
        return <- create ProposerProxy()
    }

  pub resource interface VoterPublic {
    // voted Proposal id <-> options index mapping
    pub fun getVotedOption(ProposalId: UInt64): Int?
    pub fun getVotedOptions(): { UInt64: Int }
  }

  // Detentor do recurso Voter (de eleitor) pode votar em propostas.
  pub resource Voter: VoterPublic {
    access(self) var records: { UInt64: Int }

    pub fun vote(ProposalId: UInt64, optionIndex: Int) {
      pre {
        self.records[ProposalId] == nil: "Already voted"
        optionIndex < ExampleDAO.Proposals[ProposalId].options.length: "Invalid option"
      }
      ExampleDAO.Proposals[ProposalId].vote(voterAddr: self.owner!.address, optionIndex: optionIndex)
      self.records[ProposalId] = optionIndex
    };

    pub fun getVotedOption(ProposalId: UInt64): Int? {
      return self.records[ProposalId]
    }

    pub fun getVotedOptions(): { UInt64: Int } {
      return self.records
    }

    init() {
      self.records = {}
    }
  }

  pub struct Proposal {
    pub let id: Int;
    pub let proposer: Address
    pub var title: String
    pub var description: String
    pub var options: [String]
    // options index <-> result mapping
    pub var votesCountActual: [UInt64]
    pub let createdAt: UFix64
    pub var updatedAt: UFix64
    pub var startAt: UFix64
    pub var endAt: UFix64
    pub var sealed: Bool
    pub var countIndex: Int
    pub var voided: Bool
    pub let minHoldedGVTAmount: UFix64

    init(proposer: Address, title: String, description: String, options: [String], startAt: UFix64?, endAt: UFix64?, minHoldedGVTAmount: UFix64?) {
      pre {
        title.length <= 1000: "New title too long"
        description.length <= 1000: "New description too long"
      }

      self.proposer = proposer
      self.title = title
      self.options = options
      self.description = description
      self.votesCountActual = []
      self.minHoldedGVTAmount = minHoldedGVTAmount != nil ? minHoldedGVTAmount! : 0.0

      for option in options {
        self.votesCountActual.append(0)
      }

      self.id = ExampleDAO.totalProposals

      self.sealed = false
      self.countIndex = 0

      self.createdAt = getCurrentBlock().timestamp
      self.updatedAt = getCurrentBlock().timestamp

      self.startAt = startAt != nil ? startAt! : getCurrentBlock().timestamp
      self.endAt = endAt != nil ? endAt! : self.createdAt + 86400.0 * 14.0 // Around a year

      self.voided = false

      emit ProposalCreated(Title: title, Proposer: proposer, MinHoldedGVTAmount: minHoldedGVTAmount)
    }

    pub fun update(title: String?, description: String?, startAt: UFix64?, endAt: UFix64?, voided: Bool?) {
      pre {
        title?.length ?? 0 <= 1000: "Title too long"
        description?.length ?? 0 <= 1000: "Description too long"
        voided != true: "Can't update after started"
        getCurrentBlock().timestamp < self.startAt: "Can't update after started"
      }

      self.title = title != nil ? title! : self.title
      self.description = description != nil ? description! : self.description
      self.endAt = endAt != nil ? endAt! : self.endAt
      self.startAt = startAt != nil ? startAt! : self.startAt
      self.voided = voided != nil ? voided! : self.voided
      self.updatedAt = getCurrentBlock().timestamp
    }

    pub fun vote(voterAddr: Address, optionIndex: Int) {
      pre {
        self.isStarted(): "Vote not started"
        !self.isEnded(): "Vote ended"
        ExampleDAO.votedRecords[self.id][voterAddr] == nil: "Already voted"
      }

      let voterGVT = ExampleDAO.getHoldedGVT(address: voterAddr)

      assert(voterGVT >= self.minHoldedGVTAmount, message: "Not enought GVT in your Vault to vote")

      ExampleDAO.votedRecords[self.id][voterAddr] = optionIndex

      emit VoteSubmitted(Voter: voterAddr, ProposalId: self.id, OptionIndex: optionIndex)
    }

    // retorna se a contagem for finalizada
    pub fun count(size: Int): [UInt64] {
      if self.isEnded() == false {
        return self.votesCountActual
      }
      if self.sealed {
        return self.votesCountActual
      }
      // Busca as chaves de todos que votaram nesta proposta
      let votedList = ExampleDAO.votedRecords[self.id].keys
      // Contagem desde a última vez que você contou
      var batchEnd = self.countIndex + size
      // Se o índice da contagem é maior que o número de eleitores
      // define o índice de contagem para o número de eleitores
      if batchEnd > votedList.length {
        batchEnd = votedList.length
      }

      while self.countIndex != batchEnd {
        let address = votedList[self.countIndex]
        let votedOptionIndex = ExampleDAO.votedRecords[self.id][address]!
        self.votesCountActual[votedOptionIndex] = self.votesCountActual[votedOptionIndex] + 1

        self.countIndex = self.countIndex + 1
      }

      self.sealed = self.countIndex == votedList.length

      return self.votesCountActual
    }

    pub fun isEnded(): Bool {
      return getCurrentBlock().timestamp >= self.endAt
    }

    pub fun isStarted(): Bool {
      return getCurrentBlock().timestamp >= self.startAt
    }

    pub fun getTotalVoted(): Int {
      return ExampleDAO.votedRecords[self.id].keys.length
    }
  }

  pub fun getHoldedGVT(address: Address): UFix64 {
    let acct = getAccount(address)
    let vaultRef = acct.getCapability(GovernanceToken.VaultPublicPath)
        .borrow<&GovernanceToken.Vault{FungibleToken.Balance}>()
        ?? panic("Could not borrow Balance reference to the Vault")

    return vaultRef.balance
  }

  pub fun getProposals(): [Proposal] {
    return self.Proposals
  }

  pub fun getProposalsLength(): Int {
    return self.Proposals.length
  }

  pub fun getProposal(id: UInt64): Proposal {
    return self.Proposals[id]
  }

  pub fun count(ProposalId: UInt64, maxSize: Int): [UInt64] {
    return self.Proposals[ProposalId].count(size: maxSize)
  }

  pub fun initVoter(): @ExampleDAO.Voter {
    return <- create Voter()
  }

  init () {
    self.Proposals = []
    self.votedRecords = []
    self.totalProposals = 0

    self.AdminStoragePath = /storage/ExampleDAOAdmin
    self.ProposerStoragePath = /storage/ExampleDAOProposer
    self.ProposerProxyPublicPath = /public/ExampleDAOProposerProxy
    self.ProposerProxyStoragePath = /storage/ExampleDAOProposerProxy
    self.VoterStoragePath = /storage/ExampleDAOVoter
    self.VoterPublicPath = /public/ExampleDAOVoter
    self.VoterPath = /private/ExampleDAOVoter

    self.account.save(<-create Admin(), to: self.AdminStoragePath)
    self.account.save(<-create Proposer(), to: self.ProposerStoragePath)
    self.account.save(<-create Voter(), to: self.VoterStoragePath)
    self.account.link<&ExampleDAO.Voter>(
            self.VoterPublicPath,
            target: self.VoterStoragePath
        )

    emit ContractInitialized()
  }
}
Enter fullscreen mode Exit fullscreen mode
# Contrato inteligente da DAO completo.

Agora, tudo o que falta é escrever algumas transações e scripts e testá-los todos com o Overflow! Criaremos um conjunto de testes completo que podemos continuar a usar no futuro quando tentarmos adicionar novas funcionalidades ao contrato da DAO (o que faremos).

A primeira transação que precisamos é uma para criar o recurso Voter dentro da conta de nossos membros da DAO. Essa é bem simples, desde que já tenhamos definido uma função pública para isso dentro do nosso contrato inteligente.

import ExampleDAO from "../../contracts/DAO/ExampleDAO.cdc"

transaction {

  prepare(signer: AuthAccount) {

    let voter <- ExampleDAO.initVoter()

    signer.save(<-voter, to: /storage/ExampleDAOVoter)
    signer.link<&BlockVersityDAO.Voter>(
      /public/ExampleDAOVoter,
      target: /storage/ExampleDAOVoter
    )
  }
}
Enter fullscreen mode Exit fullscreen mode
# Crie um recurso Voter (eleitor) dentro de qualquer conta.

A seguir está uma transação que permite aos usuários com o recurso Voter votar em uma proposta específica. Qualquer um que tentar votar sem esse recurso falhará.

import ExampleDAO from "../../contracts/DAO/ExampleDAO.cdc"

transaction(ProposalId: UInt64, OptionIndex: Int) {

  prepare(signer: AuthAccount) {
  let voter = signer
  .borrow<&ExampleDAO.Voter>(from: ExampleDAO.VoterStoragePath)
        ?? panic("Signer is not a Voter")
    voter.vote(ProposalId: ProposalId, optionIndex: OptionIndex)
  }

}
Enter fullscreen mode Exit fullscreen mode
# Transação para votar na proposta.

Por enquanto, essas são todas as transações de que precisamos. Vamos criar alguns scripts que nos ajudarão a garantir que tudo esteja funcionando conforme o esperado. Vou fazer uma lista de tudo o que precisamos para poder buscar em nosso contrato inteligente da DAO:

  • Uma lista de todas as propostas.
  • Todos os detalhes de uma proposta específica.
  • O número total de pessoas que votaram em uma proposta específica.
  • Um usuário deve ser capaz de revisar uma lista de todas as opções votadas que já fizeram neste contrato inteligente da DAO.
  • E, por último, um script público que permite a qualquer pessoa contar os votos feitos em uma proposta. Afinal, esta é uma função pública dentro da nossa estrutura proposal.

Tudo isso é muito simples de implementar e nós já escrevemos a funcionalidade pública que ajudará a buscar todas essas informações do contrato. Aqui estão todos os quatro scripts necessários:

import ExampleDAO from "../../contracts/DAO/ExampleDAO.cdc"

pub fun main(): [ExampleDAO.Proposal] {
  return ExampleDAO.getProposals()
}
Enter fullscreen mode Exit fullscreen mode
# Obter todas as propostas.
import ExampleDAO from "../../contracts/DAO/ExampleDAO.cdc"

pub fun main(ProposalId: UInt64): ExampleDAO.Proposal {
  return ExampleDAO.getProposal(id: ProposalId)
}
Enter fullscreen mode Exit fullscreen mode
# Obter todos os detalhes em uma proposta específica.
import ExampleDAO from "../../contracts/DAO/ExampleDAO.cdc"

pub fun main(ProposalId: UInt64): Int {
  return ExampleDAO.getProposal(id: ProposalId).getTotalVoted()
}
Enter fullscreen mode Exit fullscreen mode
# Obter o total de votos feitos em uma proposta específica.
import ExampleDAO from "../../contracts/DAO/ExampleDAO.cdc"

pub fun main(address: Address): {UInt64: Int} {
  let account = getAccount(address)

      let voterRef = account.getCapability(/public/ExampleDAOVoter)
        .borrow<&ExampleDAO.Voter>()
        ?? panic("Could not borrow Voter reference")

        return voterRef.getVotedOptions()
}
Enter fullscreen mode Exit fullscreen mode
# Buscar todas as opções votadas já feitas por uma conta.
import ExampleDAO from "../../contracts/DAO/ExampleDAO.cdc"

pub fun main(ProposalId: UInt64): [UInt64] {
  return ExampleDAO.getProposal(id: ProposalId).count(size: 2)
}


Enter fullscreen mode Exit fullscreen mode
# Contagem de votos em uma proposta.

E é isso! Vamos voltar ao nosso conjunto de testes e adicionar todas essas novas transações e scripts para garantir que tudo funcione conforme o esperado. Para economizar tempo, você pode simplesmente pular para o repositório de amostra BlockVersityDAO e copiar a configuração do arquivo Flow.json caso o Overflow não esteja em execução para você. O conjunto completo de testes Go deve ficar assim:

package main

import (
"fmt"
"testing"

. "github.com/bjartek/overflow"
"github.com/fatih/color"
"github.com/stretchr/testify/assert"
)

func TestProposers(t *testing.T) {

o, err := OverflowTesting()
assert.NoError(t, err)

fmt.Println("Testing Proposer creation and interactions")
fmt.Println("Press any key to continue")
fmt.Scanln()
/*
 - Bob deposita um ProposerProxy dentro de sua conta
 - Bob tenta fazer uma proposta sem o recurso Proposer
 - O administrador deposita um recurso Proposer dentro da conta de Bob
 - Bob cria uma proposta
*/
color.Red("Should be able setup a ProposerProxy inside Bob's account")
o.Tx("DAO/setupProposerProxy",
 WithSigner("bob")).AssertSuccess(t).Print()
color.Green("Pass")

color.Red("Bob will attempt to make a proposal with the Proxy, but fails")
o.Tx("DAO/createProposal",
 WithSigner("bob"),
 WithArg("title", "How much $BVT tokens grant should the BlockVersity ecosystem fund allocate in support for Ukraine?"),
 WithArg("description", "BlockVersity is dedicated to stop the doomsday clock from moving any closer to midnight, at any cost."),
 WithArg("options", `["200K $BVT", "600K $BVT", "1000K $BVT"]`),
 WithArg("startAt", "1641373200.0"),
 WithArg("endAt", "1759546000.0"),
 WithArg("minHoldedBVTAmount", "100.0"),
).AssertFailure(t, "unexpectedly found nil while forcing an Optional value")
color.Green("Pass")

color.Red("Admin should be able deposit a Proposer resource inside Bob's account")
o.Tx("DAO/depositProposer",
 WithSigner("account"),
 WithArg("proposerAddress", "bob"),
).AssertSuccess(t).Print()
color.Green("Pass")

color.Red("Bob will attempt to make a proposal again, and succeeds")
o.Tx("DAO/createProposal",
 WithSigner("bob"),
 WithArg("title", "How much $BVT tokens grant should the BlockVersity ecosystem fund allocate in support for Ukraine?"),
 WithArg("description", "BlockVersity is dedicated to stop the doomsday clock from moving any closer to midnight, at any cost."),
 WithArg("options", `["200K $BVT", "600K $BVT", "1000K $BVT"]`),
 WithArg("startAt", "1641373200.0"),
 WithArg("endAt", "1759546000.0"),
 WithArg("minHoldedBVTAmount", "100.0"),
).AssertSuccess(t).Print()
color.Green("Pass")

// Bob vota
color.Red("Bob tries to Vote on a proposal without the voter resource")
o.Tx("DAO/vote",
 WithSigner("bob"),
 WithArg("ProposalId", "0"),
 WithArg("OptionIndex", "1"),
).AssertFailure(t, "Signer is not a Voter")
color.Green("Pass")

// Cria um Voter
color.Red("Should be able to create a Voter resource into the signer's account")
o.Tx("DAO/createVoter",
 WithSigner("bob")).AssertSuccess(t).Print()
color.Green("Pass")

// Bob vota
color.Red("Bob tries to Vote on a proposal but doesn't hold enough BVT")
o.Tx("DAO/vote",
 WithSigner("bob"),
 WithArg("ProposalId", "0"),
 WithArg("OptionIndex", "1"),
).AssertFailure(t, "Could not borrow Balance reference to the Vault")
color.Green("Pass")

// Configura o Bob com o BVT
color.Red("Should be able to setup Bob's account to receive BVT")
o.Tx("BlockVersity/setup_account", WithSigner("bob"))
color.Green("Pass")

// Transfere 101 BVT da conta para Bob
color.Red("Should be able to send 101 BVT to Bob")
o.Tx("BlockVersity/transferBVT", WithSigner("account"), WithArg("amount", "101.0"), WithArg("recipient", "bob"))
color.Green("Pass")

color.Red("Bob should be able to vote on Proposal now")
o.Tx("DAO/vote",
 WithSigner("bob"),
 WithArg("ProposalId", "0"),
 WithArg("OptionIndex", "1"),
).AssertSuccess(t)
color.Green("Pass")

// Votos da conta
color.Red("Account should be able to Vote on a proposal")
o.Tx("DAO/vote",
 WithSigner("account"),
 WithArg("ProposalId", "0"),
 WithArg("OptionIndex", "1"),
).AssertSuccess(t)
color.Green("Pass")

color.Red("Should be able to fetch Voter's voted options")
o.Script("DAO/getVoterOptions",
 WithArg("address", "account")).Print()
color.Green("Pass")

// Qualquer um deve ser capaz de contar os votos no contrato
color.Red("Alice should be able to count votes on a proposal")
o.Script("DAO/countVotes",
 WithArg("ProposalId", "0"),
).Print()
color.Green("Pass")

// Qualquer um deve ser capaz de buscar uma lista de propostas do contrato da DAO
color.Red("Should be able to fetch a list of proposals")
o.Script("DAO/getProposals").Print()
color.Green("Pass")

// Contagem de votos na proposta
color.Red("Should be able to count votes on one proposal")
o.Script("DAO/countVotes",
 WithArg("ProposalId", "0")).Print()
color.Green("Pass")

// Busca uma proposta.
color.Red("Should be able to fetch a single proposal")
o.Script("DAO/getProposal",
 WithArg("ProposalId", "0")).Print()
color.Green("Pass")

// Busca uma contagem de proposta
color.Red("Should be able to fetch the number of total votes on one proposal")
o.Script("DAO/getProposalTotalVoted",
 WithArg("ProposalId", "0")).Print()
color.Green("Pass")
}
Enter fullscreen mode Exit fullscreen mode

E depois de executar isso com o testador do VSCode, devemos obter este resultado no nosso histórico de usuário:

# Passou!

E dessa forma, escrevemos nosso primeiro contrato inteligente de governança. É a versão mais simples dele e pode ser melhorada de várias maneiras! Existem diferentes formas de abordar a mecânica da governança, a que usamos neste contrato inteligente é a mais básica e não necessariamente a mais segura ou eficiente. Um sistema interessante é o sistema de voto quadrático, que podemos explorar no futuro, mas você tem aqui uma base sólida para começar e construir sua própria DAO.

Para conferir o código completo, você pode acessar o repositório público BlockVersity, na ramificação DAO-Example.

Esse artigo foi escrito por Noah Overflow e traduzido por Isabela Curado Nehme. Seu original pode ser lido aqui.

Top comments (0)