WEB3DEV

Cover image for Como Construir um Aplicativo de Votação Simples com o Solidity
Panegali
Panegali

Posted on

Como Construir um Aplicativo de Votação Simples com o Solidity

Neste tutorial, construímos, testamos e implantamos um aplicativo de votação simples usando o Solidity, uma linguagem de programação projetada especificamente para contratos inteligentes.

Foto de Element5 Digital no Unsplash

Introdução

A votação é um processo fundamental em qualquer sociedade democrática e é essencial garantir que o processo seja transparente, seguro e exato. A tecnologia blockchain tem o potencial de transformar a maneira como conduzimos a votação, fornecendo um sistema seguro, descentralizado e inviolável para registro e contagem de votos. Contratos inteligentes, em particular, podem ser usados ​​para criar sistemas de votação autoexecutáveis ​​que são transparentes e incorruptíveis.

Neste tutorial, iremos explorar como construir um aplicativo de votação simples usando o Solidity, uma linguagem de programação projetada especificamente para contratos inteligentes. Iremos orientar você no processo de criação de um contrato inteligente para um aplicativo de votação, compilando, testando e implantando o contrato.

Seja você um desenvolvedor interessado em aprender sobre o Solidity e contratos inteligentes ou alguém que queira entender como a tecnologia blockchain pode ser usada para sistemas de votação, este tutorial fornecerá a você uma experiência prática na construção de um aplicativo de votação simples na blockchain. Então vamos começar!

Configurando o ambiente de desenvolvimento

Para configurar seu ambiente de desenvolvimento, siga o guia de introdução do Hardhat. Estaremos usando o Typescript para este projeto.

O contrato inteligente do aplicativo de votação simples

Estamos construindo um aplicativo de votação simples usando a linguagem de programação Solidity. Este aplicativo permitirá que qualquer pessoa crie uma votação com um conjunto de opções ou candidatos, especifique um horário de início e término para a votação e permita que os usuários votem. Terminado o período de votação, o aplicativo contará os votos e declarará o(s) vencedor(es) com base nas regras da eleição.

O objetivo deste aplicativo é mostrar o uso do Solidity na construção de aplicativos descentralizados com mecanismos de votação transparentes e seguros. Com este aplicativo, podemos demonstrar como o Solidity pode ser usado para criar contratos inteligentes que podem garantir a integridade dos processos de votação, promovendo transparência e confiança.

Principais ações

  1. Criando uma votação: O aplicativo deve permitir que qualquer usuário crie uma nova votação com um conjunto específico de opções, um início e uma duração para a votação.
  2. Votação: Qualquer usuário deve ser capaz de votar nas opções listadas na votação. O aplicativo deve impedir que os usuários votem várias vezes e garantir que os votos sejam lançados apenas durante a janela de votação especificada.
  3. Apuração dos votos: Uma vez encerrado o período de votação, o aplicativo deverá apurar os votos e declarar o(s) vencedor(es) com base nas regras da eleição.

Casos de teste — Criando uma votação

Vamos escrever quatro casos de teste para esta parte.

  1. Deve criar uma votação.
  2. Deve reverter se a votação tiver menos de 2 opções.
  3. Deve reverter se a hora de início for menor que a hora atual.
  4. Deve reverter se a duração for menor que 1.

Vamos criar um arquivo: test/SimpleVoting.ts onde iremos descrever todos os nossos testes de casos. A estrutura básica consistirá em um método deploy, que será utilizado em todos os testes.

import { loadFixture, time } from "@nomicfoundation/hardhat-network-helpers"
import { expect } from "chai"
import { BigNumber } from "ethers"
import { ethers } from "hardhat"

describe("SimpleVoting", function () {

   async function deploy() {
       const Contract = await ethers.getContractFactory("SimpleVoting")
       const contract = await Contract.deploy()
       await contract.deployed()
       return { contract }
   }

})
Enter fullscreen mode Exit fullscreen mode

Vamos escrever marcadores de posição para nossos primeiros 4 casos de teste:

