WEB3DEV

Cover image for Compreendendo a Carteira MULTI-SIG
Fatima Lima
Fatima Lima

Posted on

Compreendendo a Carteira MULTI-SIG

Image description

Foto por Kanchanara no Unsplash

A carteira Multisig (múltiplas assinaturas) exige mais de uma assinatura para autorizar transações, o que acrescenta uma camada extra de segurança aos fundos armazenados. As assinaturas estão associadas a diferentes chaves privadas criptográficas. Qualquer proprietário de uma carteira multisig pode iniciar uma transação assinada com sua chave privada, mas essa transação ficará pendente até que sejam aprovadas/atingidas as confirmações necessárias.

Há vários tipos de carteiras Multisig, mas há dois tipos populares: um exige que todas as partes assinem a transação antes de ela ser confirmada:

N-N, todos os signatários devem ser confirmados antes que a transação seja validada, geralmente dois signatários; onde "N" indica o número de signatários.

M-N, um número predefinido de signatários do pool deve ser atendido para confirmar uma transação, em que "N" representa o número total de signatários e "M" representa o número necessário de assinaturas para validar uma transação.

As carteiras Multisig são diferentes das carteiras tradicionais, pois distribuem o acesso por várias chaves para evitar a perda fácil de fundos. As carteiras tradicionais também são conhecidas como Contas de Propriedade Externa, são menos seguras e são controladas por chaves privadas geradas pelos proprietários e, em combinação com um endereço público, são usadas para se comunicar com a blockchain. Diferentemente das carteiras Multisig, as carteiras tradicionais ou carteiras de chave única são boas para transações menores e mais rápidas; as carteiras Mulitsig, por outro lado, são boas para o controle conjunto de fundos em contas divididas, facilitam a transparência em organizações descentralizadas (DAOs - Organizações Autônomas Descentralizadas) e reforçam a segurança para usuários com uma quantidade considerável de fundos.

A estrutura de design das carteiras Multisig reduz o risco de comprometimento ao distribuir a responsabilidade de assinatura, eliminando assim um único ponto de falha ou o risco de pessoa-chave normalmente associado a Contas de Propriedade Externa. O risco de pessoa-chave refere-se a quando uma empresa depende quase que inteiramente de um único indivíduo para ter sucesso. Esse risco é muito comum nas criptomoedas, principalmente nos casos em que um indivíduo tem o controle da frase-semente de uma carteira. Muitas blockchains integram funcionalidades que permitem aos usuários implementar carteiras com várias assinaturas. As bolsas de criptomoedas também implementam carteiras Multisig e armazenam chaves privadas associadas em diversos locais para proteger os ativos dos clientes.

Benefícios da Multisig

Maior segurança e transparência, pois as chaves são distribuídas em vários locais e dispositivos, reduzindo a dependência de uma única parte. Não há risco de pessoa-chave. Serve como autenticação de dois fatores.

Como funciona?

As carteiras Multisig exigem duas ou mais assinaturas para que uma transação seja validada. Durante a configuração, os signatários definem as regras de acesso, inclusive o número mínimo necessário de chaves para a execução de uma tarefa na configuração N-M ou, se todas as chaves forem necessárias para a validação, N-N. As carteiras Multisig usam contratos inteligentes para governança on-chain.

Os signatários geram os pares privado-público usando um algoritmo criptográfico; a combinação de ambas as chaves cria um endereço Multisig associado à carteira.

Em seguida, os usuários definem os requisitos de confirmação, que podem ser N-N ou N-M, conforme explicado acima.

Quando uma parte inicia uma transação, ela permanece pendente até que os requisitos de signatário sejam atendidos e, em seguida, a transação é enviada à rede para verificação, após o que é confirmada.

Vamos escrever um contrato minimalista para uma carteira multi-sig.

Contrato da Carteira Multisig

Definimos um array de endereços de proprietários para armazenar todos os signatários para a carteira.

