WEB3DEV

Cover image for As Vulnerabilidades Mais Comuns em Solidity
Diogo Jorge
Diogo Jorge

Posted on

As Vulnerabilidades Mais Comuns em Solidity

Esse artigo é uma tradução de (Metabay Security) feita por (Diogo Jorge). Você pode encontrar o artigo original aqui. (https://medium.com/@Metabay/most-common-solidity-vulnerabilities-d708705bb9bf).

Bem Vindo ao blog da Metabay. Visite nossas redes sociais para mais informação:

Website: https://metabay.network

Telegram: https://t.me/metabay_network

Twitter: https://twitter.com/MetabayS

Reddit: https://www.reddit.com/r/Metabay/

Medium: https://medium.com/@Metabay

A seguir listamos ataques conhecidos que você deve estar ciente e se defender ao escrever smart contracts.

Reentrância

Um dos maiores perigos de chamar contratos externos é que eles podem dominar o fluxo de controle, e fazer mudanças em seus dados que a função chamada não estava esperando. Este tipo de bug pode ter várias formas, e os dois principais bugs que levaram ao colapso da DAO eram desse tipo.

Reentrância em uma Simples Função

A primeira versão desse bug a ser notada envolvia funções que podiam ser chamadas repetidamente, antes que a primeira invocação da função terminasse. Dessa forma causando diferentes invocações da função para interagir de formas destrutivas.

// INSECURE
mapping (address => uint) private userBalances;
function withdrawBalance() public {
   uint amountToWithdraw = userBalances[msg.sender];
   (bool success, ) = msg.sender.call.value(amountToWithdraw)(""); // At this point, the caller's code is executed, and can call withdrawBalance again
   require(success);
   userBalances[msg.sender] = 0;
}
Enter fullscreen mode Exit fullscreen mode

Uma vez que o saldo do usuário não é definido em 0 até o final da função, a segunda invocação (e posteriores) ainda vão acontecer, e vão sacar o saldo repetidamente.

!!! Factóide: Uma DAO é uma Organização Autônoma Descentralizada. Seu objetivo é codificar as regras e o aparato de tomada de decisão de uma organização eliminando a necessidade de documentos e pessoas governando, criando uma estrutura com controle descentralizado.

Em 17 de junho de 2016, [The DAO] (https://www.coindesk.com/understanding-da-hack-journalists) foi hackeada e 3.6 milhões de Ether (US$50 milhões) foram roubados usando o primeiro ataque de reentrância.
A Fundação Ethereum realizou uma atualização crítica para reverter o hack. Isto resultou no Ethereum tendo um fork para Ethereum Classic e Ethereum. 
Enter fullscreen mode Exit fullscreen mode

No exemplo dado, a melhor forma de prevenir este ataque é ter certeza de não chamar uma função externa até que você tenha terminado todo o trabalho interno que você tenha que fazer.

Note que se você tiver outra função que chamava withdrawBalance(), ela poderia potencialmente ser submetida ao mesmo ataque, então você tem que tratar qualquer função que chama um contrato não confiável como não confiável ela mesma. Veja abaixo uma discussão aprofundada sobre potenciais soluções.

Reentrância de função-cruzada

Um atacante pode também fazer um ataque similar usando duas funções diferentes que compartilham o mesmo estado.

// INSECURE
mapping (address => uint) private userBalances;
function transfer(address to, uint amount) {
   if (userBalances[msg.sender] >= amount) {
      userBalances[to] += amount;
      userBalances[msg.sender] -= amount;
   }
}
function withdrawBalance() public {
   uint amountToWithdraw = userBalances[msg.sender];
   (bool success, ) = msg.sender.call.value(amountToWithdraw)(""); // At this point, the caller's code is executed, and can call transfer()
   require(success);
   userBalances[msg.sender] = 0;
}
Enter fullscreen mode Exit fullscreen mode

Neste caso o atacante chama transfer() quando seu código é executado na chamada externa em withdrawBalance. Uma vez que o saldo delas ainda não foi definido para 0 elas podem transferir os tokens mesmo que elas já tenham recebido o saque. Esta vulnerabilidade também foi usada no ataque à DAO.

As mesmas soluções vão funcionar, com as mesmas advertências. Note também que, neste exemplo, ambas funções eram parte do mesmo contrato. Entretanto, o mesmo bug pode ocorrer sobre múltiplos contratos, se estes contratos compartilham o estado.

Armadilhas em Soluções de Reentrância

Uma vez que a reentrância pode ocorrer através de múltiplas funções, e até múltiplos contratos, qualquer solução para prevenir reentrância usando uma única função não vai ser suficiente.

Ao invés disso, nós temos recomendado terminar todo o trabalho interno (ie. mudanças de estado) primeiro, e só então chamar a função externa. Esta regra, se seguida cuidadosamente, vai lhe permitir evitar vulnerabilidades de reentrância. Entretanto, você precisa evitar não apenas chamar funções externas cedo demais, mas também chamar funções que chamam funções externas. No exemplo a seguir vemos a insegurança:

// INSECURE
mapping (address => uint) private userBalances;
mapping (address => bool) private claimedBonus;
mapping (address => uint) private rewardsForA;
function withdrawReward(address recipient) public {
   uint amountToWithdraw = rewardsForA[recipient];
   rewardsForA[recipient] = 0;
   (bool success, ) = recipient.call.value(amountToWithdraw)("");
   require(success);
}
function getFirstWithdrawalBonus(address recipient) public {
   require(!claimedBonus[recipient]); // Each recipient should only be able to claim the bonus once
   rewardsForA[recipient] += 100;
   withdrawReward(recipient); // At this point, the caller will be able to execute getFirstWithdrawalBonus again.
   claimedBonus[recipient] = true;
}
Enter fullscreen mode Exit fullscreen mode

Mesmo que getFirstWithdrawalBonus() não chama diretamente um contrato externo, a chamada em withdrawReward() é suficiente para torná-lo vulnerável à uma reentrância. Você deve então tratarwithdrawReward() como se também fosse não confiável.

mapping (address => uint) private userBalances;
mapping (address => bool) private claimedBonus;
mapping (address => uint) private rewardsForA;
function untrustedWithdrawReward(address recipient) public {
   uint amountToWithdraw = rewardsForA[recipient];
   rewardsForA[recipient] = 0;
   (bool success, ) = recipient.call.value(amountToWithdraw)("");
   require(success);
}
function untrustedGetFirstWithdrawalBonus(address recipient) public {
   require(!claimedBonus[recipient]); // Each recipient should only be able to claim the bonus once
   claimedBonus[recipient] = true;
   rewardsForA[recipient] += 100;
   untrustedWithdrawReward(recipient); // claimedBonus has been set to true, so reentry is impossible
}
Enter fullscreen mode Exit fullscreen mode

Em adição ao conserto para fazer reentrância impossível, funções não confiáveis foram marcadas. Este mesmo padrão repete em todo nível: Uma vez que untrustedGetFirstWithdrawalBonus() chama untrustedWithdrawReward(), o qual chama um contrato externo, você também deve tratar untrustedGetFirstWithdrawalBonus() como inseguro.

Outra solução sugerida é um mutex. Ele permite a você “trancar” algum estado, de forma que ele só pode ser alterado pelo dono da tranca. Um exemplo simples parece com isso:

// Note: This is a rudimentary example, and mutexes are particularly useful where there is substantial logic and/or shared state
mapping (address => uint) private balances;
bool private lockBalances;
function deposit() payable public returns (bool) {
   require(!lockBalances);
   lockBalances = true;
   balances[msg.sender] += msg.value;
   lockBalances = false;
   return true;
}
function withdraw(uint amount) payable public returns (bool) {
   require(!lockBalances && amount > 0 && balances[msg.sender] >= amount);
   lockBalances = true;
   (bool success, ) = msg.sender.call(amount)("");
   if (success) { // Normally insecure, but the mutex saves it
     balances[msg.sender] -= amount;
   }
   lockBalances = false;
   return true;
}
Enter fullscreen mode Exit fullscreen mode

Se o usuário tentar chamar withdraw() novamente antes da primeira chamada terminar, a tranca vai prevenir que ela tenha algum efeito. Isto pode ser um padrão efetivo, mas fica complicado quando você tem múltiplos contratos que precisam cooperar. O exemplo seguinte é inseguro:

// INSECURE
contract StateHolder {
   uint private n;
   address private lockHolder;
   function getLock() {
       require(lockHolder == address(0));
       lockHolder = msg.sender;
   }
   function releaseLock() {
       require(msg.sender == lockHolder);
       lockHolder = address(0);
   }
   function set(uint newState) {
       require(msg.sender == lockHolder);
       n = newState;
   }
}
Enter fullscreen mode Exit fullscreen mode

Um atacante chama getLock() e então nunca chamareleaseLock(). Se eles fizerem isso então o contrato ficará trancado para sempre e nenhuma mudança posterior poderá ser feita. Se você usa mutexes para se proteger contra reentrâncias, você terá que cuidadosamente garantir que não haja meios de uma tranca ser requisitada e nunca liberada. (Há outros perigos potenciais ao programar com mutexes, como deadlocks e livelocks. Você deve consultar a abundante literatura já escrita sobre mutexes se você decidir tomar esse caminho.)

Veja SWC-107

Acima tivemos exemplos de reentrância envolvendo um atacante executando códigos maliciosos dentro de uma única transação. Os seguintes são tipos diferentes de ataque inerente às Blockchains: O fato que a ordem das transações (e.g. dentro de um bloco) são facilmente suscetíveis a manipulação.

Manipulação de Oráculo

Protocolos que dependem de dados externos como inputs (do que são conhecidos como oráculo) executam automaticamente mesmo que os dados estejam incorretos, devido à natureza dos smart contracts. Se o protocolo depende de um oráculo que é hackeado, deprecado, ou tem intenção maliciosa, todos os processos que dependem do oráculo podem agora operar com efeitos desastrosos.

Por exemplo:

  1. O protocolo recebe preços de uma única pool da Uniswap

  2. Atores maliciosos drenam um lado da pool com uma grande transação

  3. A pool da Uniswap começa respondendo com um preço maior que 100x o que deveria ser.

  4. O protocolo opera com se fosse o preço atual, dando ao manipulador um preço melhor

Nós temos visto exemplos onde isto liquidou posições, permitiu arbitragens insanas, arruinou posições em DEX e mais.

Soluções para Manipulações de Oráculo

A maneira mais fácil de resolver isto é usar oráculos descentralizados. A Chainlink é o provedor de oráculo descentralizado líder, e a rede da Chainlink pode ser alavancada para trazer dados descentralizados on-chain.

Outra solução comum é usar um feed de preço médio temporizado, de forma que o peço é uma média sobre x períodos de tempo. Isso não apenas previne a manipulação do oráculo, isso reduz a chance de front-run (largarem na sua frente), e uma ordem executada exatamente antes da sua não vai ter um impacto dramático no preço. Uma ferramenta que agrupa feeds de preço da Uniswap a cada 30 minutos é Keep3r. Se você busca construir uma solução customizada, a Uniswap oferece uma janela de exemplo.

Front-running

Uma vez que todas as transações são visíveis na mempool por um curto período antes de serem executadas, observadores da rede podem ver e reagir a uma ação antes de ser incluída em um bloco. Um exemplo de como isso pode ser explorado é com uma exchange descentralizada onde uma transação de ordem de compra pode ser vista, e a segunda ordem pode ser divulgada e executada antes da primeira ordem ser incluída. Se proteger contra isso é difícil, porque isso pode ser relacionado ao contrato específico.

Largar na frente, termo cunhado originalmente para mercados financeiros tradicionais, é a corrida para organizar o caos para benefício dos vencedores. Em mercados financeiros o fluxo de informação faz nascer intermediários que podem simplesmente lucrar por serem os primeiros a saber e reagir à alguma informação. Estes ataques em sua maioria tem sido dentro dos negócios do mercado de ações e registros inciais de domínios, como os portais de whois.

!!! cite “front-run·ning (/ˌfrəntˈrəniNG/)”

*noun*: front-running;
1. *STOCK MARKET*
   > the practice by market makers of dealing on advance information provided by their brokers and investment analysts, before their clients have been given the information.
   <!-- [[OXFORD](https://www.lexico.com/en/definition/front-running)] -->
Enter fullscreen mode Exit fullscreen mode

Taxonomia

Ao definir uma taxonomia e diferenciar um grupo do outro, nós podemos facilitar a discussão do problema e encontrar soluções para cada grupo.

Nós definimos as seguintes categorias de ataques front-running:

  1. Deslocamento

  2. Inserção

  3. Supressão

Deslocamento

No primeiro tipo de ataque, um displacement attack, não é importante para a função da Alice (usuária) chamar para executar após Mallory (Adversária) executar a função dela. A função da Alice pode ficar órfã ou rodar sem nenhum efeito significativo. Exemplos de deslocamento incluem:

  • Alice tentando registrar um nome de domínio e Mallory registrando ele primeiro;

  • Alice tentando submeter um bug para receber uma bonificação e Mallory roubando e submetendo ele primeiro;

  • Alice tentando submeter uma oferta em um leilão e a Mallory copiando.

Este ataque geralmente ocorre aumentando o gasPrice mais do que a média da rede, geralmente por um múltiplo de 10 ou mais.

Inserção

Para este tipo de ataque, é importante para a adversária que a função chamada original rode depois da transação dela. Em um ataque de inserção, depois que a Mallory roda a função dela, o estado do contrato é trocado e ela necessita que a função original da Alice rode neste estado modificado. Por exemplo, se a Alice coloca uma ordem de compra em um ativo de blockchain a um preço maior do que a melhor oferta, a Mallory vai inserir duas transações: ela vai comprar na melhor oferta de compra e então oferecer o mesmo ativo à venda no preço de compra um pouco mais alto da Alice. Se a transação da Alice é, então, rodada depois, a Mallory vai lucrar na diferença de preço sem ter que possuir o ativo.

Assim como ataques de deslocamento, isto é feito geralmente dando uma oferta melhor que a da transação da Alice no leilão de preço de gas.

!!! info “Dependencia da Ordem de Transação” Dependencia da Ordem de Transação é equivalente a uma condição de corrida em smart contracts. Por exemplo, se uma função define a porcentagem de recompensa, e a função de saque usa esta porcentagem; então a transação de saque pode ser ultrapassada (front-run) por uma mudança na recompensa da função chamada, a qual impacta na quantia que vai ser sacada eventualmente.

See [SWC-114](https://swcregistry.io/docs/SWC-114)
Enter fullscreen mode Exit fullscreen mode

Supressão

Em um ataque de supressão, também conhecido como ataques de Block Stuffing (enchimento de blocos), depois da Mallory rodar a função dela, ela tenta atrasar a Alice em rodar sua função.

Este foi o caso com o primeiro vencedor do jogo “Fomo3d”, e outros hacks on-chain. O atacante enviou múltiplas transações com um alto gasPrice e gasLimit para customizar smart contracts que afirmam (ou usam outros meios) para consumir todo o gas e encher o gasLimit do bloco.

!!!Nota “Variantes” Cada um desses ataques tem duas variantes, asimétricas e a granel (bulk).

Em alguns casos, Alice e Mallory estão realizando diferentes operações. Por exemplo, Alice está tentando cancelar uma oferta e a Mallory está tentando preencher ele primeiro. Nós chamamos isto "deslocamento assimétrico". Em outros casos, a Mallory está tentando rodar uma lista larga de funções: por exemplo, a Alice e outras estão tentando comprar uma lista limitada de parcelas oferecidas por uma firma em um blockchain. Nós chamamos isso de "deslocamento a granel".
Enter fullscreen mode Exit fullscreen mode

Mitigações

Front-running é um tema pervasivo em blockchains públicas como a Ethereum.

A melhor remediação é remover o benefício do front-running em sua aplicação, principalmente removendo a importância da ordem das transações ou do tempo. Por exemplo, em mercados seria melhor implementar lotes de leilões (isto também protege contra problemas com trades de alta frequência). Outra forma é um esquema de pré-comprometimento (“Vou submeter os detalhes depois”). Uma terceira opção é mitigar o custo do front-running ao especificar uma amplitude máxima e mínima aceitável de preço em uma trade, assim limitando o deslizamento do preço.

Ordenação de transações: Nodes Go-Ethereum (Geth), ordene as transações com base nos gasPrice e considere o nonce. Isto, entretanto, resulta em um leilão de gas entre participantes da rede a serem incluídos no bloco sendo minerado no momento.

Confidencialidade: Outra abordagem é limitar a visibilidade das transações, isso pode ser feito usando um esquema “commit and reveal” (comprometer-se e revelar).

Uma implementação simples é armazenar o hash keccak256 dos dados na primeira transação, depois revelar os dados e verificá-los comparando com o hash na segunda transação. Entretanto note que na própria transação vaza a intenção e possivelmente o valor da colateralização. Existem esquemas melhorados de commit and reveal que são mais seguros, entretanto requerem mais transações para funcionar, e.g. envios submarinos.

Dependência de Timestamp (Registro de tempo)

Esteja consciente de que o registro de tempo do bloco pode ser manipulado pelo minerador, e todos os usos diretos e indiretos da timestamp devem ser considerados.

!!!Note Veja a seção de Recomendações para considerações relacionadas com Dependência de Timestamp.

Veja SWC-116

Integer Overflow e Underflow

Considere uma transferência simples de tokens:

mapping (address => uint256) public balanceOf;
// INSECURE
function transfer(address _to, uint256 _value) {
   /* Check if sender has balance */
   require(balanceOf[msg.sender] >= _value);
   /* Add and subtract new balances */
   balanceOf[msg.sender] -= _value;
   balanceOf[_to] += _value;
}
// SECURE
function transfer(address _to, uint256 _value) {
   /* Check if sender has balance and for overflows */
   require(balanceOf[msg.sender] >= _value && balanceOf[_to] + _value >= balanceOf[_to]);
   /* Add and subtract new balances */
   balanceOf[msg.sender] -= _value;
   balanceOf[_to] += _value;
}
Enter fullscreen mode Exit fullscreen mode

Se um saldo atinge o_ valor máximo de uint _(2256) ele vai circular de volta ao zero, o qual checa para esta condição. Isto pode ou não ser relevante, dependendo da implementação. Pense se o valor uint tem uma oportunidade de abordar um número tão grande. Pense como a variável uint muda seu estado, e quem tem autoridade para fazer tais mudanças. Se qualquer usuário pode chamar funções que atualizam o valor uint, ele está mais vulnerável ao ataque. Se apenas um admin tem acesso a mudar o estado da variável, você deve estar seguro. Se um usuário pode incrementar por apenas 1 por vez, você provavelmente também está seguro, porque não há uma maneira viável de atingir este limite.

O mesmo é verdade para underflow. Se uma uint é feita para ser menos que zero, ela vai causar um underflow e será definida em seu valor máximo.

Tenha cuidado com tipos de dados menores, como uint8, uint 16, uint24… etc: eles podem ainda mais facilmente atingir seu valor máximo.

!!! Cuidado Esteja ciente que existem cerca de 20 casos de overflow e underflow.

Uma solução simples para mitigar os erros comuns de overflow e underflow é usar a biblioteca SafeMath.sol para funções aritiméticas.

Veja SWC-101

DoS com (inesperada) reversão

Considere um simples contrato de leilão:

// INSECURE
contract Auction {
   address currentLeader;
   uint highestBid;
   function bid() payable {
       require(msg.value > highestBid);
       require(currentLeader.send(highestBid)); // Refund the old leader, if it fails then revert
       currentLeader = msg.sender;
       highestBid = msg.value;
   }
}
Enter fullscreen mode Exit fullscreen mode

Se um atacante dá um lance usando um smart contract que tem uma função fallback que reverte qualquer pagamento, o atacante pode arrematar qualquer leilão. Quando o contrato tenta devolver os fundos para o antigo líder, ele reverte caso a devolução falhe. Isto significa que um apostador malicioso pode se tornar o líder se ele garantir que qualquer devolução para o endereço deles vai sempre falhar. Desta forma, eles podem prevenir qualquer um de chamar a função lance(), e ficar como lider para sempre. Uma recomendação é, ao invés disso, definir um sistema de pagamento puxado como definido anteriormente.

Outro exemplo é quando um contrato pode iterar dentro de um vetor para pagar usuários (e.g., apoiadores em um contrato de crowdfunding). É comum querer ter certeza que cada pagamento será bem sucedido. Se não, um deveria reverter. Essa questão é que se uma chamada falhar, você estará revertendo o sistema de pagamento inteiro, significando que o círculo nunca vai se completar. Ninguém é pago porque um endereço está forçando um erro.

address[] private refundAddresses;
mapping (address => uint) public refunds;
// bad
function refundAll() public {
   for(uint x; x < refundAddresses.length; x++) { // arbitrary length iteration based on how many addresses participated
       require(refundAddresses[x].send(refunds[refundAddresses[x]])) // doubly bad, now a single failure on send will hold up all funds
   }
}
Enter fullscreen mode Exit fullscreen mode

Novamente, a solução recomendada é favorecer pagamentos puxados em detrimento dos empurrados.

See SWC-113

DoS com Limite de Gas do Bloco

Cada bloco tem um teto do quantidade de gas que pode ser gasto, e assim, a necessária computação pode ser feita. Este é o Limite de Gas do Bloco. Se o gasto em Gas excede este limite, a transação falhará. Isto leva a alguns possíveis vetores de Negação de Serviço (Denial of Service):

Limite de Gas do DoS em um Contrato via Operações Não-delimitadas

Você deve ter notado outro problema com o exemplo anterior: ao pagar todo mundo de uma vez, você arrisca chegar ao limite de gas do bloco. Isto pode levar a problemas mesmo na ausência de um ataque intencional. Entretanto, é especialmente ruim se um atacante pode manipular a quantidade de gas necessária. No caso do exemplo anterior, o atacante poderia adicionar um monte de endereços, sendo que cada um precisaria receber uma pequena devolução. O custo de gas da devolução para cada um dos endereços do atacante poderia, dessa forma, acabar sendo maior do que o limite de gas, bloqueando totalmente a transação de devolução dos fundos.

Esta é outra razão para dar preferencia para os pagametos puxados em detrimento dos empurrados.

Se você absolutamente precisa fazer uma volta sobre um vetor de tamanho desconhecido, então você deve planejar para que ele potencialmente necessite muitos blocos, e portanto precise de múltiplas transações. Você vai precisar rastrear quão longe você foi, e vai ter que estar pronto para continuar daquele ponto, como no exemplo seguinte:

struct Payee {
   address addr;
   uint256 value;
}
Payee[] payees;
uint256 nextPayeeIndex;
function payOut() {
   uint256 i = nextPayeeIndex;
   while (i < payees.length && msg.gas > 200000) {
     payees[i].addr.send(payees[i].value);
     i++;
   }
   nextPayeeIndex = i;
}
Enter fullscreen mode Exit fullscreen mode

Você vai precisar ter certeza que nada de ruim vai acontecer se outras transações forem processadas enquanto espera pela próxima iteração da função payOut(), então apenas use esse padrão se for absolutamente necessário.

DoS de Limite de Gas na Rede via Enchimento de Bloco

Mesmo que seu contrato não contenha uma volta não delimitada, um atacante pode prevenir outras transações de serem incluídas na blockchain por diversos blocos ao colocar transações de computacionalmente intensiva com um preço de gas alto o suficiente.

Para fazer isso, o atacante pode enviar diversas transações que irão consumir todo o limite de gas com um preço de gas alto suficiente para ser incluído tão logo o próximo bloco seja minerado. Nenhum preço de gas pode garantir inclusão no bloco, mas quanto maior o preço, maior a chance.

Este ataque foi conduzido na Fomo3D, um aplicativo de apostas. O aplicativo foi projetado para recompensar o último endereço a comprar uma “chave”. Cada compra de chave estende o cronômetro, e o jogo termina quando o cronômetro chegar a 0. O atacante comprou uma chave e então encheu 13 blocos em seguida até que o cronômetro fosse engatilhado e o pagamento fosse realizado. Transações enviados por um atacante gastaram 7.9 milhões de gas em cada bloco , então o limite de gas permitiu algumas transações “enviadas” (as quais gastaram 21.000 gas cada), mas não permitiu qualquer chamada com a função buyKey()(que custavam 300.000 + gas).

Um ataque de enchimento do bloco pode ser usado em qualquer contrato pedindo uma ação dentro de um certo período de tempo. Entretanto, como em qualquer ataque, só é lucrativo quando as recompensas esperadas superam seus custos. O custo desse ataque é diretamente proporcional ao numero de blocos que precisam ser enchidos. Se um grande pagamento pode ser obtido ao prevenir ações de outros participantes, é bem capaz que seus contratos sejam alvejados por tal ataque.

Veja SWC-128

Perturbação por gas insuficiente

Este ataque pode ser possível em um contrato que aceita data genérica e usa isso para fazer uma chamada a outro contrato (uma ‘sub-chamada’) via a função de baixo nível address.call(), como é frequentemente o caso com transações de multiassinaturas e contratos de retransmissão (relayer).

Se a chamada falhar, o contrato tem duas opções:

  1. Reverter a inteira transação

  2. Execução contínua.

Tome o seguinte exemplo de um contrato Relayer simplificado que continua executando indiferente do resultado da sub-chamada:

contract Relayer {
   mapping (bytes => bool) executed;
   function relay(bytes _data) public {
       // replay protection; do not call the same transaction twice
       require(executed[_data] == 0, "Duplicate call");
       executed[_data] = true;
       innerContract.call(bytes4(keccak256("execute(bytes)")), _data);
   }
}
Enter fullscreen mode Exit fullscreen mode

Este contrato permite retransmissão de transações. Alguém que quer fazer uma transação mas não pode executá-la sozinho (e.g. por causa da falta de ether para pagar por gas) pode assinar dados de que ele quer passar e transferir os dados com sua assinatura sobre qualquer meio. Uma terceira parte “encaminhadora” pode então submeter esta transação para a rede em nome do usuário.

Se, dada a quantidade certa de gas, a Relayer poderia completar a execução gravando o argumento _data no mapeamento executed, mas a subchamada falharia porque ele recebeu gas insuficiente para completar a execução.

!!!Note Quando um contrato faz uma sub-chamada para outro contrato a EVM limita o gas encaminhado ao 63/64 do gas remanescente.

Este ataque é uma forma de “perturbação”: Isto não beneficia diretamente o atacante, mas causa perturbação para a vítima. Um atacante dedicado, disposto a gastar consistentemente uma pequena quantidade de gas, poderia teoricamente censurar todas as transações desta forma, se eles fossem os primeiros a submetê-los a Relayer.

Uma forma de resolver isso é implementar uma lógica exigindo que encaminhadores ofereçam gas suficiente para finalizar a subchamada. Se o minerador tentar conduzir o ataque neste cenário, a declaração require falharia e a chamada interna iria reverter. Um usuário pode especificar um gasLimitmínimo junto com os outros dados (neste exemplo, tipicamente, o valor do gasLimitseria verificado por uma assinatura, mas que é omitido por simplicidade neste caso).

// contract called by Relayer
contract Executor {
   function execute(bytes _data, uint _gasLimit) {
       require(gasleft() >= _gasLimit);
       ...
   }
}
Enter fullscreen mode Exit fullscreen mode

Outra solução é permitir apenas contas confiáveis para retransmitir a transação.

Enviando Ether Forçadamente para um Contrato

É possível, de forma forçada, enviar Ether para um contrato sem engatilhar esta função fallback. Esta é uma consideração importante ao se colocar uma importante lógica na função fallback ou fazer cálculos baseados no saldo do contrato. Veja o seguinte exemplo:

contract Vulnerable {
   function () payable {
       revert();
   }
   function somethingBad() {
       require(this.balance > 0);
       // Do something bad
   }
}
Enter fullscreen mode Exit fullscreen mode

A lógica do contrato parece não permitir pagamentos para o contrato e portanto evita que “algo ruim” acontecesse. Entretanto, existem alguns métodos para forçar o envio de ether para o contrato e portanto fazer seu saldo maior que zero.

O método de contrato selfdestruct permite ao usuário especificar um beneficiário para enviar qualquer excesso de ether. Selfdestruct não engatilha a função de um contrato fallback.

!!!Warning Também é possível pré-computar um endereço de contrato e enviar Ether para aquele contrato antes de fazer o deploy do contrato.

Veja SWC-132

Deprecação/ ataques históricos

Há ataques que não são mais possíveis devido à mudanças no protocolo ou melhorias da solidity. Eles estão gravadas aqui para prosperidade e consciência.

Ataque de Chamada Profunda (Deprecada)

Como no hardfork EIP 150, ataques de chamada profunda não são mais relevantes* (todo gas seria consumido bem antes de alcançar o limite 1024 de chamada profunda).

Ataque de Reentrância Constantinopla

Em 16 de Janeiro de 2019, a atualização do protocolo Constantinopla foi atrasada devido a uma vulnerabilidade de segurança habilitada pelo EIP 1283. EIP 1283: Medição de gas na rede par SSTORE sem mapas sujos propõe mudanças para reduzir custos de gas excessivo em escritas de armazenamento sujas.

Esta mudança leva à possibilidade de um novo vetor de reentrância fazendo padrões de saque previamente seguros (.send() e .transfer()) inseguros em situações específicas*, onde o atacante poderia sequestrar o fluxo de controle e usar o gas remanescente habilitado pelo EIP 1283, levando a vulnerabilidades devido à reentrância.

Outras Vulnerabilidades

O Registro de Classificação de Fraquezas em Smart Contract oferece um catálogo completo e atualizado de vulnerabilidades e anti-padrões conhecidos em smart contracts, juntamente com exemplos do mundo real. Navegar o registro é uma forma de se manter atualizado com os últimos ataques.

Top comments (0)