describe("Criar uma votação ", function () {
 it("deve criar uma votação ")
 it("deve reverter se a votação tiver menos de 2 opções")
 it("deve reverter se a hora de início for menor que a hora atual")
 it("deve reverter se a hora final for menor ou igual à hora inicial")
})
Enter fullscreen mode Exit fullscreen mode

Execute o comando de teste:

npx hardhat test
Enter fullscreen mode Exit fullscreen mode

Você obterá o seguinte:

SimpleVoting
   Criar uma votação
     - deve criar uma votação
     - deve reverter se a votação tiver menos de 2 opções
     - deve reverter se a hora de início for menor que a hora atual
     - deve reverter se a hora final for menor ou igual à hora inicial


 0 passing (2ms)
 4 pending
Enter fullscreen mode Exit fullscreen mode

Isso ocorre porque ainda não escrevemos nossos testes de casos. Vamos escrever nosso primeiro.

it("deve criar uma votação", async function () {
 const { contract } = await loadFixture(deploy)
 const startTime = await time.latest() + 60 // iniciar a votação em 60 segundos
 const duration = 300 // a votação ficará aberta por 300 segundos
 const question = "Quem é o maior rapper de todos os tempos?"
 const options = [
   "Tupac Shakur",
   "The Notorious B.I.G.",
   "Eminem",
   "Jay-Z"
 ]
 await contract.createBallot(
   question, options, startTime, duration
 )
 expect(await contract.getBallotByIndex(0)).to.deep.eq([
   question,
   options,
   BigNumber.from(startTime), // converter de uint
   BigNumber.from(duration), // converter de uint
 ])
})
Enter fullscreen mode Exit fullscreen mode

Agora, se executarmos o comando de teste, obteremos o seguinte erro:

HardhatError: HH700: Artifact for contract "SimpleVoting" not found.
Enter fullscreen mode Exit fullscreen mode

Isso porque ainda não criamos um contrato. Vamos fazê-lo.

Nosso contrato básico ficará assim:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

contract SimpleVoting {

}
Enter fullscreen mode Exit fullscreen mode

Agora precisamos definir nossos métodos e estruturas de dados.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

contract SimpleVoting {
   // nos permite usar um mapeamento
   // em vez de um array para as votações
   // isso é mais eficiente em termos de consumo de gás
   uint public counter = 0;

   // a estrutura de um objeto de votação
   struct Ballot {
       string question;
       string[] options;
       uint startTime;
       uint duration;
   }

   mapping(uint => Ballot) private _ballots;

   function createBallot(
       string memory question_,
       string[] memory options_,
       uint startTime_,
       uint duration_
   ) external {
       _ballots[counter] = Ballot(question_, options_, startTime_, duration_);
       counter++;
   }

   function getBallotByIndex(uint index_) external view returns (Ballot memory ballot) {
       ballot = _ballots[index_];
   }
}
Enter fullscreen mode Exit fullscreen mode

Agora, se executarmos os testes, obteremos o seguinte, primeiro teste aprovado!

SimpleVoting
   Criando uma votação
      deve criar uma votação (646ms)
     - deve reverter se a votação tiver menos de 2 opções
     - deve reverter se a hora de início for menor que a hora atual
     - deve reverter se a duração for menor que 1


 1 passing (650ms)
 3 pending
Enter fullscreen mode Exit fullscreen mode

Nos próximos testes, testaremos as validações.

it("deve reverter se a cédula tiver menos de 2 opções", async function () {
           const { contract } = await loadFixture(deploy)
           const startTime = await time.latest() + 60 // iniciar a votação em 60 segundos
           const duration = 300 // a votação ficará aberta por 300 segundos
           const question = "Quem é o maior rapper de todos os tempos?"
           const options = [
               "Tupac Shakur",
               // "The Notorious B.I.G.",
               // "Eminem",
               // "Jay-Z"
           ]
           await expect(contract.createBallot(
               question, options, startTime, duration
           )).to.be.revertedWith("Providenciar, no mínimo, duas opções")
       })
Enter fullscreen mode Exit fullscreen mode

Vamos modificar a função createBallotde nossos contratos:

