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
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
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;
}
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
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;
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;
}
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);
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");
_;
}
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);
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);
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;
}
}
}
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);
}
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);
}
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)