Address [] public owners
Enter fullscreen mode Exit fullscreen mode

Em seguida, definimos um mapeamento dos proprietários para verificar se um endereço inválido está tentando enviar uma transação

Mapping (address=>bool) public isOwners;
Uint public required
Enter fullscreen mode Exit fullscreen mode

A variável required mantém o rastreamento do número de assinaturas predefinidas necessárias para validar uma transação; isso será configurado no constructor.

Definimos uma struct que contém os detalhes da transação proposta, que chamamos de Transaction (transação), com 4 valores: address to, unit value, bytes data, bool executed:

 struct Transactions {
       address to;
       uint value;
       bytes data;
       bool executed;
   }
Enter fullscreen mode Exit fullscreen mode

Address to: é o endereço do destinatário.

Uint value: o valor a ser enviado.

Bytes data: são os dados da transação.

Bool executed: rastreia se a transação foi executada ou não.

Em seguida, definimos uma matriz da struct de transações para armazenar todas as transações.

Transactions [] public transaction
Enter fullscreen mode Exit fullscreen mode

Em seguida, criamos um mapeamento do índice de cada transação para seu endereço e para um bool de sua aprovação

mapping (uint => mapping(address=>bool)) public approved;
Enter fullscreen mode Exit fullscreen mode

Em seguida, definimos nosso constructor, que recebe um array de owners e um uint require. Primeiro, verificamos se há algum _owner no array e, em seguida, verificamos se _required é maior que zero e não é maior que o número de proprietários. Em seguida, colocamos todos os _owners informados na variável de estado _owners, executando um loop for; primeiro, verificamos se os endereços não são iguais ao endereço zero; depois, verificamos se os proprietários ainda não estão no array owners; em seguida, colocamos os proprietários no array owners e atualizamos o mapeamento de isOwners. Por fim, definimos a entrada necessária para a variável de estado requirerequired=_required; e é isso, terminamos o constructor.

constructor(address[] memory _owners, uint _required) {
       if (_owners.length <= 0) {
           revert MultsigWallet_OwnersRequired();
       }

       require(
           _required > 0 && _required <= _owners.length,
           "Invalid required number of owners"
       );
       //colocamos todos os proprietários dentro da variável de estado owners
       for (uint i; i < _owners.length; i++) {
           address owner = _owners[i];
           require(owner != address(0), "invalid owner");
           require(!isOwners[owner], "Owner isnt unique");

           //inserimos os novos proprietários dentro do mapeamento de proprietários e do array de proprietários
           isOwners[owner] = true;
           owners.push(owner);
       }
       //definir o required para o required da entrada.
       required = _required;
   }
Enter fullscreen mode Exit fullscreen mode

Agora, habilitamos esse contrato para receber dinheiro e, em seguida, emitimos um evento que leva em consideração o remetente e o valor recebido.

//configuramos a carteira para poder receber ether
   receive() external payable {
       emit Deposit(msg.sender, msg.value);
   }

   event Deposit(address indexed sender, uint amount);
Enter fullscreen mode Exit fullscreen mode

Temos cinco funções, quatro externas e uma interna, mas primeiro vamos declarar nosso modificador. Temos quatro modificadores e todos eles são autoexplicativos. Garantimos que onlyOwner (apenas proprietário) possa aprovar uma transação; garantimos que a transação exista, txExist; garantimos que ela ainda não tenha sido aprovada por esse endereço, notApproved; e que ainda não tenha sido executada, notExecuted. Em seguida, acessamos o mapeamento approved e o definimos como verdadeiro.

modifier onlyOwner() {
       require(isOwners[msg.sender], "Not owner");
       _;
   }
   modifier txExists(uint _txId) {
       // verificamos se o txId é menor que o comprimento
       require(_txId < transactions.length, "invalid transactions ID");
       _;
   }

   modifier notApproved(uint _txId) {
       require(!approved[_txId][msg.sender], "not approved");
       _;
   }
   modifier notExecuted(uint _txId) {
       require(!transactions[_txId].executed, "tx  already executed");
       _;
   }