function createBallot(
       string memory question_,
       string[] memory options_,
       uint startTime_,
       uint duration_
   ) external {
       require(options_.length >= 2, "Providenciar, no minimo, duas opcoes"); // novo
       _ballots[counter] = Ballot(question_, options_, startTime_, duration_);
       counter++;
   }
Enter fullscreen mode Exit fullscreen mode

Execute os testes e você deve obter o seguinte:

SimpleVoting
   Criando uma votação
      deve criar uma votação (685ms)
      deve reverter se a cédula tiver menos de 2 opções
     - deve reverter se a hora de início for menor que a hora atual
     - deve reverter se a duração for menor que 1


 2 passing (712ms)
 2 pending
Enter fullscreen mode Exit fullscreen mode

Em seguida, vamos validar a hora de início. Aqui está nosso teste de caso, alteramos a declaração de hora de início.

it("deve reverter se a hora de início for menor que a hora atual", async function () {
           const { contract } = await loadFixture(deploy)
           const startTime = await time.latest() - 60 // iniciar a votação 60 segundos antes da hora atual
           const duration = 300 // a votação ficará aberta por 300 segundos
           const question = "Quem é o maior rapper de todos os tempos?"
           const options = [
               "Tupac Shakur",
               "The Notorious B.I.G.",
               "Eminem",
               "Jay-Z"
           ]
           await expect(contract.createBallot(
               question, options, startTime, duration
           )).to.be.revertedWith("A hora de inicio deve ser no futuro")
       })
Enter fullscreen mode Exit fullscreen mode

Vamos modificar a função createBallot:

function createBallot(
       string memory question_,
       string[] memory options_,
       uint startTime_,
       uint duration_
   ) external {
       require(startTime_ > block.timestamp, "A hora de inicio deve ser no futuro"); // new
       require(options_.length >= 2, "Providenciar, no minimo, duas opcoes");
       _ballots[counter] = Ballot(question_, options_, startTime_, duration_);
       counter++;
   }
Enter fullscreen mode Exit fullscreen mode

Faça os testes…

SimpleVoting
   Criando de uma votação
      deve criar uma votação (737ms)
      deve reverter se a cédula tiver menos de 2 opções
      deve reverter se a hora de início for menor que a hora atual
     - deve reverter se a duração for menor que 1


 3 passing (786ms)
 1 pending
Enter fullscreen mode Exit fullscreen mode

O último teste na seção de criação de votação é a verificação da duração.

it("deve reverter se a duração for menor que 1", async function () {
           const { contract } = await loadFixture(deploy)
           const startTime = await time.latest() + 60 // iniciar a votação em 60 segundos
           const duration = 0 // a votação nunca estará aberta
           const question = "Quem é o maior rapper de todos os tempos?"
           const options = [
               "Tupac Shakur",
               "The Notorious B.I.G.",
               "Eminem",
               "Jay-Z"
           ]
           await expect(contract.createBallot(
               question, options, startTime, duration
           )).to.be.revertedWith("A duração deve ser maior que 0")
       })
Enter fullscreen mode Exit fullscreen mode

Vamos atualizar a função createBallot no contrato:

function createBallot(
       string memory question_,
       string[] memory options_,
       uint startTime_,
       uint duration_
   ) external {
       require(duration_ > 0, "A duracao deve ser maior que 0"); // new
       require(startTime_ > block.timestamp, "A hora de inicio deve ser no futuro");
       require(options_.length >= 2, "Providenciar, no minimo, duas opcoes");
       _ballots[counter] = Ballot(question_, options_, startTime_, duration_);
       counter++;
   }
Enter fullscreen mode Exit fullscreen mode

Casos de teste —Votação

Temos quatro casos de teste para votação:

  1. Deve ser possível votar.
  2. Deverá reverter se o usuário tentar votar antes do horário de início.
  3. Deverá reverter se o usuário tentar votar após o horário de término.
  4. Deverá reverter se o usuário tentar votar várias vezes.
