MultiSig é uma abreviação para Multi-Signature ou Multi-Assinatura, um requisito obrigatório e um recurso poderoso em alguns casos de uso de smart contracts. Com MultiSig, nós dizemos que algo só acontecerá ou passará a valer quando duas ou mais partes assinarem uma transação. Falamos um pouco disso no bate-papo a seguir que substitui essa introdução teórica.

Vamos a alguns exemplos práticos.
Imagine por exemplo uma empresa com tesouraria em criptomoedas, que movimenta milhões ou até bilhões em criptoativos, como a MicroStrategy. Se essas transações de altíssimo valor dependerem de apenas uma assinatura para serem realizadas, o risco de fraude ou mesmo de perda dessa única carteira poderia arruinar completamente a empresa, certo? Mas e se ao todos existissem três carteiras detendo controle da conta, mas para que as transações fossem concretizadas, ao menos duas delas teriam de assinar concordando? Já ficaria muito mais seguro, tanto contra fraudes quanto contra o risco de perda de uma carteira.
Imagine outro exemplo, de um contrato de compra e venda de um ativo do mundo real (RWA). O contrato prevê alguns direitos e deveres a ambas as partes, como a transferência do RWA do vendendor para o comprador, bem como a transferência do pagamento do comprador para o vendedor, entre outras cláusulas que podem estar presentes como quitação de dívidas, comissões a corretores, etc. Um contrato desses só pode valer quanto ambas as partes assinarem o mesmo, certo? Não basta uma assinatura, tem de ser feitas uma por parte envolvida. Esse é outro exemplo de MultiAssinatura.
Mas voltando ao primeiro exemplo, de uma conta empresarial que possui múltiplos administradores. Esse caso de uso é muito popular no mercado e costuma se chamar de Carteira MultiSig ou Smart Wallet, pois na prática é um smart contract que replica algumas funcionalidades de carteira cripto, mas com a vantagem de permitir a configuração de n carteiras administradoras que podem ser usadas para assinar as transferências de fundos, dando mais resiliência e segurança para a conta em questão.
Mas se carteiras multisigs são tão boas assim, porque todo mundo não usa elas?
Apesar das vantagens das carteiras multisig também existem desvantagens. Além de possíveis bugs no smart contract, um revés que não tem como fugir é o de aumento do custo nas transações (gás). Como você verá no exemplo a seguir, uma transferência feita por carteira multisig sempre irá consumir mais taxas de transação do que uma carteira comum, mesmo havendo apenas um assinante, pois são feitos processamentos adicionais em várias etapas.
Assim, se o seu caso de uso se beneficia muito de multisig e o custo adicional não for proibitivo, aí sim vale a pena.
Mostrarei a seguir um exemplo de smart wallet implementada com Solidity, a linguagem de programação mais popular para smart contracts e usarei para os testes a IDE Remix e a carteira MetaMask (que ensino a configurar neste tutorial).
Se preferir, você pode acompanhar a prática no vídeo abaixo, o conteúdo é o mesmo.

Vamos lá!

