La "
Le résultat final de mon article original a démontré comment une association de propriétaires (HOA) pouvait utiliser la technologie Web3 pour héberger son scrutin électoral. Le problème avec la conception originale est que le contrat intelligent sous-jacent ne permettait qu'une seule réponse oui ou non. C'était par conception pour garder le contrat intelligent simple tout en introduisant d'autres concepts nécessaires pour créer un bulletin de vote HOA à l'aide des technologies Web3.
Le but de cette publication est d'approfondir les contrats intelligents pour créer une application qui non seulement capture les besoins et les fonctions réalistes d'un scrutin HOA, mais en conçoit une qui peut être réutilisée d'une élection à l'autre.
Avant de commencer, définissons un contrat intelligent :
« Un contrat intelligent est un programme qui s'exécute à une adresse sur Ethereum. Ils sont constitués de données et de fonctions qui peuvent s'exécuter lors de la réception d'une transaction. Voici un aperçu de ce qui constitue un contrat intelligent. »
la source
ethereum.org
Croyez-le ou non, un exemple simple de contrat intelligent peut être trouvé dans une simple machine à chewing-gum :
Les gens comprennent facilement le coût lié à l'achat avec la machine à chewing-gum. Normalement, il s'agit d'un trimestre (américain). Il est important de préciser ici que le client est anonyme, car le distributeur de chewing-gum ne nécessite pas de savoir qui est une personne avant de lui donner un chewing-gum salé.
Le consommateur anonyme place de l'argent dans le distributeur de chewing-gum et tourne le cadran pour accepter les termes du contrat. Cette étape est importante car la transaction est transparente et peer-to-peer : entre vous et la machine. La transaction est également sécurisée puisque vous devez fournir la devise attendue pour utiliser le distributeur de chewing-gum.
Une fois que la monnaie tombe à l'intérieur de la machine à gommes, les termes du contrat sont acceptés et une boule de gomme roule vers le bas de la machine, permettant au client de recevoir son achat. À ce stade, le contrat est entièrement exécuté.
Le client doit accepter ce qui est fourni, ce qui signifie qu'il ne peut pas retourner le chewing-gum ou inverser le cadran pour récupérer sa monnaie. De la même manière, les contrats intelligents sont généralement irréversibles et non modifiables.
Outre les exemples à motivation financière, certains scénarios dans lesquels des interactions anonymes, sans confiance, décentralisées et transparentes, irréversibles et non modifiables pourraient être mises en œuvre sont indiqués ci-dessous :
Dans tous les cas, le contenu du contrat intelligent peut être rappelé et revu aussi souvent que possible, sans possibilité de changer ou de modifier les résultats. Chaque cas d'utilisation ci-dessus fournit le contrat intelligent comme système d'enregistrement des informations sous-jacentes.
À l'heure actuelle, les contrats intelligents ne sont pas des accords juridiquement contraignants, à l'exception de quelques exceptions. Cela signifie que si vous n'êtes pas satisfait du résultat de votre contrat intelligent, il n'est pas possible de porter votre problème devant un juge dans certains systèmes judiciaires.
Il existe quelques exceptions, comme dans l'État de l'Arizona, où les contrats intelligents sont considérés comme juridiquement contraignants. De plus, si vous êtes dans l'État de Californie et que votre licence de mariage est contenue dans un contrat intelligent, cet accord est également juridiquement contraignant. On s'attend à ce que davantage de gouvernements reconnaissent à l'avenir les contrats intelligents comme des accords juridiquement contraignants.
S'appuyant sur le simple contrat intelligent binaire (oui/non) de la publication "Moving From Full-Stack Developer To Web3 Pioneer", faisons un pas en avant et supposons que l'exigence suivante existe pour un scrutin HOA pour un quartier qui a un seul poste à pourvoir :
Idéalement, l'objectif serait qu'un seul contrat intelligent soit utilisé chaque fois qu'il y a une élection HOA. On s'attend à ce que les candidats au poste de président changent d'une élection à l'autre.
Maintenant, commençons à créer un contrat intelligent pour répondre à nos besoins.
En utilisant Solidity, j'ai travaillé avec
// 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; } }
Voici quelques éléments clés liés à la conception du contrat intelligent :
Maintenant, préparons le contrat intelligent prêt à l'emploi.
Afin de pouvoir utiliser notre contrat intelligent, nous allons créer un projet Truffle simple et déployer le contrat sur le testnet de Ropsten. Pour ce faire, nous aurons d'abord besoin de la version la plus récente de Truffle. Avec
npm install -g truffle
L'installation de la dernière version nous donnera accès au
Ensuite, créez un nouveau répertoire et initialisez un nouveau projet Truffle.
mkdir hoa-ballot-contract && cd hoa-ballot-contract truffle init
Cela créera un projet de contrat intelligent barebones que nous pourrons remplir comme bon nous semble. Alors ouvrez le projet dans votre éditeur de code préféré, et allons-y !
Afin de tirer parti d'OpenZeppelin, la commande suivante doit également être exécutée dans le dossier du projet :
npm install @openzeppelin/contracts
Ouvrez le fichier truffle-config.js et nous ajouterons le tableau de bord Truffle dans l'objet networks
. Mis à part tous les passe-partout commentés, notre objet devrait maintenant ressembler à ceci :
networks: { dashboard: { port: 24012, } }
Pour la prochaine étape, nous allons créer un nouveau fichier de contrat intelligent. Dans le dossier des contrats , créez un nouveau fichier et nommez-le HOABallot.sol . À partir de là, nous allons simplement coller le contrat intelligent ci-dessus.
La dernière chose que nous devons faire avant de pouvoir déployer ce contrat est de configurer le script de déploiement. En utilisant le contenu ci-dessous, nous devons créer un nouveau fichier dans le dossier migrations appelé 2_hoaballot_migration.js .
const HOABallot = artifacts.require("HOABallot"); Module.exports = function (deployer) { deployer.deploy(HOABallot); }
Nous sommes maintenant prêts à déployer notre contrat sur le testnet de Ropsten. Dans une nouvelle fenêtre de terminal, saisissez la commande suivante pour démarrer le tableau de bord :
truffle dashboard
Une fois lancé, notre navigateur devrait apparaître avec une interface nous demandant de connecter notre portefeuille. Si cela ne s'affiche pas, accédez à localhost:24012
.
Un simple clic sur le bouton METAMASK lancera MetaMask via le plug-in du navigateur. Si vous n'avez pas installé d'extension de navigateur de portefeuille, vous pouvez en obtenir une sur
Après avoir saisi un mot de passe valide et appuyé sur le bouton Déverrouiller , le Truffle Dashboard confirme le réseau à utiliser :
Après avoir cliqué sur le bouton CONFIRMER , le tableau de bord Truffle écoute maintenant les demandes :
Nous aurons besoin de Ropsten Eth pour effectuer le déploiement. Si vous n'en avez pas, vous pouvez
Il ne nous reste plus qu'à déployer le contrat. Dans votre fenêtre de terminal d'origine, assurez-vous d'être dans le dossier du projet et tapez la commande :
truffle migrate --network dashboard
Truffle compilera automatiquement notre contrat intelligent, puis acheminera la demande via le tableau de bord. Chaque demande suivra le même flux indiqué ci-dessous.
Tout d'abord, le tableau de bord Truffle demande une confirmation pour traiter la demande :
En appuyant sur le bouton PROCESS , le plug-in MetaMask demandera également une confirmation :
Le bouton Confirmation permettra de retirer des fonds de ce portefeuille associé afin de traiter chaque demande.
Une fois le processus terminé, les informations suivantes apparaîtront dans la fenêtre du terminal utilisée pour émettre la commande truffle migrate :
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
Maintenant, en utilisant la valeur de l' adresse du contrat , nous pouvons valider le contrat intelligent en utilisant l'URL suivante :
Nous pouvons maintenant basculer et commencer à construire le Dapp.
Je vais créer une application React appelée hoa-ballot-client
à l'aide de la CLI React :
npx create-react-app hoa-ballot-client
Ensuite, j'ai changé les répertoires dans le dossier nouvellement créé et j'ai exécuté ce qui suit pour installer les dépendances web3 et OpenZepplin dans l'application React :
cd hoa-ballot-client npm install web3 npm install @openzeppelin/contracts —save
Sur la base du contenu du fichier de contrat intelligent HOABallot.sol
, j'ai navigué vers le dossier build/contracts et ouvert le fichier HOBallot.json
, puis utilisé les valeurs de la propriété "abi" pour la constante hoaBallot
du fichier abi.js
comme indiqué ci-dessous:
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 } ];
Ce fichier a été placé dans un dossier abi nouvellement créé dans le dossier src de l'application React.
Maintenant, nous devons mettre à jour le fichier React Apps.js. Commençons d'abord par le haut du fichier, qui doit être configuré comme indiqué ci-dessous :
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);
Le contractAddress peut être trouvé de plusieurs façons. Dans ce cas, j'ai utilisé les résultats de la commande CLI truffle — migrate. Une autre option consiste à utiliser le site Etherscan.
Maintenant, il ne reste plus qu'à créer un code React standard pour accomplir les choses suivantes :
Dans ma publication "Moving From Full-Stack Developer To Web3 Pioneer", j'ai également ajouté le composant Nav, afin que l'adresse de l'électeur soit affichée pour une référence facile.
L'application React mise à jour apparaît maintenant comme suit :
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;
Pour démarrer la Dapp basée sur React, la CLI Yarn peut être utilisée :
yarn start
Une fois compilée et validée, l'application apparaîtra à l'écran, comme indiqué ci-dessous :
Pendant la vidéo :
Depuis le déploiement du contrat intelligent, n'importe qui peut consulter l'historique complet à l'URL suivante :
https://ropsten.etherscan.io/address/0x2981d347e288E2A4040a3C17c7e5985422e3cAf2
Depuis 2021, j'essaie de vivre selon l'énoncé de mission suivant, qui, selon moi, peut s'appliquer à tout professionnel de la technologie :
« Consacrez votre temps à fournir des caractéristiques/fonctionnalités qui étendent la valeur de votre propriété intellectuelle. Tirez parti des cadres, des produits et des services pour tout le reste.
J.Vester
Les contrats intelligents permettent à deux parties de conclure un accord où le résultat du contrat devient un enregistrement officiel gravé dans le marbre de la transaction. L'adoption d'un contrat intelligent adhère à mon énoncé de mission personnelle en ce sens que le cadre sous-jacent évite de réinventer la roue lorsque le besoin se fait sentir d'un tel contrat.
Dans le même temps, la conception du contrat intelligent elle-même va encore plus loin et répond à mon énoncé de mission à partir d'un facteur de réutilisation. Dans cet exemple, le même contrat intelligent HOA peut être utilisé, même si différents candidats se présentent aux élections en cours. Ici, nous tirons parti de la puissance du contrat intelligent pour éviter de créer un nouveau contrat intelligent à chaque fois qu'il y a une élection.
Lors de l'utilisation d'Etherscan pour rechercher la valeur de conversion de l'une des transactions à l'aide du convertisseur ETH en USD de Google, le coût par transaction était de 0,24 (USD) pour 0,0001348975 ETH. Ironiquement, c'était le prix d'un modeste chewing-gum sorti d'une machine à chewing-gum quand j'étais enfant.
Si vous souhaitez en savoir plus sur les contrats intelligents, l'équipe de ConsenSys a fourni d'excellentes ressources pour vous aider à prototyper vos idées afin de voir si l'adoption de contrats intelligents est un cas d'utilisation valable.
Si vous êtes intéressé par le code source de cet article, vous pouvez le trouver aux URL suivantes :
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
Passez une très bonne journée !