Enter fullscreen mode Exit fullscreen mode

A função Submit recebe a mesma entrada das structs de transação (_to, _value, _data), mas adicionamos o modificador onlyOwner para garantir que somente os signatários possam enviar transações. Em seguida, enviamos os valores de entrada para o array de transações. Em seguida, emitimos um evento com o índice da transação. Obtemos o índice deduzindo 1 do comprimento da matriz de transações. O onlyOwner garante que somente os signatários da carteira possam enviar uma transação.

function submit(
       address _to,
       uint _value,
       bytes calldata _data
   ) external onlyOwner {
       // Enviamos as transações submetidas para a struct Transaction
       transactions.push(
           Transactions({to: _to, value: _value, data: _data, executed: false})
       );
       // As primeiras transações são armazenadas como índice 0, depois como índice 1 e assim por diante
       emit Submit(transactions.length - 1);
   }

   event Submit(uint indexed txId);
Enter fullscreen mode Exit fullscreen mode

Em seguida, temos a função approve, que recebe o txId como único parâmetro e aprova a transação do ID da transação do owner(msg.sender), emitindo um evento.

function approve(
       uint _txId
   ) external onlyOwner txExists(_txId) notApproved(_txId) notExecuted(_txId) {
       approved[_txId][msg.sender] = true;
       emit Approve(msg.sender, _txId);
   }
   event Approve(address indexed owner, uint indexed txId);
Enter fullscreen mode Exit fullscreen mode

Em seguida, precisamos obter _ approvalCount; essa é uma função privada que recebe _ txId como único parâmetro e retorna uma variável count do tipo uint; percorremos os proprietários para obter os endereços de cada um deles e, em seguida, acessamos os endereços aprovados e os adicionamos à variável count. Ps: declaramos a count na declaração de função para economizar gas.

 function _getApprovalCount(uint _txId) private view returns (uint count) {
       for (uint i; i < owners.length; i++) {
           if (approved[_txId][owners[i]]) {
               count += 1;
           }
       }
   }
Enter fullscreen mode Exit fullscreen mode

Em seguida, temos a função execute, que, assim como as outras, só pode ser chamada pelos proprietários. Essa função aguarda que a count de aprovações seja igual à exigida, cria uma instância da transação, verifica se a execução na transação é verdadeira e envia os fundos para o endereço to. Em seguida, emitimos um evento _Execute com o txId. Ps: declaramos a transação como armazenamento porque iremos atualizar o array de transações.

function execute(uint _txId) external txExists(_txId) notExecuted(_txId) {
       require(
           _getApprovalCount(_txId) >= required,
           "appproval count is less than required"
       );
       Transactions storage transaction = transactions[_txId];
       transaction.executed = true;
       (bool success, ) = transaction.to.call{value: transaction.value}(
           transaction.data
       );
       require(success, "transaction failed");
       emit Execute(_txId);
   }
Enter fullscreen mode Exit fullscreen mode

Por último, para o nosso contrato, declaramos a função revoke, apenas para o caso de um usuário decidir mudar de ideia. A função recebe o (_ txId), verifica se onlyOwner pode executar essa função, o txExists e o notApproved e, em seguida, emite um evento com o Approved com o msg.sender e o txId.

function revoke(
       uint _txId
   ) external onlyOwner txExists(_txId) notExecuted(_txId) {
       require(approved[_txId][msg.sender], "tx not apporved");
       approved[_txId][msg.sender] = false;
       emit Revoke(msg.sender, _txId);
   }
Enter fullscreen mode Exit fullscreen mode

Obrigado pela leitura. Para obter o código completo, consulte meu Github. cc Solidity como exemplo.

Esse artigo foi escrito por Abusomwan Santos e traduzido por Fátima Lima. O original pode ser lido aqui.

Latest comments (0)