#1 – Escopo Administrativo
Vamos começar nosso desenvolvimento estruturando inicialmente o contrato, com as diretivas padrões e o escopo geral.
|
1 2 3 4 5 6 7 8 9 |
// SPDX-License-Identifier: MIT pragma solidity 0.8.30; contract MultiSigWallet { } |
Aqui eu defini que vamos usar Solidity 0.8.30 e que o nosso contrato vai se chamar MultiSigWallet. Vamos quebrar o desenvolvimento do nosso contrato em duas etapas: uma administrativa e outra financeira. O escopo administrativo compreende a gestão de donos desta smart wallet, vamos começar ele a partir de algumas variáveis de estado dentro do escopo do contrato, como abaixo.
|
1 2 3 4 |
mapping(address => bool) public owners; uint public ownersQty; |
O mapping de owners servirá para registrar os donos da smart wallet, identificados por suas carteiras e um booleano indicando se são ou não donos. Já o ownersQty vai servir para mantermos registro de quantos owners temos atualmente, sendo que neste exemplo fictício que vou limitar a quantidade máxima de owners em 3 e mínima em 2, já que não faz sentido ter uma carteira multisig com apenas um owner.
A próxima etapa é criar o construtor do contrato, onde já vamos adicionar quem fez o deploy como primeiro owner do mesmo e incrementamos a quantidade de owners.
|
1 2 3 4 5 6 |
constructor() { owners[msg.sender] = true; ownersQty++; } |
A seguir, vamos criar o function modifier que será útil nas demais funções, para garantir que somente owners consigam usar nossa smart wallet.
|
1 2 3 4 5 6 |
modifier onlyOwner() { require(owners[msg.sender] == true, "Not an owner"); _; } |
O primeiro local onde vamos usar esse function modifier é na função que adiciona novos owners, abaixo. Pode ser um novo sócio que entrou na empresa ou uma nova carteira de um owner antigo.
|
1 2 3 4 5 6 7 8 |
function addOwner(address newOwner) public onlyOwner { require(ownersQty < 3, "You cannot have more than 3 owners"); require(!owners[newOwner], "This address already is an owner"); owners[newOwner] = true; ownersQty++; } |
Aqui começamos validando a quantidade de owners, conforme já citado antes, bem como validamos que a carteira já não é owner do contrato (para evitar distorções na contagem de owners). Depois fazemos a atribuição do novo owner no mapping e incrementamos a contagem.
Na sequência, vamos fazer a função que remove um owner antigo. Pode ser alguém que não é mais sócio da empresa ou uma carteira que tenha sido perdida, por exemplo.
|
1 2 3 4 5 6 7 8 |
function removeOwner(address oldOwner) public onlyOwner { require(ownersQty > 2, "You cannot have less than 2 owners"); require(owners[oldOwner], "This address is not an owner"); owners[oldOwner] = false; ownersQty--; } |
Aqui validamos para que a carteira não fique com poucos owners, garantimos que o owner a ser removido exista e por fim fazemos a desatribuição no mapping e o decremento na variável de controle.
Com isso temos todas as funções necessárias para a gestão e controle dos owners. Recomendo que teste no Remix antes de avançar pois agora é a etapa financeira.
#2 – Escopo Financeiro
Agora que já temos todas as ferramentas para fazer a gestão dos owners da smart wallet, é hora de implementarmos as funcionalidades ligadas às transferências em si. Vamos começar pela struct de uma transferência, pois ela será a base para tudo que faremos depois.
|
1 2 3 4 5 6 7 8 9 10 11 |
struct Transfer { address author; address to; uint amount; address closedBy; bool sent; uint startTimestamp; uint endTimestamp; } |
Os campos de toda transferência são:
- author: quem criou a transferência (um dos owners da carteira);
- to: para quem a transferência será enviada;
- amount: a quantidade de moeda a ser transferida (em wei)
- closedBy: quem concluiu a transferência (outro owner da carteira);
- sent: se a transferência foi realizada ou não (ela pode ser cancelada);
- startTimestamp: quando ela foi aberta;
- endTimestamp: quando ela foi fechada;
Com essa struct pronta, podemos criar as variáveis de estado necessárias para registrar tudo corretamente.
|
1 2 3 4 5 |
uint public nextTransferId = 1; mapping(uint => Transfer) public transfers; uint public lockedBalance = 0; |
Nossas transferências serão armazenadas em um mapping de transferências, onde o identificador único entre cada uma delas será gerado incrementalmente por outra variável de controle. Por fim, adicionei uma lockedBalance pois entre a criação de uma transferência e a sua execução precisamos ter um mecanismo de bloqueio de fundos na carteira. Ou seja, disse que ia transferir 100 wei? Tenho de deixar registrado um bloqueio de 100 wei até que a transferência aconteça de fato.
O próximo passo é criar os eventos que vamos emitir. É muito importante que um contrato como esse tenha eventos bem construídos para que as partes possam monitorar o progresso das assinaturas e transações, algo útil para construção de dapps mais tarde.
|
1 2 3 4 5 6 |
event TransferReceived(address indexed from, uint amount); event TransferStart(uint indexed transferId, address indexed author, address indexed to, uint amount); event TransferApprove(uint indexed transferId, address indexed approvedBy, address indexed to, uint amount); event TransferDenied(uint indexed transferId, address indexed deniedBy, address indexed to); |
Aqui eu criei quatro eventos que emitiremos mais tarde, a saber:
- TransferReceived: quando a smart wallet recebe fundos;
- TransferStart: quando um owner cadastra uma nova transferência;
- TransferApprove: quando um owner autoriza uma transferência existente;
- TransferDenial: quando um owner cancela uma transferência existente;
E já que falamos de evento de receber fundos, para que nosso contrato possa receber fundos nativos (não usarei tokens ERC20 neste exemplo, mas seria possível) precisamos implementar uma função receive, como abaixo.
|
1 2 3 4 5 |
receive() external payable { emit TransferReceived(msg.sender, msg.value); } |
Quando for fazer os testes de recebimento no Remix, caso nunca tenha feito isso antes, basta preencher primeiro a quantia a ser transferida no campo value.