describe("Votação", function () {
       let contract;
       const duration = 300 // a votação ficará aberta por 300 segundos

       beforeEach(async function () {
           const fixture = { contract } = await loadFixture(deploy)
           const startTime = await time.latest() + 60 // iniciar a votação em 60 segundos
           const question = "Quem é o maior rapper de todos os tempos?"
           const options = [                "Tupac Shakur",                "The Notorious B.I.G.",                "Eminem",                "Jay-Z"            ]
           await contract.createBallot(
               question, options, startTime, duration
           )
       })
       it("deve poder votar")
       it("deve reverter se o usuário tentar votar antes da hora de início")
       it("deve reverter se o usuário tentar votar após o tempo final")
       it("deve reverter se o usuário tentar votar várias vezes")
   })
Enter fullscreen mode Exit fullscreen mode

Teste #1

it("deve poder votar", async function () {
           const [signer] = await ethers.getSigners()
           await time.increase(61) // certifique-se de que a votação esteja aberta
           await contract.cast(0, 0)
           expect(await contract.hasVoted(0, signer.address)).to.eq(true)
           expect(await contract.getTally(0,0)).to.eq(1)
       })
Enter fullscreen mode Exit fullscreen mode

Contrato

// ...

mapping(uint => mapping(uint => uint)) private _tally;
mapping(uint => mapping(address => bool)) public hasVoted;

// ...

function cast(uint ballotIndex_, uint optionIndex_) external {
 _tally[ballotIndex_][optionIndex_]++;
 hasVoted[ballotIndex_][msg.sender] = true;
}

function getTally(uint ballotIndex_, uint optionIndex_) external view returns (uint) {
 return _tally[ballotIndex_][optionIndex_];
}
Enter fullscreen mode Exit fullscreen mode

Teste #2

it("deve reverter se o usuário tentar votar antes da hora de início", async function () {
 await expect(contract.cast(0, 0)).to.be.revertedWith("Não é possível lançar antes do horário de início")
})
Enter fullscreen mode Exit fullscreen mode

Contrato

function cast(uint ballotIndex_, uint optionIndex_) external {
 Ballot memory ballot = _ballots[ballotIndex_]; // novo
 require(block.timestamp >= ballot.startTime, "Nao e possivel lancar antes do horario de inicio"); // novo
 _tally[ballotIndex_][optionIndex_]++;
 hasVoted[ballotIndex_][msg.sender] = true;
}
Enter fullscreen mode Exit fullscreen mode

Teste #3

it("deve reverter se o usuário tentar votar após o tempo final", async function () {
 await time.increase(2000)
 await expect(contract.cast(0, 0)).to.be.revertedWith("Não é possível lançar após o horário final")
})
Enter fullscreen mode Exit fullscreen mode

Contrato

function cast(uint ballotIndex_, uint optionIndex_) external {
 Ballot memory ballot = _ballots[ballotIndex_];
 require(block.timestamp >= ballot.startTime, "Nao e possivel lanaar antes do horario de inicio");
 require(block.timestamp < ballot.startTime + ballot.duration, "Nao e possivel lanaar apos o horario final"); // novo
 _tally[ballotIndex_][optionIndex_]++;
 hasVoted[ballotIndex_][msg.sender] = true;
}
Enter fullscreen mode Exit fullscreen mode

Teste #4

it("deve reverter se o usuário tentar votar várias vezes", async function () {
 await time.increase(61) // certifique-se de que a votação esteja aberta
 await contract.cast(0, 0)
 await expect(contract.cast(0,1)).to.be.revertedWith("Endereço já votou em uma votação")
})
Enter fullscreen mode Exit fullscreen mode

Contrato

function cast(uint ballotIndex_, uint optionIndex_) external {
 require(!hasVoted[ballotIndex_][msg.sender], "Endereco ja votou em uma votacao"); // new
 Ballot memory ballot = _ballots[ballotIndex_];
 require(block.timestamp >= ballot.startTime, "Nao e possivel lancar antes do horario de inicio");
 require(block.timestamp < ballot.startTime + ballot.duration, "Nao e possivel lancar apos o horario final");
 _tally[ballotIndex_][optionIndex_]++;
 hasVoted[ballotIndex_][msg.sender] = true;
}
Enter fullscreen mode Exit fullscreen mode

Execute nossos testes e obteremos o seguinte:

