O que é um Contrato Inteligente?
A Ethereum possui dois tipos comuns de contas: Contas Externas de Propriedade (EOA) e Contas de Contrato Inteligente (SCA).
EOA são muito semelhantes às contas financeiras eletrônicas que comumente usamos para armazenar fundos e interagir com aplicativos. Por exemplo, os usuários depositam moeda fiduciária por meio do PayPal e interagem com vários sites, lojas e aplicativos para pagamentos.
Os mineradores de DeFi geralmente armazenam criptomoedas em seus EOA, interagem com DeFi dApps e depositam fundos em dApps para obter lucros. No entanto, os EOA têm um recurso que as contas financeiras eletrônicas não possuem: os usuários devem ter seu controle sobre o EOA verificado por meio da propriedade de chaves privadas – não suas chaves, não suas moedas.
SCA também são um tipo de conta que está essencialmente associada a um segmento de bytecode executável (também conhecido como contrato inteligente).
O contrato inteligente descreve várias lógicas de negócios e serve como back-end para dApps.
No entanto, apesar de ter mais restrições em comparação com as linguagens de desenvolvimento Turing completas tradicionais, os contratos inteligentes quasi-Turing completos ainda são vulneráveis a vários ataques, causando inúmeros golpes para a indústria blockchain.
Ataques Comuns a Contratos Inteligentes
- Ataque de Reentrância
O ataque mais comum e notório é o ataque de reentrância, responsável pelo fork Ethereum que levou à criação do Ethereum Classic. Em 2016, hackers executaram um ataque de reentrância no contrato The DAO, roubando 3.600.000 ETH avaliados em mais de US$ 150 milhões na época. Esse ataque, ocorrido durante os estágios iniciais do Ethereum, devastou o ecossistema e abalou a confiança dos investidores, levando a um fork.
Lógica Específica
Aqui está um exemplo para ajudá-lo a entender melhor o princípio do ataque de reentrância. O Banco B anteriormente emprestou dinheiro ao Banco A. Um dia, o Banco B inicia uma transferência para o Banco A, solicitando a transferência de todo o dinheiro de volta para o Banco B. O caminho normal é o seguinte:
- Etapa 1: O Banco B solicita a retirada de fundos
- Etapa 2: O Banco A transfere os fundos para o Banco B
- Etapa 3: O Banco A confirma a transferência bem-sucedida para o Banco B
- Etapa 4: O Banco A atualiza o saldo da conta do Banco B.
No entanto, se o Banco B criar uma brecha após a Etapa 2 e continuar solicitando todo o dinheiro do Banco A sem confirmação na Etapa 3, o saldo da conta do Banco A no Banco B permanecerá inalterado. Esta chamada recursiva esvaziará todos os ativos do Banco A.
Contratos Inteligentes Relacionados
O contrato do Banco A inclui duas funções:
- deposit(): Uma função de depósito que deposita dinheiro no Banco A e atualiza o saldo do usuário.
- withdraw(): Uma função de saque que permite aos usuários sacar todos os seus fundos do Banco A.
O ataque ao contrato do Banco B
O ataque ao contrato do Banco B envolve principalmente um loop que aciona a função de callback receive(), que por sua vez chama a função withdraw() do contrato do Banco A para drenar os ativos do Banco A através de uma sequência de 1 depósito, 1 saque e chamadas de função de callback receive(), e finalmente atualiza o saldo do Banco B no Banco A.
O contrato de ataque inclui duas funções:
- receive(): Uma função de callback acionada quando ETH é recebido, que chama recursivamente a função withdraw() do contrato do Banco para fazer saques.
- attack(): Primeiro chama a função deposit() do contrato do Banco para atualizar o saldo e depois a função withdraw() para iniciar o primeiro saque, e aciona a função de callback receive() para chamar recursivamente withdraw() para drenar os ativos do contrato do Banco.
Solução
- Implementando um Bloqueio de Reentrância
Um bloqueio de reentrância é um modificador usado para evitar a reentrância, garantindo que uma chamada deve concluir sua execução antes que possa ser invocada novamente. Por exemplo, como o ataque do Banco B requer chamar a função withdraw() do contrato do Banco várias vezes, ele falhará com a implementação de um bloqueio de reentrância.
Como usá-lo
2. Uso indevido de tx.origin
A principal função de tx.origin em um contrato inteligente é recuperar a conta original que iniciou a transação. Aqui, discutiremos duas variáveis comuns em contratos inteligentes: msg.sender e tx.origin. msg.sender recupera a conta que chama diretamente o contrato inteligente, enquanto no mundo blockchain, devido às chamadas aninhadas e mútuas de diferentes contratos inteligentes (como DeFi Lego), tx.origin é necessário para obter a conta original que iniciou a transação.
Uma vulnerabilidade surge quando os desenvolvedores de dApp apenas verificam a segurança de tx.origin no código, negligenciando a verificação de segurança de atacantes que implantam contratos intermediários para contornar tx.origin e lançar ataques.
Lógica Específica
Aqui está um exemplo para ajudá-lo a se aprofundar no cenário de ataque comum. Bill tem uma carteira inteligente que verifica se ele é o iniciador de uma transferência. Uma vez, Bill cunhou um NFT em um site de phishing. Isso permitiu que o site obtivesse a identidade de Bill e iniciasse uma transferência de sua carteira inteligente usando sua identidade, resultando em perdas de ativos.
Em circunstâncias normais, os usuários são menos propensos a cair nessa armadilha, mas ao interagir com dApps usando uma carteira, eles geralmente se esquecem de verificar os prompts de interação. Por exemplo, se ambos envolvem a função Mint(), usuários descuidados podem facilmente cair em uma armadilha de phishing. A lógica de negócios dentro do site de phishing está repleta de armadilhas, por isso é importante verificar se há erros nos prompts de interação durante as interações regulares.
Contrato de Carteira Inteligente
O contrato de carteira inteligente inclui uma função:
- transfer(): Uma função de saque que só pode ser iniciada pelo proprietário da carteira, que neste caso é Bill.
Contrato de Ataque de Phishing
Em um contrato de ataque de phishing, a função Mint() induz os usuários a transferir fundos para o endereço de um hacker. Ela inclui uma função:
- Mint(): Uma vez chamada, a função de phishing executa internamente a função transfer() do contrato da carteira. Como o iniciador original é o próprio usuário (neste exemplo, Bill), a verificação “require(tx.origin == owner, “Not owner”);” não será um problema. No entanto, o endereço de destino para a transferência já foi adulterado para o endereço do hacker, resultando no roubo de fundos.
Soluções
- Use msg.sender em vez de tx.origin
Independentemente de quantas chamadas de contrato estejam envolvidas (Contrato A → Contrato B →…→ contrato alvo), verifique apenas o msg.sender, ou seja, o chamador direto, para evitar ataques causados por contratos intermediários maliciosos.
- Verifique tx.origin == msg.sender
Este método pode manter os contratos maliciosos longe, mas os desenvolvedores precisam considerar suas próprias realidades de negócios, pois ele efetivamente isola todas as outras chamadas de contratos externos.
3. Ataque ao Gerador de Números Aleatórios (RNG)
Este ataque remonta à tendência de dApps de jogos ou apostas por volta de 2018 e 2019. Normalmente, os desenvolvedores usam determinadas sementes em contratos inteligentes para gerar números aleatórios para selecionar vencedores durante os sorteios. As sementes comuns incluem block.number, block.timestamp, blockhash e keccak256. No entanto, os mineradores podem controlar totalmente essas sementes, então, em alguns casos, mineradores mal-intencionados podem manipular as variáveis para obter benefícios.
Contratos de Dado Comum
O Contrato de Dado inclui uma função:
- Bet(): Uma função de aposta onde os usuários inserem um número de aposta e pagam um ETH. Um número aleatório é gerado com várias sementes e, se o número de aposta corresponder ao número aleatório, o usuário ganha todo o prêmio.
Ataque de Contrato por Mineradores
Mineradores podem ganhar se pre-computarem o número aleatório vencedor e o executarem no mesmo bloco. Isso inclui uma função:
- attack(): Uma função de aposta que pré-calcula o número aleatório vencedor. Como é executado no mesmo bloco, blockhash(block.number – 1) e block.timestamp no mesmo bloco são iguais. Então, o minerador chama Bet() do contrato de Dado para concluir o ataque.
Solução
- Usar números aleatórios fora da rede fornecidos por projetos oracle
Os projetos oracle, como o Chainlink, injetam números aleatórios gerados fora da rede em contratos dentro da rede para garantir aleatoriedade e segurança. No entanto, esses projetos também apresentam riscos de centralização, tornando necessário o uso de serviços oracle mais maduros.
- Ataque de repetição
Um ataque de repetição envolve reiniciar uma transação utilizando uma assinatura usada anteriormente, com o objetivo de roubar fundos. Um dos ataques de repetição mais conhecidos nos últimos anos foi o roubo de 20 milhões de tokens $OP do criador de mercado Wintermute no Optimism, que foi um ataque de repetição cross-chain.
Como a conta multi-assinatura de Wintermute foi temporariamente implantada apenas na rede principal do Ethereum, o hacker usou a assinatura da transação para a implantação de um endereço multi-assinatura da Wintermute no Ethereum para reexecutar a mesma transação na rede Optimism, obtendo assim o controle da conta multi-assinatura no Optimism.
Uma conta de carteira multi-assinatura é essencialmente uma conta de contrato inteligente, o que também demonstra uma diferença significativa entre SCA e EOA:
- EOA: um usuário normal só precisa de uma chave privada para controlar todos os endereços no Ethereum e redes compatíveis com EVM (as strings de endereço são exatamente as mesmas)
- SCA: só é efetivo em uma rede após ser implantado.
Lógica específica
Aqui, fornecemos um exemplo de um ataque de repetição típico (ataque de repetição na mesma rede). Bill tem uma carteira inteligente que exige que ele insira sua assinatura eletrônica antes que cada transação possa ser executada. Agora que a hacker Lucy roubou a assinatura eletrônica de Bill, ela pode iniciar um número ilimitado de transações para esgotar a carteira inteligente de Bill.
Exemplo
Um contrato com vulnerabilidades consiste em três funções:
- checkSig(): Função de verificação ECDSA, garantindo que o resultado da verificação seja o signatário originalmente definido.
- getMsgHash(): Função para gerar hash, que combina to e amount para formar o hash.
- transfer(): Função de transferência, permitindo aos usuários sacar fundos do pool de liquidez. Devido à falta de restrições na assinatura, a mesma assinatura pode ser reutilizada, permitindo que hackers roubem fundos continuamente.
Solução
Para evitar ataques de repetição, inclua nonce na combinação de assinatura. O princípio do parâmetro é o seguinte:
- nonce: Descreve a variável do número de transações de uma EOA na rede blockchain. Possui ordem e unicidade. A cada transação adicional, o valor do nonce aumentará em 1. A rede blockchain verificará se o nonce da transação é consistente com o nonce atual da conta. Portanto, um hacker falhará ao usar uma assinatura usada porque o valor do nonce na combinação de assinatura é menor que o valor do nonce atual da EOA.
5. Ataque de negação de serviço (DoS)
O ataque de Negação de Serviço (DoS) não é nada novo no mundo tradicional da Web2. Refere-se a qualquer interferência com um servidor, como o envio de uma grande quantidade de informação inútil ou disruptiva, prejudicando ou destruindo completamente sua disponibilidade.
Da mesma forma, os contratos inteligentes são atormentados por tais ataques, que essencialmente visam fazer o contrato inteligente funcionar mal.
Lógica Específica
Vejamos um exemplo. O Projeto A está conduzindo uma oferta pública para o token do protocolo, onde todos os usuários podem contribuir com fundos para o pool de liquidez (Contrato Inteligente) para comprar cotas por ordem de chegada, e os fundos excedentes serão devolvidos aos participantes. A hacker Alice explora o ataque de contrato para participar da oferta pública. Assim que o pool de liquidez tentar devolver fundos ao contrato de ataque de Alice, um ataque DoS será acionado, impedindo que a ação de retorno seja realizada. Como resultado, uma grande quantidade de fundos fica bloqueada no contrato inteligente.
Exemplo
O contrato de oferta pública inclui duas funções:
- deposit(): função de depósito, registrando o endereço do depositante e o valor contribuído.
- refund(): função de reembolso, com o qual a equipe do projeto devolve os fundos aos investidores.
Contrato de ataque DoS
O contrato de ataque DoS inclui apenas uma função:
- attack(): Embora seja uma função de ataque, ela não apresenta nenhum problema. O principal problema está na função de retorno de pagamento receive() incorporada ao contrato Hacker, que inclui um julgamento de exceções. Qualquer contrato externo que transfira fundos para o contrato Hacker irá acionar uma exceção através de revert(), impedindo assim a conclusão da operação.
Soluções
- Evitar bloqueio de funcionalidade crítica ao evocar contratos externos:
- Remova o require(success, “Refund Fail!”); da função refund() do contrato PublicSale. Isso garantirá que a operação de reembolso possa continuar mesmo se um reembolso para um único endereço falhar.
- Desacoplamento:
- Na função refund() acima do contrato PublicSale, permita que os usuários solicitem reembolsos por conta própria, em vez de distribuir os reembolsos, minimizando assim interações desnecessárias com contratos externos.
6. Ataque de Permissão
Em um ataque de permissão, a Conta A fornece a assinatura para uma parte designada com antecedência. Então, a Conta B, ao obter a assinatura, pode realizar transferências de tokens autorizadas para roubar uma certa quantia de tokens. Aqui, discutimos principalmente duas funções comuns para autorização de tokens em Contratos Inteligentes: approve() e permit().
No contrato ERC20 comum, a Conta A pode chamar approve() para autorizar uma certa quantia de tokens para a Conta B, permitindo que o último transfira esses tokens do primeiro. Além disso, permit() foi introduzido em contratos ERC20 no EIP-2612, e o Uniswap lançou um novo padrão de autorização de tokens, Permit2, em novembro de 2022.
Lógica Específica
Aqui está um exemplo. Um dia, Bill estava navegando em um site de notícias blockchain quando, de repente, um pop-up de assinatura Metamask apareceu. Como muitos sites ou aplicativos blockchain usam assinaturas para verificar logins de usuários, Bill não pensou muito e concluiu a assinatura diretamente. Cinco minutos depois, seus ativos Metamask estavam esgotados. Bill então descobriu no explorador de blockchain que um endereço desconhecido iniciou uma transação permit(), seguida por uma transação transferFrom() que esvaziou sua carteira.
Exemplo
As duas funções são as seguintes:
- approve(): Uma função de autorização padrão onde a Conta A autoriza um determinado valor de fundos para a Conta B.
- permit(): Uma função de autorização por assinatura onde a Conta B envia e conclui a verificação da assinatura para obter o valor autorizado da Conta A. Os parâmetros incluem o owner (proprietário) que concede a autorização, o spender (quem gasta) que está sendo autorizado, o amount (valor) autorizado, a deadline (prazo) da assinatura e os dados da assinatura do proprietário v, r e s.
Soluções
- Preste atenção a todas as assinaturas em interações on-chain
Apesar de algumas carteiras decodificarem e exibirem informações de assinatura de autorização “approve()“, elas quase não fornecem alertas para phishing de assinatura “permit()“, aumentando o risco de ataques. Portanto, é altamente recomendável inspecionar rigorosamente todas as assinaturas desconhecidas para garantir que sejam destinadas à função “permit()“.
- Separe a carteira para interações regulares da carteira que armazena ativos
Isso é extremamente importante para usuários de criptomoedas, especialmente caçadores de airdrop, pois eles interagem com inúmeros dApps ou sites todos os dias e estão propensos a armadilhas. Armazenar apenas uma pequena quantidade de fundos em uma carteira para interação regular pode manter as perdas dentro de um limite gerenciável.
7. Ataque Honeypot (Pote de Mel)
Na indústria de blockchain, um ataque de honeypot refere-se a um tipo de contrato de token malicioso implantado pelas equipes do projeto. O contrato concede apenas à equipe do projeto a permissão para vender, enquanto usuários regulares só podem comprar, não vender, sofrendo assim perdas.
Lógica Específica
Aqui está um exemplo. Em um anúncio no Telegram, o Projeto A informa aos usuários que o token foi implantado na rede principal e está disponível para negociação. Como o token só pode ser comprado e não vendido, o preço continuou subindo no início, e os usuários que temem perder continuam comprando. Depois de algum tempo, quando os usuários não conseguem vender, a equipe do projeto aproveita a oportunidade e vende os tokens, fazendo com que o preço despenque.
Exemplo:
Função Principal:
_beforeTokenTransfer(): Uma função interna chamada durante as transferências de token, que só pode ser bem-sucedida quando chamada pelo proprietário; chamadas de outras contas falharão.
Solução
Utilize ferramentas de varredura de segurança:
a. Token Sniffer para tokens Ethereum
b. Ave Check para tokens em outras redes
c. Websites de mercado com ferramentas de detecção integradas como Dextools
Evite negociar tokens com pontuações baixas.
8. Ataque de Front-Running
O front-running surgiu originalmente nos mercados financeiros tradicionais, onde a assimetria de informações permitia que os intermediários financeiros obtivessem lucros tomando ações rápidas com base em informações específicas do setor. Na indústria blockchain, o front-running deriva principalmente do front-running on-chain, que envolve a manipulação de mineradores para priorizar o empacotamento de suas próprias transações na rede para obter lucros.
No campo blockchain, os mineradores podem lucrar manipulando as transações que empacotam em blocos, por exemplo, excluindo certas transações e reordenando as transações. Esse lucro pode ser medido com o Valor Máximo Extrativo (MEV). Antes que a transação de um usuário seja adicionada à mainnet da Ethereum, a maioria das transações é agregada no mempool. Os mineradores procuram transações com preços de gás mais altos neste mempool e priorizam o empacotamento para maximizar seus ganhos. Geralmente, as transações com preços de gás mais altos são mais facilmente empacotadas pelos mineradores. Enquanto isso, alguns bots de MEV também vasculham o mempool em busca de transações com rentabilidade.
Lógica Específica
Bill descobre um novo token em alta com flutuações de preço significativas. Para garantir o sucesso das transações de tokens no Uniswap, Bill define uma faixa de slippage excepcionalmente ampla. Infelizmente, o bot MEV de Alice detecta essa transação no mempool e prontamente aumenta a taxa de gás, iniciando uma transação de compra antes de Bill e inserindo uma transação de venda depois de Bill no mesmo bloco. Após a confirmação do bloco, isso causa perdas significativas de slippage para Bill, enquanto Alice lucra com uma operação de arbitragem de compra em baixa e venda em alta.
Exemplo
A função é a seguinte:
solve(): Uma função de adivinhação onde qualquer pessoa pode enviar uma resposta e, se a resposta enviada corresponder à resposta alvo, o remetente pode receber 10 ethers.
Processo:
- Bill encontra a resposta correta.
- Alice monitora o pool de memória, esperando que alguém submeta a resposta correta.
- Bill chama a função “solve()” para enviar a resposta e define o preço do gás como 100 Gwei.
- Alice vê a transação enviada por Bill e descobre a resposta. Ela define um preço de gás mais alto que o de Bill (200 Gwei) e chama a função “solve()“.
- A transação de Alice é embalada pelo minerador antes da de Bill.
- Alice ganha uma recompensa de 10 ethers.
Solução
As três principais funções são as seguintes:
- commitSolution(): Uma função para submeter resultados, colocando a solução (solutionHash) da resposta fornecida pelo usuário, o tempo de envio (commitTime) e o estado revelado (revealed) na estrutura Commit.
- getMySolution(): Uma função para obter resultados, permitindo que os usuários visualizem suas respostas enviadas e informações relacionadas, incluindo a solução (solutionHash) da resposta fornecida pelo usuário, o tempo de envio (commitTime) e o estado revelado (revealed).
- revealSolution(): Uma função para reivindicar recompensas por adivinhar o enigma, permitindo que os usuários reivindiquem recompensas após fornecer a resposta e a senha que definiram.
Processo:
- Bill encontra a resposta correta.
- Bill chama commitSolution() para enviar a resposta correta.
- No próximo bloco, Bill chama revealSolution(), fornecendo a resposta e a senha que ele definiu para reivindicar a recompensa.
- Em commitSolution(), Bill envia uma string criptografada, mantendo os dados de texto simples enviados apenas para si mesmo. Nesta etapa, o horário de envio do bloco commitTime também é registrado.
- Em seguida, em revealSolution(), o horário do bloco é verificado para evitar front-running dentro do mesmo bloco. Como chamar revealSolution() requer o envio da resposta em texto simples, esta etapa visa impedir que outros ignorem commitSolution() e chamem revealSolution() diretamente.
- Após a verificação bem-sucedida, a recompensa será distribuída se a resposta for considerada correta.
Conclusão
Os contratos inteligentes desempenham um papel crucial na tecnologia blockchain e oferecem inúmeras vantagens. Em primeiro lugar, eles permitem a execução descentralizada e automatizada, garantindo a segurança e confiabilidade das transações sem terceiros. Em segundo lugar, os contratos inteligentes reduzem as etapas intermediárias e os custos, melhorando a eficiência das transações.
Apesar de tantos benefícios, os contratos inteligentes também enfrentam o risco de ataques que geram perdas financeiras aos usuários. Portanto, alguns hábitos são essenciais para usuários on-chain. Primeiramente, os usuários devem sempre escolher cuidadosamente os dApps para interagir e revisar completamente o código do contrato e as regras relacionadas. Além disso, eles devem atualizar e usar regularmente carteiras seguras e ferramentas de interação com contratos para mitigar o risco de ataques de hackers. Além disso, é aconselhável armazenar seus fundos em vários endereços para minimizar as perdas potenciais de ataques a contratos.
Para os players da indústria, garantir a segurança e a estabilidade dos contratos inteligentes é de igual importância. A primeira prioridade deve ser fortalecer a auditoria de contratos inteligentes para identificar e corrigir potenciais vulnerabilidades e riscos de segurança. Em segundo lugar, os players da indústria devem se manter informados sobre os últimos desenvolvimentos de blockchain relacionados a ataques a contratos e tomar medidas de segurança de acordo. Por último, mas não menos importante, eles também devem aprimorar a educação do usuário e a conscientização sobre segurança em termos do uso correto de contratos inteligentes.
Concluindo, com os esforços conjuntos de usuários e players da indústria, os riscos de segurança representados por contratos inteligentes podem ser mitigados significativamente. Os usuários devem sempre selecionar cuidadosamente os contratos e proteger os ativos pessoais, enquanto os players da indústria devem intensificar a auditoria de contratos, acompanhar os avanços tecnológicos e aprimorar a educação do usuário e a conscientização sobre segurança. Juntos, impulsionaremos o desenvolvimento seguro e confiável de contratos inteligentes.
Referências:
Solidity by Example
https://solidity-by-example.org/
Blockchain Know-how of SlowMist
Chainlink – Top 10 DeFi Security Best Practices
https://blog.chain.link/defi-security-best-practices/#post-title
WTF – Solidity 104 Contract Security
https://www.wtf.academy/solidity-104/
Vulnerabilities in DeFi Smart Contracts in 4 Categories with 38 Scenarios
https://www.weiyangx.com/381670.html
OpenZeppelin