E depois usar o botão Transact que fica bem ao final, junto ao Low level interactions, mantendo CALLDATA vazio.

Com o recebimento implementado, hora de criarmos a função que inicia uma nova transferência.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
function startTransfer(address payable to, uint amount) public onlyOwner { require(amount <= address(this).balance - lockedBalance, "Insufficient balance"); Transfer memory transfer = Transfer({ author: msg.sender, to: to, amount: amount, closedBy: address(0), sent: false, startTimestamp: block.timestamp, endTimestamp: 0 }); transfers[nextTransferId] = transfer; lockedBalance += amount; emit TransferStart(nextTransferId, msg.sender, to, amount); nextTransferId++; } |
Aqui recebemos quem vai receber os fundos (to) e a quantidade (amount), sendo obrigatório ser um owner para conseguir chamar esta função. Fazemos uma validação de fundos e construímos o objeto de transferência na memória primeiro, usando o msg.sender como author e o block.timestamp como instante atual. Esse objeto é salvo no mapping, o saldo é bloqueado na variável de controle, o evento é emitido e o id autoincremental é…incrementado, para que a próxima transferência tenha outro id.
Para finalizar, precisamos agora da função que finaliza uma transferência, seja aprovando ela ou não.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
function endTransfer(uint transferId, bool send) public onlyOwner { Transfer memory transfer = transfers[transferId]; require(transfer.closedBy == address(0), "Already closed"); if (send) { require(transfer.author != msg.sender, "Cannot approve himself"); require(transfer.amount <= address(this).balance,"Insufficient balance"); } transfer.closedBy = msg.sender; transfer.endTimestamp = block.timestamp; transfer.sent = send; transfers[transferId] = transfer; lockedBalance -= transfer.amount; if (send){ emit TransferApprove(transferId, msg.sender, transfer.to, transfer.amount); payable(transfer.to).transfer(transfer.amount); } else emit TransferDenied(transferId, msg.sender, transfer.to); } |
Aqui um owner precisa informar o id da transferência e se deseja que ela envie os fundos ou não (send). Começamos resgatando o objeto de transferência do mapping e em cima dele fazemos algumas validações como se ela já não foi finalizada e se o author e amount estão corretos (no caso de querer que a transferência seja feita).
Estando tudo certo com as validações, o msg.sender é assumido como sendo o responsável pela conclusão, o block.timestamp registra o instante do fechamento e o send indica se a grana foi enviada ou não. Após o salvamento das atualizações no mapping, é hora de desbloquear os fundos, emitir o evento adequado e de fato transferir a grana pro destinatário.
Agora é a hora de testar novamente e se você fez tudo corretamente como mostrei acima, agora tem um bom exemplo de smart wallet para se basear nos seus projetos envolvendo multisig.
Até a próxima!

Olá, tudo bem?
O que você achou deste conteúdo? Conte nos comentários.
