Imagine que você é dono de um caixa eletrônico, aqueles ATMs de banco, sabe? Você deve supor que se deixá-lo em um lugar público, com câmeras e muita gente passando, ou em um lugar privado, mas com guardas e seguranças ao redor, será melhor do que deixá-lo sozinho em uma viela qualquer da cidade, certo?
Porque se você optar pela segunda opção, será muito mais difícil mantê-lo seguro, certo? Frequentemente vai ter alguém tentando arrombá-lo, já que ninguém está vendo e tem muito dinheiro dentro dele.
O mesmo acontece com os protocolos DeFi. Sempre que você faz deploy do smart contract de um novo protocolo DeFi na blockchain e ele passa a receber fundos dos usuários, ele se torna uma presa tão fácil quanto um caixa eletrônico carregado “dando sopa” em um beco escuro de uma grande cidade à noite. Vão tentar te atacar, isso é certo.
E dentre os ataques possíveis que os protocolos DeFi vem a ser alvo, quero falar de um deles hoje chamado Gas Griefing Attack. Ele não é tão comum quanto os Reentrancy Attacks, mas igualmente perigoso para alguns tipos de protocolos como os de leilões, entre outros.
E antes que eu possa ensiná-lo como se proteger do mesmo, você precisa entender sobre taxas de transação (aprenda aqui) e eu preciso lhe ensinar como esse ataque é feito, para fins didáticos. Peço que não use conhecimento para o mal. Se preferir, pode assistir ao vídeo abaixo ao invés de ler o post, o conteúdo é o mesmo.
Vamos lá!
#1 – Gas Griefing Attack Explicado
Imagine que você tem um contrato de protocolo DeFi de leilões, como abaixo, que permite dar lances em produtos (NFTs, por exemplo). Olhe e pense se ele parece ou não ok, em especial a função de bid, que será o alvo dos hackers.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract Auction { address public highestBidder; uint256 public highestBid; uint256 public auctionEnd; constructor(){ auctionEnd = block.timestamp + (7 * 24 * 60 * 60);//7 days in the future } function bid() external payable { require(msg.value > highestBid, "Bid is not high enough"); require(block.timestamp <= auctionEnd, "Auction finished"); //refund the previous highest bidder if (highestBidder != address(0)) { (bool success, ) = highestBidder.call{value: highestBid}(""); require(success, "refund failed"); } highestBidder = msg.sender; highestBid = msg.value; } } |
A função bid recebe um lance e verificar se ele é o mais alto. Se for, ela armazena os dados do bidder e da bid. Se mais tarde alguém der um lance ainda mais alto, o bidder anterior receberá de volta o valor do seu lance e o novo bidder tomará o seu lugar. Quando o prazo da auction terminar, novos lances não podem mais ser realizados.
Repare que tomei o cuidado aqui de verificar o retorno do call de devolução do bid, já que se não fizesse isso estaríamos expostos a outros ataques possivelmente mais graves e frequentes.
Antes de lhe dizer qual é o risco dessa função, caso não tenha suposto por si só, eu preciso lhe explicar como o ataque é feito. Primeiro, é importante relembrar que na blockchain tanto as carteiras cripto quanto os smart contracts possuem endereços capazes de enviar e receber criptomoedas. Assim, eu posso interagir com o protocolo acima tanto usando uma carteira cripto quanto usando um outro smart contract qualquer, que é o que o hacker irá fazer.
Um hacker querendo fazer um Ataque de Luto por Gás (Gas Griefing Attack), vai criar um smart contract semelhante ao abaixo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; interface IAuction { function bid() external payable; } contract GGA { function attack(address _auction) external payable { IAuction(_auction).bid{value: msg.value}(); } receive() external payable { keccak256("just wasting some gas..."); keccak256("just wasting some gas..."); keccak256("just wasting some gas..."); keccak256("just wasting some gas..."); keccak256("just wasting some gas..."); //etc... } } |
Existem variações mais sofisticadas, mas como meu intuito não é entregar ferramenta de bandido para vocês, o exemplo acima já serve. O hacker vai fazer deploy desse contrato na blockchain, passando o endereço do protocolo que vai ser vítima do ataque. Claro que antes o hacker estuda o contrato do protocolo vítima para ver se ele é suscetível ao ataque, caso contrário só perderá dinheiro no ataque e até mesmo impossibilitará o mesmo, já que precisamos montar a interface IAuction de maneira que funcione com o contrato alvo (repare que a assinatura da função é igual a do contrato Auction.sol).
O que esse contrato faz após o deploy? O hacker chama a função attack, mandando junto dela um lance que deve ser maior do que o atual maior lance da auction, para poder superá-lo e se tornar o vencedor. Esse valor será transferido para o protocolo, então quanto mais cedo o atacante entrar na auction, melhor (pra ele). O problema é o que vem a seguir.
Uma vez que o lance do atacante se torne o maior, se ninguém mais der lances maiores, ele será o vencedor, correto? Mas e se esse contrato do atacante impedir que novos lances sejam dados?
Vamos supor que outro participante do leilão dê um lance maior. O require de highestBid vai verificar isso e portanto vai iniciar a transferência do valor depositado pelo atacante anteriormente, de volta para a carteira dele. No entanto, quando ela fizer a transferência ela estará transferindo para o smart contract do atacante e não para uma conta comum. Toda vez que um smart contract recebe um depósito ele dispara uma função nativa chamada receive, que por padrão não faz nada, apenas recebe o saldo mesmo.
No entanto, você pode implementar código na sua receive, para reagir a esses depósitos e aí que entra o “luto por gás”. Dentro da receive o hacker coloca código para gastar muito gás, sem fazer nada de útil. Isso vai fazer com que essa devolução atinja o gas limit por bloco e consequentemente a transação falhe, sendo revertida automaticamente pela EVM. Ou seja, nada de devolver o dinheiro do atacante e nem de registrar o novo bidder como vencedor.
Está feito o estrago, é só esperar a auction terminar pois o atacante será o vencedor e receberá o seu prêmio.
#2 – Como se proteger de Gas Griefing Attack
Infelizmente até o momento não existe uma solução definitiva para este tipo de ataque (tecnicamente falando), exigindo que a própria EVM seja atualizada em versões futuras para preveni-lo. Ainda assim, existem algumas soluções de contorno sendo a principal delas repensar na sua lógica de negócio do contrato.
No exemplo hipotético acima, você realmente tem de devolver os bids vencidos antes da auction terminar? Porque se você puder deixar a devolução apenas após entregar o prêmio ao vencedor, isso garante a justiça do leilão e mesmo que durante a devolução dos valores (em funções separadas, para que o leilão possa ser encerrado em etapas, garantindo a independência das atividades) dê erro, uma função para devolução manual poderia ser acionada.
Outra possibilidade, ainda pensando em rever fluxo de negócio é a devolução ser via saque (withdraw) do próprio “perdedor”. Ou seja, fica de responsabilidade de quem perdeu o leilão pegar a sua grana de volta quando quiser, com o contrato mantendo registro dessa dívida e bloqueando os fundos do mesmo. Apenas tem de cuidar para que essa funcionalidade de saque não seja suscetível a Reentrancy Attacks.
Outra forma de evitar estes e outros ataques que usam as funções receive e fallback para executar código personalizado é não usar o token nativo da rede (ETH no caso da Ethereum), mas sim algum token ERC-20, de repente algum próprio. Assim as transferências de fundos acontecerão no contrato do token, o que custará mais gás do que usar o token nativo, mas menos suscetível a essas brechas.
Outra coisa a se pensar é por mais que seja tentadora a ideia da sua empresa operar como uma DAO, 100% automatizada e descentralizada através de smart contracts, é importante que você tenha monitoramentos ativos dos seus contratos para garantir que não tem ninguém agindo de má fé, como no caso de Gas Griefing Attacks.
O vencedor de um leilão não é mais substituído por novos lances há muito tempo? Suspeito!
O primeiro lance é o vencedor de um leilão que deveria ser disputadíssimo? Suspeito.
Processos manuais, que permitam um ok antes de liberar o prêmio por exemplo, já são o suficiente para inibir esse tipo de ataque já que o atacante sabe que a sua atividade vai despertar atenção quando inspecionada. E eu sei que isso soa contraprodutivo e até mesmo centralizador demais, não sendo congruente com a proposta da web3, mas estamos falando da proteção do seu negócio em situações extremas (só nos leilões mais importantes, de repente).
Mas Luiz, não há absolutamente nada que possamos fazer tecnicamente como proteção?
Sim, há, dependendo de como o ataque está sendo efetuado.
Se ele está sendo efetuado com o intuito de exaurir os fundos dos usuários (o gás não é devolvido em caso de erro da transação), a solução mais comum é a proposta pela ConsenSys (empresa criadora da MetaMask e Infura aliás) e documentada no SWC Registry nesse artigo, onde eles dão duas opções: uma é somente usuários pré-autorizados poderem receber as chamadas externas (a devolução da bid, no exemplo anterior) e outra é o usuário definir quanto de gás ele quer gastar na transação para evitar cair em armadilhas como aquela. Como essa segunda opção não é nada prática, existem variações dessa proposta onde o próprio desenvolvedor do smart contract faz esses cálculos e coloca no código para evitar abusos.
Agora se ele está sendo usado como DoS (Denial of Service) do serviço, uma solução é usar a função send ao invés de call, que por si só já tem um gas limit suficiente para que o envio ocorra, assim como a função transfer e diferente da função call. No entanto, ela é o melhor dos dois mundos já que assim como a call, não dá erro se a transferência falhar, mas sim devolve um booleano que lhe permite decidir o que fazer, podendo por exemplo salvar estas transferências para novo processamento mais tarde.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract Auction { address public highestBidder; uint256 public highestBid; uint256 public auctionEnd; constructor(){ auctionEnd = block.timestamp + (7 * 24 * 60 * 60);//7 days in the future } function bid() external payable { require(msg.value > highestBid, "Bid is not high enough"); require(block.timestamp <= auctionEnd, "Auction finished"); //refund the previous highest bidder if (highestBidder != address(0)) { bool success = payable(highestBidder).send(highestBid); //save the failed ones and treat them later, to don't block the business flow } highestBidder = msg.sender; highestBid = msg.value; } } |
E com isso espero ter te ajudado a entender mais sobre este famoso ataque e de como se proteger dele.
Até a próxima!
Olá, tudo bem?
O que você achou deste conteúdo? Conte nos comentários.