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
- 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.
- 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.
- 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.
- 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 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 }
}
})
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")
})
Execute o comando de teste:
npx hardhat test
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
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
])
})
Agora, se executarmos o comando de teste, obteremos o seguinte erro:
HardhatError: HH700: Artifact for contract "SimpleVoting" not found.
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 {
}
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_];
}
}
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
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")
})
Vamos modificar a função createBallot
de 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++;
}
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
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")
})
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++;
}
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
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")
})
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++;
}
Casos de teste —Votação
Temos quatro casos de teste para votação:
- Deve ser possível votar.
- Deverá reverter se o usuário tentar votar antes do horário de início.
- Deverá reverter se o usuário tentar votar após o horário de término.
- 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")
})
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)
})
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_];
}
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")
})
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;
}
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")
})
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;
}
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")
})
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;
}
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)
Casos de teste - Apuração de votos
- Deve retornar os resultados para cada opção.
- Deve retornar o vencedor de uma votação.
- 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")
})
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),
])
})
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;
}
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])
})
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;
}
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])
})
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
- Retornar o número de votações.
- Emitir o evento CreateBallot com o endereço do remetente + o número da votação.
- Adicionar métodos para votações ativas e expiradas.
Artigo escrito por Cyrille. Traduzido por Marcelo Panegali
Latest comments (0)