O "
O resultado final do meu artigo original demonstrou como uma associação de proprietários de imóveis (HOA) poderia usar a tecnologia Web3 para hospedar sua cédula eleitoral. O problema com o design original é que o contrato inteligente subjacente permitia apenas uma única resposta sim ou não. Isso foi planejado para manter o contrato inteligente simples e, ao mesmo tempo, introduzir outros conceitos necessários para criar uma cédula HOA usando tecnologias Web3.
O objetivo desta publicação é aprofundar os contratos inteligentes para criar um aplicativo que não apenas capture necessidades e funções realistas para uma cédula de HOA, mas também projete um que possa ser reutilizado de uma eleição para outra.
Antes de começarmos, vamos definir um contrato inteligente:
“Um contrato inteligente é um programa executado em um endereço no Ethereum. Eles são compostos de dados e funções que podem ser executados ao receber uma transação. Aqui está uma visão geral do que compõe um contrato inteligente.”
fonte
ethereum.org
Acredite ou não, um exemplo fácil de contrato inteligente pode ser encontrado em uma simples máquina de chicletes:
As pessoas entendem facilmente o custo relacionado à compra com a máquina de chicletes. Normalmente, este é um trimestre (EUA). É importante ressaltar aqui que o cliente é anônimo, pois a máquina de chicletes não exige saber quem é a pessoa antes de lhe dar um chiclete salgado.
O consumidor anônimo coloca dinheiro na máquina de chicletes e gira o botão para aceitar os termos do contrato. Essa etapa é importante porque a transação é transparente e ponto a ponto: entre você e a máquina. A transação também é segura, pois você deve fornecer a moeda esperada para usar a máquina de chicletes.
Assim que a moeda cai dentro da máquina de chicletes, os termos do contrato são aceitos e um chiclete rola até o fundo da máquina, permitindo que o cliente receba sua compra. Neste ponto, o contrato é totalmente executado.
O cliente deve aceitar o que é fornecido, o que significa que não pode devolver o chiclete ou inverter o mostrador para receber a moeda de volta. Da mesma forma, os contratos inteligentes geralmente são irreversíveis e não modificáveis.
Além de exemplos com base financeira, alguns cenários em que interações anônimas, sem confiança, descentralizadas e transparentes que são irreversíveis e não modificáveis podem ser implementadas são observados abaixo:
Em todos os casos, o conteúdo do contrato inteligente pode ser recuperado e revisado sempre que possível, sem a capacidade de alterar ou modificar os resultados. Cada caso de uso acima fornece o contrato inteligente como o sistema de registro para as informações subjacentes.
Neste momento, os contratos inteligentes não são acordos juridicamente vinculativos, exceto por alguns valores discrepantes. Isso significa que, se você não estiver satisfeito com o resultado do seu contrato inteligente, não será possível levar sua questão a um juiz em algum sistema judicial.
Existem algumas exceções, como no estado do Arizona, onde os contratos inteligentes são considerados juridicamente vinculativos. Além disso, se você estiver no estado da Califórnia e sua certidão de casamento estiver contida em um contrato inteligente, esse contrato também será juridicamente vinculativo. A expectativa é que mais governos reconheçam os contratos inteligentes como acordos juridicamente vinculativos no futuro.
Com base no contrato inteligente binário simples (sim/não) da publicação “Moving From Full-Stack Developer To Web3 Pioneer”, vamos dar um passo à frente e assumir que existe o seguinte requisito para uma cédula HOA para um bairro que tem um único cargo a preencher:
Idealmente, o objetivo seria que um único contrato inteligente fosse usado toda vez que houvesse uma eleição de HOA. Espera-se que os candidatos ao cargo de presidente mudem de uma eleição para outra.
Agora, vamos começar a fazer um contrato inteligente para atender às nossas necessidades.
Usando o Solidity, trabalhei com
// SPDX-License-Identifier: MIT pragma solidity ^0.8.13; /********************************************************/ /* For learning purposes ONLY. Do not use in production */ /********************************************************/ // Download into project folder with `npm install @openzeppelin/contracts` import "@openzeppelin/contracts/access/Ownable.sol"; // Inherits the Ownable contract so we can use its functions and modifiers contract HOABallot is Ownable { // Custom type to describe a Presidential Candidate and hold votes struct Candidate { string name; uint256 votes; } // Array of Presidential Candidates Candidate[] public candidates; // Add a President Candidate - onlyOwner function addCandidate(string memory _name) public onlyOwner { require(bytes(_name).length > 0, "addCandidate Error: Please enter a name"); candidates.push(Candidate({name: _name, votes: 0})); } // Remove a Candidate - onlyOwner function removeCandidate(string memory _name) public onlyOwner { require(bytes(_name).length > 0, "removeCandidate Error: Please enter a name"); bool foundCandidate = false; uint256 index; bytes32 nameEncoded = keccak256(abi.encodePacked(_name)); // Set index number for specific candidate for (uint256 i = 0; i < candidates.length; i++) { if (keccak256(abi.encodePacked(candidates[i].name)) == nameEncoded) { index = i; foundCandidate = true; } } // Make sure a candidate was found require(foundCandidate, "removeCandidate Error: Candidate not found"); // shift candidate to be removed to the end of the array and the rest forward for (uint256 i = index; i < candidates.length - 1; i++) { candidates[i] = candidates[i + 1]; } // remove last item from array candidates.pop(); } // Reset the President Vote Counts - onlyOwner function resetVoteCount() public onlyOwner { for (uint256 p = 0; p < candidates.length; p++) { candidates[p].votes = 0; } } // Add a vote to a candidate by name function addVoteByName(string memory _name) public { require(bytes(_name).length > 0, "addVoteByName Error: Please enter a name"); // Encode name so only need to do once bytes32 nameEncoded = keccak256(abi.encodePacked(_name)); for (uint256 i = 0; i < candidates.length; i++) { // solidity can't compare strings directly, need to compare hash if (keccak256(abi.encodePacked(candidates[i].name)) == nameEncoded) { candidates[i].votes += 1; } } } // Returns all the Presidential Candidates and their vote counts function getCandidates() public view returns (Candidate[] memory) { return candidates; } function getWinner() public view returns (Candidate memory winner) { uint256 winningVoteCount = 0; for (uint256 i = 0; i < candidates.length; i++) { if (candidates[i].votes > winningVoteCount) { winningVoteCount = candidates[i].votes; winner = candidates[i]; } } return winner; } }
Aqui estão alguns itens importantes relacionados ao design do contrato inteligente:
Agora, vamos preparar o contrato inteligente para uso.
Para poder usar nosso contrato inteligente, criaremos um projeto Truffle simples e implantaremos o contrato na rede de teste Ropsten. Para fazer isso, primeiro precisamos da versão mais recente do Truffle. Com
npm install -g truffle
A instalação da versão mais recente nos dará acesso ao
Em seguida, crie um novo diretório e inicialize um novo projeto Truffle.
mkdir hoa-ballot-contract && cd hoa-ballot-contract truffle init
Isso criará um projeto de contrato inteligente básico que podemos preencher como acharmos adequado. Então abra o projeto em seu editor de código favorito e vamos ao que interessa!
Para aproveitar o OpenZeppelin, o seguinte comando também precisa ser executado na pasta do projeto:
npm install @openzeppelin/contracts
Abra o arquivo truffle-config.js e adicionaremos o Truffle Dashboard dentro do objeto de networks
. Além de todo o clichê comentado, nosso objeto agora deve ficar assim:
networks: { dashboard: { port: 24012, } }
Para a próxima etapa, criaremos um novo arquivo de contrato inteligente. Dentro da pasta de contratos , crie um novo arquivo e nomeie-o HOABallot.sol . A partir daqui, vamos apenas colar o contrato inteligente acima.
A última coisa que precisamos fazer antes de implantar este contrato é configurar o script de implantação. Usando o conteúdo abaixo, precisamos criar um novo arquivo na pasta migrations chamado 2_hoaballot_migration.js .
const HOABallot = artifacts.require("HOABallot"); Module.exports = function (deployer) { deployer.deploy(HOABallot); }
Agora estamos prontos para implantar nosso contrato na rede de teste Ropsten. Em uma nova janela de terminal, digite o seguinte comando para iniciar o painel:
truffle dashboard
Uma vez em execução, nosso navegador deve aparecer com uma interface solicitando que conectemos nossa carteira. Se isso não aparecer para você, navegue até localhost:24012
.
Clicar uma vez no botão METAMASK iniciará o MetaMask por meio do plug-in do navegador. Se você não tiver uma extensão de navegador de carteira instalada, poderá obter uma em
Após inserir uma senha válida e utilizar o botão Desbloquear , o Painel de Trufas confirma a rede a ser utilizada:
Depois de clicar no botão CONFIRMAR , o Painel de trufas agora está ouvindo as solicitações:
Vamos precisar de Ropsten Eth para realizar a implantação. Se você não tiver nenhum, você pode
Tudo o que temos a fazer agora é implantar o contrato. Na janela original do terminal, verifique se você está na pasta do projeto e digite o comando:
truffle migrate --network dashboard
O Truffle compilará automaticamente nosso contrato inteligente e encaminhará a solicitação por meio do painel. Cada solicitação seguirá o mesmo fluxo listado abaixo.
Primeiramente, o Painel de Trufas pede confirmação para processar o pedido:
Ao pressionar o botão PROCESS , o plug-in MetaMask também pedirá confirmação:
O botão de confirmação permitirá que os fundos sejam removidos desta carteira associada para processar cada solicitação.
Quando o processo estiver concluído, as seguintes informações aparecerão na janela do terminal usado para emitir o comando truffle migrar:
2_hoaballot_migration.js ======================== Deploying 'HOABallot' --------------------- > transaction hash: 0x5370b6f9ee1f69e92cc6289f9cb0880386f15bff389b54ab09a966c5d144f59esage. > Blocks: 0 Seconds: 32 > contract address: 0x2981d347e288E2A4040a3C17c7e5985422e3cAf2 > block number: 12479257 > block timestamp: 1656386400 > account: 0x7fC3EF335D16C0Fd4905d2C44f49b29BdC233C94 > balance: 41.088173901232893417 > gas used: 1639525 (0x190465) > gas price: 2.50000001 gwei > value sent: 0 ETH > total cost: 0.00409881251639525 ETH > Saving migration to chain. > Saving artifacts ------------------------------------- > Total cost: 0.00409881251639525 ETH Summary ======= > Total deployments: 1 > Final cost: 0.00409881251639525 ETH
Agora, usando o valor do endereço do contrato , podemos validar o contrato inteligente usando a seguinte URL:
Agora podemos mudar e começar a construir o Dapp.
Vou criar um aplicativo React chamado hoa-ballot-client
usando o React CLI:
npx create-react-app hoa-ballot-client
Em seguida, alterei os diretórios para a pasta recém-criada e executei o seguinte para instalar as dependências web3 e OpenZepplin no aplicativo React:
cd hoa-ballot-client npm install web3 npm install @openzeppelin/contracts —save
Com base no conteúdo do arquivo de contrato inteligente HOABallot.sol
, naveguei até a pasta build/contracts e abri o arquivo HOBallot.json
, depois usei os valores da propriedade “abi” para a constante hoaBallot
do arquivo abi.js
como mostrado abaixo:
export const hoaBallot = [ { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", "name": "previousOwner", "type": "address" }, { "indexed": true, "internalType": "address", "name": "newOwner", "type": "address" } ], "name": "OwnershipTransferred", "type": "event" }, { "inputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "name": "candidates", "outputs": [ { "internalType": "string", "name": "name", "type": "string" }, { "internalType": "uint256", "name": "votes", "type": "uint256" } ], "stateMutability": "view", "type": "function", "constant": true }, { "inputs": [], "name": "owner", "outputs": [ { "internalType": "address", "name": "", "type": "address" } ], "stateMutability": "view", "type": "function", "constant": true }, { "inputs": [], "name": "renounceOwnership", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "newOwner", "type": "address" } ], "name": "transferOwnership", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "string", "name": "_name", "type": "string" } ], "name": "addCandidate", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "string", "name": "_name", "type": "string" } ], "name": "removeCandidate", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "name": "resetVoteCount", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "string", "name": "_name", "type": "string" } ], "name": "addVoteByName", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "name": "getCandidates", "outputs": [ { "components": [ { "internalType": "string", "name": "name", "type": "string" }, { "internalType": "uint256", "name": "votes", "type": "uint256" } ], "internalType": "struct HOABallot.Candidate[]", "name": "", "type": "tuple[]" } ], "stateMutability": "view", "type": "function", "constant": true }, { "inputs": [], "name": "getWinner", "outputs": [ { "components": [ { "internalType": "string", "name": "name", "type": "string" }, { "internalType": "uint256", "name": "votes", "type": "uint256" } ], "internalType": "struct HOABallot.Candidate", "name": "winner", "type": "tuple" } ], "stateMutability": "view", "type": "function", "constant": true } ];
Este arquivo foi colocado em uma pasta abi recém-criada dentro da pasta src do aplicativo React.
Agora, precisamos atualizar o arquivo React Apps.js. Vamos primeiro começar com o topo do arquivo, que precisa ser configurado conforme mostrado abaixo:
import React, { useState } from "react"; import { hoaBallot } from "./abi/abi"; import Web3 from "web3"; import "./App.css"; const web3 = new Web3(Web3.givenProvider); const contractAddress = "0x2981d347e288E2A4040a3C17c7e5985422e3cAf2"; const storageContract = new web3.eth.Contract(hoaBallot, contractAddress);
O contractAddress pode ser encontrado de várias maneiras. Nesse caso, usei os resultados no comando truffle — migration da CLI. Outra opção é usar o site Etherscan.
Agora, tudo o que resta é criar o código React padrão para realizar as seguintes coisas:
Em minha publicação “Moving From Full-Stack Developer To Web3 Pioneer”, adicionei o componente Nav também, para que o endereço do eleitor seja exibido para fácil referência.
O aplicativo React atualizado agora aparece da seguinte forma:
const web3 = new Web3(Web3.givenProvider); const contractAddress = "0x2981d347e288E2A4040a3C17c7e5985422e3cAf2"; const storageContract = new web3.eth.Contract(hoaBallot, contractAddress); const gasMultiplier = 1.5; const useStyles = makeStyles((theme) => ({ root: { "& > *": { margin: theme.spacing(1), }, }, })); const StyledTableCell = styled(TableCell)(({ theme }) => ({ [`&.${tableCellClasses.head}`]: { backgroundColor: theme.palette.common.black, color: theme.palette.common.white, fontSize: 14, fontWeight: 'bold' }, [`&.${tableCellClasses.body}`]: { fontSize: 14 }, })); function App() { const classes = useStyles(); const [newCandidateName, setNewCandidateName] = useState(""); const [account, setAccount] = useState(""); const [owner, setOwner] = useState(""); const [candidates, updateCandidates] = useState([]); const [winner, setWinner] = useState("unknown candidate"); const [waiting, setWaiting] = useState(false); const loadAccount = async(useSpinner) => { if (useSpinner) { setWaiting(true); } const web3 = new Web3(Web3.givenProvider || "http://localhost:8080"); const accounts = await web3.eth.getAccounts(); setAccount(accounts[0]); if (useSpinner) { setWaiting(false); } } const getOwner = async (useSpinner) => { if (useSpinner) { setWaiting(true); } const owner = await storageContract.methods.owner().call(); setOwner(owner); if (useSpinner) { setWaiting(false); } }; const getCandidates = async (useSpinner) => { if (useSpinner) { setWaiting(true); } const candidates = await storageContract.methods.getCandidates().call(); updateCandidates(candidates); await determineWinner(); if (useSpinner) { setWaiting(false); } }; const determineWinner = async () => { const winner = await storageContract.methods.getWinner().call(); if (winner && winner.name) { setWinner(winner.name); } else { setWinner("<unknown candidate>") } } const vote = async (candidate) => { setWaiting(true); const gas = (await storageContract.methods.addVoteByName(candidate).estimateGas({ data: candidate, from: account })) * gasMultiplier; let gasAsInt = gas.toFixed(0); await storageContract.methods.addVoteByName(candidate).send({ from: account, data: candidate, gasAsInt, }); await getCandidates(false); setWaiting(false); } const removeCandidate = async (candidate) => { setWaiting(true); const gas = (await storageContract.methods.removeCandidate(candidate).estimateGas({ data: candidate, from: account })) * gasMultiplier; let gasAsInt = gas.toFixed(0); await storageContract.methods.removeCandidate(candidate).send({ from: account, data: candidate, gasAsInt, }); await getCandidates(false); setWaiting(false); } const addCandidate = async () => { setWaiting(true); const gas = (await storageContract.methods.addCandidate(newCandidateName).estimateGas({ data: newCandidateName, from: account })) * gasMultiplier; let gasAsInt = gas.toFixed(0); await storageContract.methods.addCandidate(newCandidateName).send({ from: account, data: newCandidateName, gasAsInt, }); await getCandidates(false); setWaiting(false); } React.useEffect(() => { setWaiting(true); getOwner(false).then(r => { loadAccount(false).then(r => { getCandidates(false).then(r => { setWaiting(false); }); }); }); // eslint-disable-next-line react-hooks/exhaustive-deps },[]); return ( <div className={classes.root}> <Nav /> <div className="main"> <div className="card"> <Typography variant="h3"> HOABallot </Typography> {(owner && owner.length > 0) && ( <div className="paddingBelow"> <Typography variant="caption" > This ballot is owned by: {owner} </Typography> </div> )} {waiting && ( <div className="spinnerArea" > <CircularProgress /> <Typography gutterBottom> Processing Request ... please wait </Typography> </div> )} {(owner && owner.length > 0 && account && account.length > 0 && owner === account) && ( <div className="ownerActions generalPadding"> <Grid container spacing={3}> <Grid item xs={12}> <Typography variant="h6" gutterBottom> Ballot Owner Actions </Typography> </Grid> <Grid item xs={6} sm={6}> <TextField id="newCandidateName" value={newCandidateName} label="Candidate Name" variant="outlined" onChange={event => { const { value } = event.target; setNewCandidateName(value); }} /> </Grid> <Grid item xs={6} sm={6}> <Button id="addCandidateButton" className="button" variant="contained" color="primary" type="button" size="large" onClick={addCandidate}>Add New Candidate</Button> </Grid> </Grid> </div> )} <Typography variant="h5" gutterBottom className="generalPadding"> Candidates </Typography> {(!candidates || candidates.length === 0) && ( <div> <div className="paddingBelow"> <Typography variant="normal"> No candidates current exist. </Typography> </div> <div> <Typography variant="normal" gutterBottom> Ballot owner must use the <strong>ADD NEW CANDIDATE</strong> button to add candidates. </Typography> </div> </div> )} {(candidates && candidates.length > 0) && ( <div> <TableContainer component={Paper}> <Table sx={{ minWidth: 650 }} aria-label="customized table"> <TableHead> <TableRow> <StyledTableCell>Candidate Name</StyledTableCell> <StyledTableCell align="right">Votes</StyledTableCell> <StyledTableCell align="center">Actions</StyledTableCell> </TableRow> </TableHead> <TableBody> {candidates.map((row) => ( <TableRow key={row.name} sx={{ '&:last-child td, &:last-child th': { border: 0 } }} > <TableCell component="th" scope="row"> {row.name} </TableCell> <TableCell align="right">{row.votes}</TableCell> <TableCell align="center"> <Button color="success" variant="contained" onClick={() => { vote(row.name); }} > Vote </Button> {(owner && owner.length > 0 && account && account.length > 0 && owner === account) && <Button color="error" variant="contained" onClick={() => { removeCandidate(row.name); }} > Remove Candidate </Button> } </TableCell> </TableRow> ))} </TableBody> </Table> </TableContainer> <div className="generalPadding"> <Typography variant="normal" gutterBottom> {winner} is winning the election. </Typography> </div> </div> )} </div> </div> </div> ); } export default App;
Para iniciar o Dapp baseado em React, o Yarn CLI pode ser usado:
yarn start
Depois de compilado e validado, o aplicativo aparecerá na tela, conforme imagem abaixo:
Durante o vídeo:
Desde a implantação do contrato inteligente, qualquer pessoa pode visualizar o histórico completo no seguinte URL:
https://ropsten.etherscan.io/address/0x2981d347e288E2A4040a3C17c7e5985422e3cAf2
Desde 2021, tento viver de acordo com a seguinte declaração de missão, que sinto que pode ser aplicada a qualquer profissional de tecnologia:
“Concentre seu tempo em fornecer recursos/funcionalidades que ampliem o valor de sua propriedade intelectual. Aproveite estruturas, produtos e serviços para todo o resto.”
J. Vester
Os contratos inteligentes fornecem a capacidade de permitir que duas partes entrem em um acordo em que o resultado do contrato se torna um registro oficial da transação. A adoção de um contrato inteligente segue minha declaração de missão pessoal, pois a estrutura subjacente evita reinventar a roda quando surge a necessidade de tal contrato.
Ao mesmo tempo, o próprio design do contrato inteligente vai um passo além e atende à minha declaração de missão a partir de um fator de reutilização. Neste exemplo, o mesmo contrato inteligente HOA pode ser usado, apesar de diferentes candidatos concorrerem na eleição atual. Aqui, aproveitamos o poder do contrato inteligente para evitar a criação de um novo contrato inteligente sempre que houver uma eleição.
Ao usar o Etherscan para pesquisar o valor de conversão de uma das transações usando o conversor de ETH para USD do Google, o custo por transação foi de 0,24 (USD) para 0,0001348975 ETH. Ironicamente, esse era o custo de um modesto chiclete de uma máquina de chicletes quando eu era criança.
Se você quiser saber mais sobre contratos inteligentes, a equipe da ConsenSys forneceu excelentes recursos para ajudá-lo a prototipar suas ideias para ver se a adoção de contratos inteligentes é um caso de uso válido.
Se você estiver interessado no código-fonte deste artigo, poderá encontrá-lo nos seguintes URLs:
https://github.com/paul-mcaviney/smart-contract-deep-dive/blob/main/HOABallot.sol
https://gitlab.com/johnjvester/hoa-ballot-contract
https://gitlab.com/johnjvester/hoa-ballot-client
Tenha um ótimo dia!