Este artigo faz parte da série Construção de DAO na Moonbeam. Se você ainda não leu a primeira parte, recomendo que a leia no link acima.
Anteriormente, nós já tínhamos configurado o projeto com o boilerplate e também já tínhamos experimentado a funcionalidade básica da IU do boilerplate. Neste artigo, entraremos profundamente no código do contrato inteligente, para implementar todas as funcionalidades que já tentamos anteriormente.
Vamos verificar o contrato inteligente em
packages
- hardhat
- contracts
- PowDow.sol
Se você já trabalha com programação OOP, pensaria que o contrato inteligente é como uma classe, que tem muitos métodos. Sim, é quase isso, mas tem algo diferente. É como uma classe com melhoria da funcionalidade financeira incluída. Por exemplo, todos os contratos podem armazenar saldo de ETH como carteira e também podem enviar/receber ETH de outra carteira. Portanto, antes de começar, se você ainda não estiver familiarizado com a programação de Solidity, consulte aqui.
Toda linha de código é referência de arquivo em projeto, portanto, por favor, abra o arquivo enquanto caminha pelo código com este artigo.
Iniciando
Na minha opinião, quando precisamos entender algum contrato inteligente, não se deve ler o código de cima para baixo, porque isso fará você confundir a relação entre função e variável. E também o desenvolvedor do contrato inteligente sempre escreve o código para economizar o máximo possível no custo do gás, o que, algum dia, pode reduzir a legibilidade do código. No entanto, a melhor maneira de descobrir o mecanismo de algum contrato inteligente é tentar andar através do código seguindo o fluxo em cada funcionalidade do início ao fim. Muito bem! Então vamos começar com a primeira funcionalidade 🚀
Adicionar & Remover membros
uint256[] public proposalQueue;
// Criar uma proposta que mostre o endereço (membro a ser adicionado) como o proponente. E configurar para indicar o tipo de proposta, seja adicionando ou expulsando.
function _SubmitMemberProposal(address entity, string memory details, uint256 action) internal {
proposalQueue.push(proposalCount);
if(action == 0) {
Proposal storage prop = proposals[proposalCount];
prop.proposer = entity;
prop.paymentRequested = 0;
prop.startingTime = now;
// [sponsored, processed, didPass, cancelled, memberAdd, memberKick]
prop.flags = [false, false, false, false, true, false]; // memberAdd
prop.details = details;
prop.exists = true;
emit SubmitProposal(prop.proposer, 0, prop.details, prop.flags, proposalCount, msg.sender);
proposalCount += 1;
}
if(action == 1) {
Proposal storage prop = proposals[proposalCount];
prop.proposer = entity;
prop.paymentRequested = 0;
prop.startingTime = now;
prop.flags = [false, false, false, false, false, true]; // memberkick
prop.details = details;
prop.exists = true;
emit SubmitProposal(prop.proposer, 0, prop.details, prop.flags, proposalCount, msg.sender);
proposalCount += 1;
}
}
Primeiro vamos checar na linha 162 a função _SubmitMemberProtocol
. O underscore não é o açúcar sintático (é uma sintaxe dentro da linguagem de programação que tem por finalidade tornar suas construções mais fáceis de serem lidas e expressas), é apenas a convenção para diferenciar a função privada da função pública. Esta função é a interna, que cria a nova proposta na DAO. Então vamos verificar o código, você pode se perguntar o que é action
e a que este número se refere. Este também é o motivo pelo qual o contrato inteligente não é bom para a legibilidade. Na verdade esse número é apenas um enum (enumeração), mas o implementador escolhe _int (inteiro) para economizar o preço do gás. Portanto, aqui, 0 (zero) refere-se a adicionar um membro e 1 refere-se a excluir o membro. Se a ação for 0, então o contrato criará uma nova estrutura de proposta e adicionará todas as informações enviadas do cliente frontend. Vamos continuar. Há outro ponto interessante, você verá
// [sponsored, processed, didPass, cancelled, memberAdd, memberKick]
prop.flags = [false, false, false, false, true, false]
Este é um sinalizador booleano que indica o estado desta proposta. É como o indicador de status da proposta que usaremos para rastrear o estado mais tarde. Portanto, neste momento, vamos ignorá-lo. Vamos avançar. Outro ponto interessante é
emit SubmitProposal(prop.proposer, 0, prop.details, prop.flags, proposalCount, msg.sender);
Esta linha lançará o evento, que pode ser capturado pelo cliente frontend e que irá permitir que o cliente possa fazer alguma coisa com ele depois que a execução estiver concluída.
Entretanto, não quero tornar este artigo muito longo, mas você poderia, você mesmo, consultar outro caso action == 1
muito semelhante ao primeiro.
Vamos passar para a camada superior. Verifique as funções addMember
e kickMember
function addMember(address newMemAddress, string memory details) public onlyMember {
_SubmitMemberProposal(newMemAddress, details, 0); // 0 adds a member
}
function kickMember(address memToKick, string memory details) public onlyMember {
_SubmitMemberProposal(memToKick, details, 1); // 1 kicks a member
}
Aqui está apenas uma função simples que chama a função interna _SubmitMemberProposal
. Você pode perguntar por que precisamos criar uma nova função só para chamar outra função. A razão é que todo contrato Solidity é interno ou privado por padrão, mas estas duas funções têm uma palavra-chave pública que pode ser chamada de externa e estas duas funções são criadas para ser uma interface, para diminuir a quantidade dos parâmetros que o usuário precisa enviar. Não seria bom se o usuário pudesse chamar diretamente a função _SubmitMemberProposal
, pois eles podem enviar uma ação não suportada como último parâmetro para causar um overflow na função do contrato inteligente, o que o torna vulnerável a ataques.
Outra palavra-chave desta função é onlyMember
que é um modifier
que pode reduzir muito o código duplicado do contrato inteligente. Vamos verificar a função onlyMember
modifier onlyMember {
require(members[msg.sender].shares > 0, "Your are not a member of the PowDAO.: don't have shared.");("Você não é um membro da PowDAO.:não compartilhou")
_;
}
Este modificador apenas protege para permitir que apenas o membro da DAO faça a chamada. Portanto, qualquer função que utilize este modificador também bloqueará os não-membros.
PROCESSAR A PROPOSTA
Esta é a função mais importante neste contrato, que fará com que a proposta seja executada de acordo com a solicitação do usuário. Vamos verificar o código para maior compreensão.
function processProposal(uint256 proposalId) public onlyMember returns (bool) {
require(proposals[proposalId].exists, "This proposal does not exist."); ("Esta proposta não existe")
require(proposals[proposalId].flags[1] == false, "This proposal has already been processed"); ("Essa proposta já foi processada")
require(getCurrentTime() >= proposals[proposalId].startingTime, "voting period has not started"); ("O período de votação não foi iniciado")
require(hasVotingPeriodExpired(proposals[proposalId].startingTime), "proposal voting period has not expired yet"); ("o período de votação ainda não expirou)
require(proposals[proposalId].paymentRequested <= address(this).balance, "DAO balance too low to accept the proposal."); ("Saldo da DAO muito baixo para aceitar proposta")
for(uint256 i=0; i<proposalQueue.length; i++) {
if (proposalQueue[i]==proposalId) {
delete proposalQueue[i];
}
}
Proposal storage prop = proposals[proposalId];
// flags = [sponsored, processed, didPass, cancelled, memberAdd, memberKick]
if(prop.flags[4] == true) { // Adesão do membro
if(prop.yesVotes > prop.noVotes) {
members[prop.proposer] = Member(1, 0, true, 0);
prop.flags[1] = true;
prop.flags[2] = true;
}
else{
prop.flags[1] = true;
prop.flags[3] = true;
}
}
if(prop.flags[5] == true) { // Exclusão do membro
if(prop.yesVotes > prop.noVotes) {
members[prop.proposer].shares = 0;
prop.flags[1] = true;
prop.flags[2] = true;
}
else{
prop.flags[1] = true;
_cancelProposal(proposalId);
}
}
if(prop.flags[4] == false && prop.flags[5] == false) {
if(prop.yesVotes > prop.noVotes) {
prop.flags[1] = true;
prop.flags[2] = true;
_increasePayout(prop.proposer, prop.paymentRequested);
}
else{
prop.flags[1] = true;
_cancelProposal(proposalId);
}
}
emit ProcessedProposal(prop.proposer, prop.paymentRequested, prop.details, prop.flags, proposalId, prop.proposer);
return true;
}
Este código é bem longo, mas se você considerar mais profundamente, é apenas um caso de if-else para cada recurso que pode criar uma proposta para votação. Vamos verificar parte por parte.
require(proposals[proposalId].exists, "This proposal does not exist."); ("Essa proposta já foi processada")
require(proposals[proposalId].flags[1] == false, "This proposal has already been processed"); ("Essa proposta já foi processada")
require(getCurrentTime() >= proposals[proposalId].startingTime, "voting period has not started"); ("o período de votação não foi iniciado")
require(hasVotingPeriodExpired(proposals[proposalId].startingTime), "proposal voting period has not expired yet"); ("o período de votação ainda não expirou")
require(proposals[proposalId].paymentRequested <= address(this).balance, "DAO balance too low to accept the proposal."); ("Saldo da DAO muito baixo para aceitar a proposta")
for(uint256 i=0; i<proposalQueue.length; i++) {
if (proposalQueue[i]==proposalId) {
delete proposalQueue[i];
}
}
Basicamente, esta é a parte da proteção. É muito importante checar a autorização antes de executar alguma ação. Para mais detalhes você pode consultar o código diretamente. É bastante simples.
Proposal storage prop = proposals[proposalId];
// flags = [sponsored, processed, didPass, cancelled, memberAdd, memberKick]
if(prop.flags[4] == true) { // Adesão do membro
if(prop.yesVotes > prop.noVotes) {
members[prop.proposer] = Member(1, 0, true, 0);
prop.flags[1] = true;
prop.flags[2] = true;
}
else{
prop.flags[1] = true;
prop.flags[3] = true;
}
}
Esta é a parte da execução. Basicamente, é apenas obter informações da struct Proposal
e executá-la seguindo o tipo de proposta. Portanto, vou escolher apenas uma ação para apresentar. O código acima é a ação "Add Member" (Adicionar um Membro). Como já falei antes, props.flags
é um tipo de proposta, que indica a ação que esta proposta realizará após ter sido aprovada. Portanto, para isto, basta criar a nova struct Member
e adicioná-la à lista de membros. Para outros casos, você pode verificar por si mesmo, pois será semelhante à condição que eu acabei de apresentar.
Submeter um Voto
Outro processo importante na DAO é o sistema de votação. Vamos verificar o código juntos.
function submitVote(uint256 proposalId, uint8 uintVote) public onlyMember {
require(members[msg.sender].exists, "Você não é um membro da PowDAO.: não existente");
require(proposals[proposalId].exists, "Essa proposta não existe.");
address memberAddress = msg.sender;
Member storage member = members[memberAddress];
Proposal storage prop = proposals[proposalId];
require(uintVote < 3, "must be less than 3"); ("deve ser menor que 3")
Vote vote = Vote(uintVote);
require(getCurrentTime() >= prop.startingTime, "voting period has not started");
require(!hasVotingPeriodExpired(prop.startingTime), "proposal voting period has expired");
require(prop.votesByMember[memberAddress] == Vote.Null, "member has already voted"); ("o membro já votou")
require(vote == Vote.Yes || vote == Vote.No, "vote must be either Yes or No"); ("o voto deve ser Sim ou Não")
prop.votesByMember[memberAddress] = vote;
if (vote == Vote.Yes) {
prop.yesVotes = prop.yesVotes.add(member.shares);
}
else if (vote == Vote.No) {
prop.noVotes = prop.noVotes.add(member.shares);
}
emit SubmitVote(proposalId, msg.sender, memberAddress, uintVote);
}
Como no caso anterior, a primeira parte é a parte de validação autorizada. Então você mesmo pode verificá-la no código, vamos passar para a parte interessante. Você pode se perguntar por que ela tem ambos require
e o tradicional if
e qual é exatamente a diferença entre eles. A principal diferença é que o require
reverterá todo o estado se a condição não passar. Por exemplo,
address memberAddress = msg.sender;
Member storage member = members[memberAddress];
Proposal storage prop = proposals[proposalId];
require(uintVote < 3, "must be less than 3");
Nas 3 primeiras linhas, atribuímos o endereço do remetente ao memberAddress e também atribuímos qualquer outra variável. Se a verificação require
falhar, haverá reversão de todas as atribuições das variáveis acima da linha require
. É diferente do tradicional if
que simplesmente pulará o bloco de código contido em if
e continuará o processo na próxima linha.
Assim, acrescentaremos a importante proteção de verificação require
para assegurar que tudo será revertido se houver falha.
Get Paid
A última funcionalidade mais importante é "Get Paid", para pagar o proponente quando a proposta é aprovada pelo membro da DAO.
// Internal function
function _decreasePayout(address beneficiary, uint256 subtractedValue) internal returns (bool) {
uint256 currentAllowance = _payoutTotals[beneficiary];
require(currentAllowance >= subtractedValue, "ERC20: decreased payout below zero");
uint256 newAllowance = currentAllowance - subtractedValue;
_payoutTotals[beneficiary] = newAllowance;
return true;
}
function _increasePayout(address recipient, uint256 addedValue) internal returns (bool) {
uint256 currentBalance = 0;
if(_payoutTotals[recipient] != 0) {
currentBalance = _payoutTotals[recipient];
}
_payoutTotals[recipient] = addedValue + currentBalance;
return true;
}
// Publicar
function payout(address recipient) public view returns (uint256) {
return _payoutTotals[recipient];
}
// A proposer calls function and if address has an allowance, recieves ETH in return. (O proponente chama a função e se o endereço tiver permissão, receberá ETH em retorno)
function getPayout(address payable addressOfProposer) public returns (bool) {
uint256 allowanceAvailable = _payoutTotals[addressOfProposer]; // Primeiro obter o crédito disponível e armazenar em uint256.
require(allowanceAvailable > 0, "You do not have any funds available."); ("Você não tem fundos disponíveis")
if (allowanceAvailable != 0 && allowanceAvailable > 0) {
addressOfProposer.call{value:allowanceAvailable}(""); // também pode ser: addressOfProposer.transfer(allowanceAvailable)
_decreasePayout(addressOfProposer, allowanceAvailable);
// console.log("transfer success");
emit Withdraw(addressOfProposer, allowanceAvailable);
return true;
}
}
Basicamente, essa função irá verificar a allowanceAvailable
do usuário, que indica o quanto de crédito o usuário pode retirar da DAO. Essa variável aumentará quando a proposta for aprovada e quando chamar processProposal
. Então, o proponente poderá retirar o crédito com a função getPayout
.
Fluxo da DAO
Depois que já entendemos todas as principais funcionalidades da DAO, você pode descobrir o mecanismo de visão geral da DAO. Assim, visualizamos algo como:
Como você pode ver, na verdade não é muito complexo, mas por causa do estilo de código de economia de gás e também da necessidade do desenvolvedor de incluir toda a lógica em um único arquivo, pode parecer complexo à primeira vista.
Parabéns 🎉, Espero que vocês já tenham compreendido o código do contrato DAO. Entretanto, ainda temos algumas funções e recursos que ainda não foram aprovados. Assim, sugiro que analisem tudo, para entender melhor o conceito de DAO.
Na próxima parte, tentaremos implantar a DAO e reproduzir alguma funcionalidade na Moonbeam, para entender melhor os benefícios da Moonbeam para os desenvolvedores.
No entanto, não quero tornar o artigo muito longo, por isso vou dividir a próxima parte no próximo capítulo.
Muito obrigado pelo interesse de vocês. Nos vemos no próximo capítulo 😄.
Esse artigo foi escrito por Drnutsu e traduzido por Fátima Lima. O original pode ser lido aqui.
Latest comments (0)