SimpleVoting
   Criar uma votação
      deve criar uma votação (709ms)
      deve reverter se a cédula tiver menos de 2 opções
      deve reverter se a hora de início for menor que a hora atual
      deve reverter se a duração for menor que 1
   Votação
      deve poder votar
      deve reverter se o usuário tentar votar antes da hora de início
      deve reverter se o usuário tentar votar após o tempo final
      deve reverter se o usuário tentar votar várias vezes


 8 passando(993ms)
Enter fullscreen mode Exit fullscreen mode

Casos de teste - Apuração de votos

  1. Deve retornar os resultados para cada opção.
  2. Deve retornar o vencedor de uma votação.
  3. Deve retornar vários vencedores para uma votação empatada.
describe("Apuração de votos", function () {
       let contract: Contract;
       const duration = 300 // a cédula ficará aberta por 300 segundos

       beforeEach(async function () {
           const fixture = { contract } = await loadFixture(deploy)
           const startTime = await time.latest() + 60 // iniciar a votação em 60 segundos
           const question = "Quem é o maior rapper de todos os tempos?"
           const options = [
               "Tupac Shakur",
               "The Notorious B.I.G.",
               "Eminem",
               "Jay-Z"
           ]
           await contract.createBallot(
               question, options, startTime, duration
           )
           await time.increase(200)
           const signers = await ethers.getSigners()
           await contract.cast(0,0)
           await contract.connect(signers[1]).cast(0,0)
           await contract.connect(signers[2]).cast(0,1)
           await contract.connect(signers[3]).cast(0,2)
       })

       it("deve retornar os resultados para cada opção")
       it("deve retornar o vencedor para uma votação")
       it("deve retornar vários vencedores para uma votação empatada")
   })
Enter fullscreen mode Exit fullscreen mode

Teste #1

it("deve retornar os resultados para cada opção", async function () {
 await time.increase(2000)
 expect(await contract.results(0)).to.deep.eq([
   BigNumber.from(2),
   BigNumber.from(1),
   BigNumber.from(1),
   BigNumber.from(0),
 ])
})
Enter fullscreen mode Exit fullscreen mode

Contrato

function results(uint ballotIndex_) external view returns (uint[] memory) {
 Ballot memory ballot = _ballots[ballotIndex_];
 uint len = ballot.options.length;
 uint[] memory result = new uint[](len);
 for (uint i = 0; i < len; i++) {
   result[i] = _tally[ballotIndex_][i];
 }
 return result;
}
Enter fullscreen mode Exit fullscreen mode

Teste #2

it("deve retornar o vencedor de uma votação", async function () {
 await time.increase(2000)
 expect(await contract.winners(0)).to.deep.eq([true, false, false, false])
})
Enter fullscreen mode Exit fullscreen mode

Contrato

function winners(uint ballotIndex_) external view returns (bool[] memory) {
 Ballot memory ballot = _ballots[ballotIndex_];
 uint len = ballot.options.length;
 uint[] memory result = new uint[](len);
 uint max;
 for (uint i = 0; i < len; i++) {
   result[i] = _tally[ballotIndex_][i];
   if (result[i] > max) {
     max = result[i];
   }
 }
 bool[] memory winner = new bool[](len);
 for (uint i = 0; i < len; i++) {
   if (result[i] == max) {
     winner[i] = true;
   }
 }
 return winner;
}
Enter fullscreen mode Exit fullscreen mode

Teste #3

it("deve retornar vários vencedores para uma votação empatada", async function () {
 const signers = await ethers.getSigners()
 await contract.connect(signers[4]).cast(0, 2)
 await time.increase(2000)
 expect(await contract.winners(0)).to.deep.eq([true, false, true, false])
})
Enter fullscreen mode Exit fullscreen mode

Não há necessidade de alterar o código do contrato. Isso é tudo para este tutorial, há algumas sugestões de melhoria abaixo. O código pode ser encontrado no GitHub.

Sugestões de melhoria

  1. Retornar o número de votações.
  2. Emitir o evento CreateBallot com o endereço do remetente + o número da votação.
  3. Adicionar métodos para votações ativas e expiradas.

Artigo escrito por Cyrille. Traduzido por Marcelo Panegali

Top comments (0)