WEB3DEV

Cover image for Construa sua primeira DAO e implante-a na rede Moonbeam - PARTE 2: Contrato Inteligente
Fatima Lima
Fatima Lima

Posted on • Atualizado em

Construa sua primeira DAO e implante-a na rede Moonbeam - PARTE 2: Contrato Inteligente

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
Enter fullscreen mode Exit fullscreen mode

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;
       }
   }
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
   }
Enter fullscreen mode Exit fullscreen mode

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") 
       _;
   }
Enter fullscreen mode Exit fullscreen mode

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;
   }
Enter fullscreen mode Exit fullscreen mode

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];
      }
   }
Enter fullscreen mode Exit fullscreen mode

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;
           }
       }
Enter fullscreen mode Exit fullscreen mode

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);
   }
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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;
       }
   }
Enter fullscreen mode Exit fullscreen mode

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:

Image description

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.

Oldest comments (0)