Crie Sua Própria DAO Full Stack na Blockchain Celo
Introdução
Uma DAO é uma organização autônoma descentralizada que se tornou possível graças à blockchain. Elas são construídas e sustentadas por uma comunidade de indivíduos que investem pessoalmente nelas e as fortalecem por meio de um mecanismo de votação por consenso. Neste tutorial, mostrarei a você como criar um contrato DAO no Solidity. Vamos analisar a implementação de uma DAO simples que permite que os membros proponham e votem em propostas e executem as propostas depois de aprovadas. Abordaremos os aspectos essenciais de uma DAO, como a estrutura do contrato inteligente, as funções para adicionar e remover membros, criar e votar em propostas e executar as propostas aprovadas. Ao final deste tutorial, você terá uma sólida compreensão de como funciona uma DAO.
Aqui está um link de demonstração do que iremos criar.
E um screenshot.
Pré-requisitos
Para acompanhar integralmente esses tutoriais, você deve ter um conhecimento básico das seguintes tecnologias:
- Solidity, conceitos de contrato inteligente de blockchain.
- React.
- Desenvolvimento básico da Web.
Requisitos
- Solidity.
- React.
- Bootstrap.
- NodeJS na versão 12.0.1 ou superior instalado.
- Carteira de Extensão Celo.
- IDE do Remix
Contrato inteligente
Vamos começar a escrever nosso contrato inteligente no IDE do Remix
O código concluído deve ter a seguinte aparência:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract CELODAO {
address owner;
struct MemberInfo {
address memberAddress;
uint256 votingPower;
}
mapping (address => MemberInfo) public members;
uint256 public memberCount;
event NewMember(address indexed _address, uint256 _votingPower);
event MemberRemoved(address indexed _address);
event ProposalCreated(uint256 indexed proposalId, address indexed proposer, string description);
event ProposalVoted(uint256 indexed proposalId, address indexed voter, bool vote);
struct Proposal {
uint256 proposalId;
address proposer;
string description;
uint256 yesVotes;
uint256 noVotes;
mapping (address => bool) votes;
bool executed;
}
mapping (uint256 => Proposal) public proposals;
uint256 public proposalCount;
constructor() {
owner = msg.sender;
}
function addMember(address _address, uint256 _votingPower) public {
require(msg.sender == owner, "Only contract owner can add a new member.");
require(members[_address].memberAddress == address(0), "The address is already a member.");
require(_votingPower > 0, "The voting power must be positive.");
memberCount ++;
members[_address] = MemberInfo(_address, _votingPower);
emit NewMember(_address, _votingPower);
}
function removeMember(address _address) public {
require(msg.sender == owner, "Only contract owner can remove a member.");
require(members[_address].memberAddress != address(0), "The address is not a member.");
require(proposals[proposalCount].proposer != _address, "Member cannot be removed while they have an active proposal.");
members[_address].memberAddress = address(0);
memberCount --;
emit MemberRemoved(_address);
}
function createProposal(string memory _description) public {
Proposal storage proposal = proposals[proposalCount];
proposal.proposalId = proposalCount;
proposal.proposer = msg.sender;
proposal.description = _description;
proposal.yesVotes = 0;
proposal.noVotes = 0;
proposal.executed = false;
proposalCount ++;
emit ProposalCreated(proposalCount, msg.sender, _description);
}
function getProposal(uint _index) public view returns(
uint,
address,
string memory,
uint,
uint,
bool
){
Proposal storage proposal = proposals[_index];
return(
proposal.proposalId,
proposal.proposer,
proposal.description,
proposal.yesVotes,
proposal.noVotes,
proposal.executed
);
}
function vote(uint256 _proposalId, bool _vote) public {
require(proposals[_proposalId].votes[msg.sender] == false, "The member has already voted on this proposal.");
require(proposals[_proposalId].executed == false, "The proposal has already been executed.");
proposals[_proposalId].votes[msg.sender] = _vote;
if (_vote) {
proposals[_proposalId].yesVotes += members[msg.sender].votingPower;
} else {
proposals[_proposalId].noVotes += members[msg.sender].votingPower;
}
proposals[_proposalId].votes[msg.sender] == true;
emit ProposalVoted(_proposalId, msg.sender, _vote);
}
function executeProposal(uint256 _proposalId) public {
require(proposals[_proposalId].proposer == msg.sender, "Only the proposer can execute the proposal.");
require(proposals[_proposalId].executed == false, "The proposal has already been executed.");
require(proposals[_proposalId].yesVotes > proposals[_proposalId].noVotes, "The proposal must have more yes votes than no votes.");
proposals[_proposalId].executed = true;
// Execute as ações descritas na proposta aqui
// ...
}
function getProposalsLength() public view returns(uint){
return(proposalCount);
}
}
Desmembramento
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
Primeiro, declaramos nossa licença e a versão do Solidity.
contract CELODAO {
address owner;
struct MemberInfo {
address memberAddress;
uint256 votingPower;
}
mapping (address => MemberInfo) public members;
uint256 public memberCount;
}
Nesta seção, definimos nosso contrato inteligente CELODAO. Em seguida, declaramos uma variável de estado chamada owner
que armazenará o endereço do proprietário do contrato inteligente.
Também declaramos uma nova struct MemberInfo
que contém dois campos: memberAddress
e votingPower
. Essa struct será usada para armazenar informações sobre cada membro da DAO.
Por fim, declaramos um mapeamento público chamado members
que mapeia um endereço para uma struct MemberInfo
. Ele será usado para armazenar informações sobre cada membro da DAO e, em seguida, declaramos uma variável de estado chamada memberCount
, que manterá o controle do número total de membros em nossa DAO.
event NewMember(address indexed _address, uint256 _votingPower);
event MemberRemoved(address indexed _address);
event ProposalCreated(uint256 indexed proposalId, address indexed proposer, string description);
event ProposalVoted(uint256 indexed proposalId, address indexed voter, bool vote);
Aqui declaramos vários eventos que serão emitidos quando determinadas ações forem executadas na DAO. Esses eventos podem ser ouvidos por aplicativos externos para rastrear o estado da nossa DAO.
struct Proposal {
uint256 proposalId;
address proposer;
string description;
uint256 yesVotes;
uint256 noVotes;
mapping (address => bool) votes;
bool executed;
}
mapping (uint256 => Proposal) public proposals;
uint256 public proposalCount;
constructor() {
owner = msg.sender;
}
Nesta seção, declaramos uma nova struct chamada Proposal
que será usada para armazenar informações sobre cada proposta. Ela contém vários campos, incluindo o ID da proposta, o endereço do proponente, uma descrição da proposta, o número de votos "sim", o número de votos "não", um mapeamento do voto de cada membro e um sinalizador para indicar se a proposta foi executada.
Também declaramos um mapeamento público chamado proposals
que mapeia um ID de proposta para uma struct Proposal. O proposalCount
manterá o controle do número total de propostas em nossa DAO.
Por fim, adicionamos uma função de construtor para o contrato CELODAO. Ela define a variável de estado do proprietário como o endereço do criador do contrato.
function addMember(address _address, uint256 _votingPower) public {
require(msg.sender == owner, "Only contract owner can add a new member.");
require(members[_address].memberAddress == address(0), "The address is already a member.");
require(_votingPower > 0, "The voting power must be positive.");
memberCount ++;
members[_address] = MemberInfo(_address, _votingPower);
emit NewMember(_address, _votingPower);
}
Em seguida, adicionamos uma nova função chamada addMember. Essa função adiciona um novo membro ao nosso contrato DAO. Ela recebe dois argumentos _address
, que é o endereço do novo membro, e _votingPower
, que é o poder de voto do novo membro. A função primeiro verifica se o chamador da função é o proprietário do contrato e se o _address
fornecido ainda não é um membro. Em seguida, aumenta a contagem de membros, cria uma nova estrutura MemberInfo
para o novo membro e a adiciona ao mapeamento de membros usando o _address
como chave. Por fim, ele emite um evento NewMember
com o endereço e o poder de voto do novo membro.
function removeMember(address _address) public {
require(msg.sender == owner, "Only contract owner can remove a member.");
require(members[_address].memberAddress != address(0), "The address is not a member.");
require(proposals[proposalCount].proposer != _address, "Member cannot be removed while they have an active proposal.");
members[_address].memberAddress = address(0);
memberCount --;
emit MemberRemoved(_address);
}
Em seguida, adicionamos uma função removeMember
. Essa função remove um membro da nossa DAO. Ela recebe um argumento _address
, que é o endereço do membro a ser removido. A função primeiro verifica se o chamador da função é o proprietário do contrato, se o _address
fornecido é realmente um membro e se o membro não tem uma proposta ativa. Em seguida, ela define o memberAddress
do membro como address(0)
, diminui o memberCount
e emite um evento MemberRemoved
com o endereço do membro removido.
function createProposal(string memory _description) public {
Proposal storage proposal = proposals[proposalCount];
proposal.proposalId = proposalCount;
proposal.proposer = msg.sender;
proposal.description = _description;
proposal.yesVotes = 0;
proposal.noVotes = 0;
proposal.executed = false;
proposalCount ++;
emit ProposalCreated(proposalCount, msg.sender, _description);
}
Agora vamos dar uma olhada na função createProposal
. Essa função cria uma nova proposta em nossa DAO. Ela recebe um argumento _description
, que é uma string que contém uma descrição da proposta.
A função primeiro cria uma referência para a struct Proposal no índice proposalCount
na matriz de propostas usando a palavra-chave storage. Em seguida, define o proposalId
como o valor de proposalCount
, o proposer como o endereço do chamador, a description como a descrição fornecida e define os yesVotes
e noVotes
iniciais como 0.
Por fim, ele define o sinalizador executed como false (falso), indicando que a proposta ainda não foi executada.
No final da função, o proposalCount
é incrementado e a nova proposta é adicionada à matriz de propostas.
function getProposal(uint _index) public view returns(
uint,
address,
string memory,
uint,
uint,
bool
){
Proposal storage proposal = proposals[_index];
return(
proposal.proposalId,
proposal.proposer,
proposal.description,
proposal.yesVotes,
proposal.noVotes,
proposal.executed
);
}
A próxima função é a getProposal()
. Essa é uma função de exibição que recebe um parâmetro _index
e retorna uma tupla contendo as várias propriedades de uma proposta: proposalId
, proposer
, description
, yesVotes
, noVotes
e executed
.
Ela cria um objeto Proposal
com o _index
correspondente e retorna as propriedades da proposta como uma tupla.
function vote(uint256 _proposalId, bool _vote) public {
require(proposals[_proposalId].votes[msg.sender] == false, "The member has already voted on this proposal.");
require(proposals[_proposalId].executed == false, "The proposal has already been executed.");
proposals[_proposalId].votes[msg.sender] = _vote;
if (_vote) {
proposals[_proposalId].yesVotes += members[msg.sender].votingPower;
} else {
proposals[_proposalId].noVotes += members[msg.sender].votingPower;
}
proposals[_proposalId].votes[msg.sender] == true;
emit ProposalVoted(_proposalId, msg.sender, _vote);
}
Em seguida, criamos uma função vote()
. A função vote permite que um membro vote em uma proposta. A função recebe dois argumentos _proposalId
é o ID da proposta que está sendo votada e _vote é um booleano que indica se o membro está votando a favor ou contra a proposta.
A primeira declaração require
verifica se o membro ainda não votou na proposta. Se o membro já tiver votado, a função falhará com uma mensagem de erro.
A segunda declaração require
verifica se a proposta ainda não foi executada. Se a proposta já tiver sido executada, a função falhará com uma mensagem de erro.
A linha proposals[_proposalId].votes[msg.sender] = _vote
registra o voto do membro no mapeamento de votos da proposta. O mapeamento de votos armazena um valor booleano que indica se um membro votou ou não na proposta. Se o membro estiver votando a favor da proposta, sua contagem de yesVotes será incrementada pelo seu poder de voto. Se ele estiver votando contra a proposta, sua contagem de noVotes será incrementada pelo seu poder de voto.
Finalmente, a função emite um evento ProposalVoted, passando o ID da proposta, o endereço do membro e seu voto. Esse evento pode ser usado para rastrear o progresso de uma proposta à medida que os membros votam nela.
function executeProposal(uint256 _proposalId) public {
require(proposals[_proposalId].proposer == msg.sender, "Only the proposer can execute the proposal.");
require(proposals[_proposalId].executed == false, "The proposal has already been executed.");
require(proposals[_proposalId].yesVotes > proposals[_proposalId].noVotes, "The proposal must have more yes votes than no votes.");
proposals[_proposalId].executed = true;
// Execute as ações descritas na proposta aqui.
// ...
}
O executeProposal()
é uma função que permite que o proponente de uma proposta a execute. A função primeiro verifica se o proponente é quem está chamando a função, se a proposta ainda não foi executada e se o número de votos "sim" é maior que o número de votos "não". Se todas essas condições forem atendidas, a função define o sinalizador executed
como verdadeiro, indicando que a proposta foi executada. Por fim, todas as ações descritas na proposta podem ser executadas.
function getProposalsLength() public view returns(uint){
return(proposalCount);
}
Por fim, getProposalsLength()
é uma função simples que retorna o número de propostas criadas no contrato. Ela simplesmente retorna o valor da variável proposalCount
.
Com isso, examinamos todo o código em nosso Contrato DAO. Esse contrato permite que os membros adicionem e removam outros membros, criem e votem em propostas e executem propostas que tenham sido aprovadas pelos membros. É uma implementação básica de uma DAO e pode ser estendida ou modificada para atender às necessidades de um caso de uso específico.
Implantação
Para implantar nosso contrato inteligente com sucesso, precisamos da carteira de extensão Celo, que pode ser baixada aqui.
Em seguida, precisamos depositar fundos em nossa carteira recém-criada, o que pode ser feito usando a faucet (torneira) Alfajores do Celo aqui.
Agora você pode depositar fundos em sua carteira e implementar seu contrato usando o plugin celo no Remix.
Frontend
Clique neste repositório de seu github.
- Clone o repositório em seu computador.
- Abra o projeto a partir do vscode.
- Execute o comando
npm install
para instalar todas as dependências necessárias para executar o aplicativo localmente.
App.js
O código completo deve ter a seguinte aparência:
import "./App.css";
import Home from "./components/home";
import { Proposals } from "./components/proposals";
import { useState, useEffect, useCallback } from "react";
import Web3 from "web3";
import { newKitFromWeb3 } from "@Celo_Academy/contractkit";
import celodao from "./contracts/celo-dao.abi.json";
const ERC20_DECIMALS = 18;
const contractAddress = "0x69dfb020bA12Ce303118E3eF81f9b9E4eB08cE17";
function App() {
const [contract, setcontract] = useState(null);
const [address, setAddress] = useState(null);
const [kit, setKit] = useState(null);
const [cUSDBalance, setcUSDBalance] = useState(0);
const [proposals, setProposals] = useState([]);
const connectToWallet = async () => {
if (window.celo) {
try {
await window.celo.enable();
const web3 = new Web3(window.celo);
let kit = newKitFromWeb3(web3);
const accounts = await kit.web3.eth.getAccounts();
const user_address = accounts[0];
kit.defaultAccount = user_address;
await setAddress(user_address);
await setKit(kit);
} catch (error) {
console.log(error);
}
} else {
alert("Error Occurred");
}
};
const getBalance = useCallback(async () => {
try {
const balance = await kit.getTotalBalance(address);
const USDBalance = balance.cUSD.shiftedBy(-ERC20_DECIMALS).toFixed(2);
const contract = new kit.web3.eth.Contract(celodao, contractAddress);
setcontract(contract);
setcUSDBalance(USDBalance);
} catch (error) {
console.log(error);
}
}, [address, kit]);
const getProposals = useCallback(async () => {
const proposalsLength = await contract.methods.getProposalsLength().call();
const proposals = [];
for (let index = 0; index < proposalsLength; index++) {
let _proposals = new Promise(async (resolve, reject) => {
let proposal = await contract.methods.getProposal(index).call();
resolve({
index: index,
proposalId: proposal[0],
proposer: proposal[1],
description: proposal[2],
yesVotes: proposal[3],
noVotes: proposal[4],
executed: proposal[6],
});
});
proposals.push(_proposals);
}
const _proposals = await Promise.all(proposals);
setProposals(_proposals);
}, [contract]);
const addProposal = async (_description) => {
try {
await contract.methods
.createProposal(_description)
.send({ from: address });
getProposals();
} catch (error) {
alert(error);
}
};
const addMember = async (_address, _votingPower) => {
try {
await contract.methods
.addMember(_address, _votingPower)
.send({ from: address });
getProposals();
} catch (error) {
alert(error);
}
};
const removeMember = async (_address) => {
try {
await contract.methods.removeMember(_address).send({ from: address });
getProposals();
} catch (error) {
alert(error);
}
};
const vote = async (_proposalId, _vote) => {
try {
await contract.methods.vote(_proposalId, _vote).send({ from: address });
getProposals();
} catch (error) {
alert(error);
}
};
const executeProposal = async (_proposalId) => {
try {
await contract.methods
.executedProposal(_proposalId)
.send({ from: address });
getProposals();
} catch (error) {
alert(error);
}
};
useEffect(() => {
connectToWallet();
}, []);
useEffect(() => {
if (kit && address) {
getBalance();
}
}, [kit, address, getBalance]);
useEffect(() => {
if (contract) {
getProposals();
}
}, [contract, getProposals]);
return (
<div className="App">
<Home
cUSDBalance={cUSDBalance}
addMember={addMember}
addProposal={addProposal}
removeMember={removeMember}
/>
<Proposals
proposals={proposals}
vote={vote}
executeProposal={executeProposal}
walletAddress={address}
/>
</div>
);
}
export default App;
Desmembramento
Vamos dar uma olhada no arquivo App.js
e dividi-lo em partes.
import "./App.css";
import Home from "./components/home";
import { Proposals } from "./components/proposals";
import { useState, useEffect, useCallback } from "react";
import Web3 from "web3";
import { newKitFromWeb3 } from "@Celo_Academy/contractkit";
import celodao from "./contracts/celo-dao.abi.json";
O primeiro passo é importar os componentes e as bibliotecas necessários. Começamos importando os componentes Home e Proposals da pasta de componentes. Em seguida, importamos os ganchos (hooks) useState
, useEffect
e useCallback
do React, bem como a biblioteca Web3 para interagir com a blockchain Ethereum. Por fim, importamos o contrato ABI (Application Binary Interface) para o contrato Celo-Dao da pasta contracts.
const ERC20_DECIMALS = 18;
const contractAddress = "0x69dfb020bA12Ce303118E3eF81f9b9E4eB08cE17";
Em seguida, definimos os decimais do ERC20 e o endereço do contrato de nosso contrato inteligente.
const [contract, setcontract] = useState(null);
const [address, setAddress] = useState(null);
const [kit, setKit] = useState(null);
const [cUSDBalance, setcUSDBalance] = useState(0);
const [proposals, setProposals] = useState([]);
A seguir, criamos as variáveis de estado para o aplicativo. Usamos o gancho useState para criar as variáveis de estado contract, address, kit, cUSDBalance e proposals.
const connectToWallet = async () => {
if (window.celo) {
try {
await window.celo.enable();
const web3 = new Web3(window.celo);
let kit = newKitFromWeb3(web3);
const accounts = await kit.web3.eth.getAccounts();
const user_address = accounts[0];
kit.defaultAccount = user_address;
await setAddress(user_address);
await setKit(kit);
} catch (error) {
console.log(error);
}
} else {
alert("Error Occurred");
}
};
Em seguida, criamos a função connectToWallet()
que permite que o usuário se conecte à sua carteira e defina o endereço e o kit.
const getBalance = useCallback(async () => {
try {
const balance = await kit.getTotalBalance(address);
const USDBalance = balance.cUSD.shiftedBy(-ERC20_DECIMALS).toFixed(2);
const contract = new kit.web3.eth.Contract(celo-dao, contractAddress);
setcontract(contract);
setcUSDBalance(USDBalance);
} catch (error) {
console.log(error);
}
}, [address, kit]);
A função getBalance()
nos permite obter o saldo cUSD do usuário e configurar o contrato.
const getProposals = useCallback(async () => {
const proposalsLength = await contract.methods.getProposalsLength().call();
const proposals = [];
for (let index = 0; index < proposalsLength; index++) {
let _proposals = new Promise(async (resolve, reject) => {
let proposal = await contract.methods.getProposal(index).call();
resolve({
index: index,
proposalId: proposal[0],
proposer: proposal[1],
description: proposal[2],
yesVotes: proposal[3],
noVotes: proposal[4],
executed: proposal[6],
});
});
proposals.push(_proposals);
}
const _proposals = await Promise.all(proposals);
setProposals(_proposals);
}, [contract]);
A função getProposals()
é usada para obter a lista de propostas do contrato. Usamos o método getProposalsLength para obter o número de propostas e fazemos um loop em cada proposta para obter suas propriedades. Em seguida, armazenamos as propostas na variável de estado proposals
.
const addProposal = async (_description) => {
try {
await contract.methods
.createProposal(_description)
.send({ from: address });
getProposals();
} catch (error) {
alert(error);
}
};
A função addProposal
é usada para adicionar uma proposta ao contrato. Usamos o método createProposal
para adicionar a proposta e, em seguida, chamamos a função getProposals()
para atualizar a variável de estado proposals.
const addMember = async (_address, _votingPower) => {
try {
await contract.methods
.addMember(_address, _votingPower)
.send({ from: address });
getProposals();
} catch (error) {
alert(error);
}
};
const removeMember = async (_address) => {
try {
await contract.methods.removeMember(_address).send({ from: address });
getProposals();
} catch (error) {
alert(error);
}
};
Usamos os métodos addMember()
e removeMember
para adicionar e remover membros da nossa Dao e, em seguida, chamamos a função getProposals()
para atualizar a variável de estado proposals
.
const vote = async (_proposalId, _vote) => {
try {
await contract.methods.vote(_proposalId, _vote).send({ from: address });
getProposals();
} catch (error) {
alert(error);
}
};
const executeProposal = async (_proposalId) => {
try {
await contract.methods
.executedProposal(_proposalId)
.send({ from: address });
getProposals();
} catch (error) {
alert(error);
}
};
As funções vote()
e executeProposal()
são usadas para votar em propostas e executá-las. Usamos os métodos vote()
e executedProposal()
para votar e executar propostas e, em seguida, chamamos a função getProposals()
para atualizar a variável de estado proposals
.
useEffect(() => {
connectToWallet();
}, []);
useEffect(() => {
if (kit && address) {
getBalance();
}
}, [kit, address, getBalance]);
useEffect(() => {
if (contract) {
getProposals();
}
}, [contract, getProposals]);
Usamos o gancho useEffect
para chamar as funções connectToWallet()
, getBalance()
e getProposals()
. Isso garante que o aplicativo esteja sempre atualizado com os dados mais recentes do contrato.
return (
<div className="App">
<Home
cUSDBalance={cUSDBalance}
addMember={addMember}
addProposal={addProposal}
removeMember={removeMember}
/>
<Proposals
proposals={proposals}
vote={vote}
executeProposal={executeProposal}
walletAddress={address}
/>
</div>
);
}
export default App;
E, por fim, renderizamos o componente App e retornamos os componentes Home e proposals com as props (propriedades) necessárias.
Próximos Passos
Espero que você tenha aprendido muito com este tutorial. Aqui estão alguns links relevantes que podem ajudá-lo a aprender mais.
Sobre o autor
Sou Jonathan Iheme, um desenvolvedor full stack de blockchain da Nigéria.
Obrigado!
Esse artigo foi escrito por 4undRaiser e traduzido por Fátima lima. O original pode ser lido aqui.
Latest comments (0)