Pular para o conteúdo
0%

12 boas práticas de segurança para smart contracts em Solidity

Usman Asim headshot

Escrito por Usman Asim

Publicado em 13 de novembro de 202515 min de leitura

Um escudo abstrato representando segurança de código

Smart contracts são os magos por trás da mágica da blockchain. São os blocos de código que automatizam a lógica onchain, especificando desde os limites de liquidação de protocolos de empréstimo descentralizados até a tokenização de ativos do mundo real, entre muitas outras coisas.

Mas com grande poder vem grande responsabilidade, especialmente quando o assunto é segurança. Um único bug pode levar a exploits massivos, e ninguém quer que seu projeto vire a próxima manchete de um hack de milhões de dólares.

Somente no primeiro semestre de 2025, mais de $2,3 bilhões em cripto foram perdidos em exploits e violações, sendo que problemas de controle de acesso representaram sozinhos mais de $1,6 bilhão desse prejuízo. Se você desenvolve com Solidity na Ethereum ou em chains similares, acertar a segurança não é opcional: é o que mantém os fundos dos seus usuários seguros e sua reputação intacta.

Neste post, vamos detalhar o que realmente significa segurança de smart contracts, explorar algumas das maiores vulnerabilidades de contratos e compartilhar boas práticas com exemplos de código para ajudar você a blindar seu código e implantar contratos seguros na mainnet.

E lembre-se: sempre teste em testnets antes de implantar na mainnet. É muito melhor encontrar vulnerabilidades antes que seus usuários comecem a enviar fundos para o seu contrato.

O que é segurança de smart contracts?

Smart contracts são trechos de código autoexecutáveis que rodam quando certas condições são atendidas. Pense em acordos automatizados que lidam com transferências, votações ou até manobras complexas de DeFi sem intermediários. Eles são implantados como bytecode na blockchain e, uma vez ativos, são imutáveis, então qualquer falha em um contrato fica exposta publicamente.

Segurança, nesse contexto, se resume a garantir que o código seja sólido, já que frequentemente os próprios contratos custodiam ativos onchain. Hackers conseguem drenar fundos de um determinado contrato? A lógica do contrato se sustenta em casos extremos estranhos? Os ativos estão protegidos contra acesso não autorizado?

Uma vulnerabilidade nessas respostas significa que um agente malicioso pode drenar os fundos dos usuários do contrato e ficar com esse dinheiro. Para proteger contra essas vulnerabilidades, você precisa pensar em implementar verificações de entrada, padrões de design e auditorias de código que endureçam seu código antes da implantação na mainnet.

Para uma introdução mais aprofundada, veja a documentação da Ethereum sobre segurança de smart contracts.

Por que a segurança importa para você

Construir contratos inseguros não é arriscado apenas para os usuários, pode acabar com seu projeto da noite para o dia. Quando um projeto é drenado, os usuários perdem a confiança e, muitas vezes, não voltam. A irreversibilidade da blockchain significa que, uma vez que os fundos se foram, não há como recuperá-los. Não existem estornos aqui. E assim como os fundos podem ser eliminados em um instante, sua reputação também pode.

Com bilhões de dólares circulando onchain, hackers estão sempre procurando pontos fracos; estatísticas recentes mostram o que está em jogo: os $2,3 bilhões perdidos no primeiro semestre de 2025. A maior parte dessas perdas veio de bugs evitáveis, como controles de acesso ruins e reentrância. Ao priorizar a segurança, você protege seus usuários, constrói confiança e evita o pesadelo de ver seu projeto ir a $0 em uma única transação.

Com as ferramentas e boas práticas disponíveis no mercado, há muitas coisas que você pode fazer para escrever um código melhor, que seja não só funcional, mas também resiliente.

Entendendo as vulnerabilidades de smart contracts

Um recurso útil para entender vulnerabilidades de smart contracts é a OWASP (sigla para Open Web Application Security Project), uma organização sem fins lucrativos que cria recursos gratuitos para melhorar a segurança de software em todo o mundo. Sua Smart Contract Top 10 é como uma lista das vulnerabilidades mais perigosas em aplicações blockchain, baseada em exploits reais e dados de incidentes que causaram bilhões em perdas.

Essa lista dá aos devs uma seleção priorizada de vulnerabilidades para focar em auditorias e no design de contratos, com base em padrões observados em hacks em diferentes ecossistemas. A edição de 2025 destaca riscos como falhas de acesso, que resultaram em $953 milhões em perdas somente no ano passado. Ao revisar seu código, estar atento a essas vulnerabilidades comuns é uma boa base para começar a construir a resiliência do seu código.

  1. Vulnerabilidades de controle de acesso: falhas que permitem que pessoas não autorizadas mexam em dados ou funções, geralmente por falta de verificações de permissão. Esse problema lidera a lista por um motivo. Pense em chaves de admin roubadas drenando contratos. Simples, mas eficaz e frequente.
  2. Manipulação de price oracle: hackers podem adulterar feeds de dados externos (oracles) para distorcer preços, levando a empréstimos ou negociações ruins. É comum em DeFi, onde preços precisos são fundamentais.
  3. Erros de lógica: bugs em que o contrato faz algo não intencional, como cunhar tokens extras ou calcular recompensas incorretamente. Uma vulnerabilidade em que você precisa cobrir todos os casos extremos. Exige auditoria profunda da lógica do contrato.
  4. Falta de validação de entrada: não verificar entradas do usuário pode permitir que dados inválidos quebrem a lógica ou explorem overflows.
  5. Ataques de reentrância: chamadas externas podem permitir que hackers reentrem em funções no meio da execução, geralmente para sacar fundos múltiplas vezes.
  6. Chamadas externas não verificadas: não tratar chamadas que falharam pode fazer o contrato seguir em frente com suposições erradas.
  7. Ataques de flash loan: abusar de empréstimos instantâneos pode manipular mercados ou protocolos em uma única transação.
  8. Overflow e underflow de inteiros: erros matemáticos quando números ultrapassam seus limites podem levar a cálculos errados ou roubo.
  9. Aleatoriedade insegura: um RNG ruim e previsível pode ser explorado em jogos ou loterias.
  10. Ataques de negação de serviço (DoS): sobrecarregar contratos com operações que consomem muito gas para torná-los inutilizáveis.

Para os detalhes completos da OWASP, acesse a página do Smart Contract Top 10. E para dicas específicas de Ethereum, veja suas diretrizes de segurança.

12 boas práticas de segurança para smart contracts

Agora que você entende o cenário de ameaças, vamos explorar defesas práticas. Cada vulnerabilidade da lista da OWASP tem boas práticas correspondentes que já foram testadas em batalha em milhares de contratos.

As seções a seguir detalham essas falhas comuns uma a uma, com exemplos de código concretos mostrando tanto padrões vulneráveis quanto implementações seguras. Pense nisso como seu manual defensivo: técnicas diretas que, quando aplicadas de forma consistente, podem reduzir sua superfície de ataque.

1. Use delegatecall com cuidado

Delegatecall permite que um contrato execute código de outro usando seu próprio storage – muito útil para bibliotecas ou upgrades, mas arriscado porque pode levar a mudanças de estado inesperadas se o código chamado mexer nas suas variáveis. Está por trás de grandes exploits em que hackers injetam lógica maliciosa que sobrescreve estado crítico, como endereços de owner, ou drena fundos.

O perigo é que delegatecall preserva o contexto do contrato que faz a chamada (msg.sender, msg.value, storage), então o código externo roda com privilégios totais. Use isso apenas quando for absolutamente necessário, e garanta que os storage layouts sejam idênticos entre os contratos - slots desalinhados podem corromper seus dados.

Aqui está um exemplo vulnerável:

solidity
Copied
contract LibraryContract { address public owner; // Slot 0 function updateOwner() public { owner = msg.sender; // Changes caller's storage slot 0! } } contract VulnerableProxy { uint256 public value; // Slot 0 - MISMATCH! address public owner; // Slot 1 function delegateCall(address lib) public { // DANGER: updateOwner() will overwrite 'value', not 'owner'! lib.delegatecall(abi.encodeWithSignature("updateOwner()")); } }

Aqui está uma abordagem mais segura:

solidity
Copied
contract SecureProxy { address public implementation; address public owner; modifier onlyOwner() { require(msg.sender == owner, "Not owner"); _; } function execute(bytes memory data) public onlyOwner { (bool success,) = implementation.delegatecall(data); require(success, "Execution failed"); } }

Boas práticas: use padrões de proxy consolidados, como os contratos upgradeable da OpenZeppelin, mantenha storage layouts idênticos entre versões (nunca reordene ou altere tipos de variáveis), restrinja delegatecall apenas a contratos confiáveis e auditados, e considere usar bibliotecas com a keyword library, que usa delegatecall de forma segura por baixo dos panos, e nunca permita endereços controlados pelo usuário em delegatecall: isso é um vetor de takeover instantâneo.

2. Use um reentrancy guard

Reentrância ocorre quando uma chamada externa entrega o controle antes que sua função termine de atualizar o estado, permitindo que quem chamou volte a entrar e repita ações, como saques, antes que os saldos sejam atualizados. Está classificado em #5 na lista de riscos Web3 da OWASP e esteve por trás de alguns dos maiores hacks do cripto, como o famoso hack da DAO (perda de $60M).

Atacantes podem explorar essa janela de reentrância entre o envio de fundos e a atualização do estado para drenar contratos, chamando funções de saque de forma recursiva. Você deve sempre presumir que contratos externos são hostis.

Exemplo vulnerável:

solidity
Copied
contract VulnerableBank { mapping(address => uint256) public balances; function withdraw() public { uint256 bal = balances[msg.sender]; require(bal > 0); // DANGER: Sends Ether before updating balance! (bool sent,) = msg.sender.call{value: bal}(""); require(sent); balances[msg.sender] = 0; // Too late - already reentered! } } contract Attacker { VulnerableBank bank; fallback() external payable { if (address(bank).balance >= 1 ether) { bank.withdraw(); // Calls withdraw again! } } function attack() external payable { bank.withdraw(); // Starts the loop } }

Implementação segura:

solidity

`import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract SecureBank is ReentrancyGuard { mapping(address => uint256) public balances;

solidity
Copied
import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; contract SecureBank is ReentrancyGuard { mapping(address => uint256) public balances; function withdraw() public nonReentrant { uint256 bal = balances[msg.sender]; require(bal > 0); // Update state BEFORE external call balances[msg.sender] = 0; (bool sent,) = msg.sender.call{value: bal}(""); require(sent); } }

Boas práticas: use o modifier ReentrancyGuard da OpenZeppelin, siga o padrão Checks-Effects-Interactions (validar → atualizar estado → chamadas externas), e considere padrões pull-over-push em que os próprios usuários saquem os fundos.

3. Use msg.sender em vez de tx.origin para autenticação

A chamada tx.origin remonta ao iniciador original da transação, enquanto msg.sender se refere a quem chamou imediatamente; essa distinção importa muito para a segurança. Em cadeias de chamadas entre múltiplos contratos, tx.origin permanece constante, mas pode ser explorado por contratos intermediários maliciosos que enganam seu código, fazendo-o pensar que o usuário original está autorizado.

Um atacante pode criar um contrato de phishing que chama sua função, e, se você verificar tx.origin, ele aponta para a vítima que iniciou a transação, ignorando completamente sua autenticação. msg.sender é sempre quem chamou diretamente, o que o torna confiável para controle de acesso. Use tx.origin apenas nos raros casos em que você especificamente precisa do iniciador da transação, e nunca para autenticação. Essa vulnerabilidade está diretamente ligada a falhas de controle de acesso, o risco #1 da OWASP.

Exemplo vulnerável:

solidity
Copied
contract VulnerableWallet { address public owner; constructor() { owner = msg.sender; } function transfer(address payable to, uint256 amount) public { require(tx.origin == owner, "Not owner"); // BAD! to.transfer(amount); } } // Attacker tricks owner into calling this contract MaliciousContract { function attack(address wallet) public { // When owner calls this, tx.origin == owner// So the wallet thinks the call is authorized! VulnerableWallet(wallet).transfer(payable(msg.sender), 1 ether); } }

Implementação segura:

solidity
Copied
contract SecureWallet { address public owner; constructor() { owner = msg.sender; } function transfer(address payable to, uint256 amount) public { require(msg.sender == owner, "Not owner"); // GOOD! to.transfer(amount); } }

Por que isso importa: se um owner acidentalmente interagir com um contrato malicioso (clicando em um link de phishing, usando um app comprometido), esse contrato pode drenar a carteira vulnerável porque tx.origin ainda aponta para o owner. Com msg.sender, apenas o próprio endereço do owner pode autorizar transferências. Garanta sempre usar msg.sender para autenticação e verificações de controle de acesso.

4. Use corretamente os modificadores de visibilidade do Solidity

Modificadores de visibilidade definem os limites de acesso: public (chamável por qualquer um, interna ou externamente), external (apenas de fora do contrato), internal (esse contrato mais contratos que herdam dele), e private (estritamente esse contrato). Em versões mais antigas do Solidity, omitir um modificador resultava em public por padrão, expondo acidentalmente funções sensíveis a atacantes externos.

Visibilidade incorreta já levou a exploits em que hackers chamaram funções administrativas ou manipularam variáveis de estado críticas que deveriam ter sido restritas. Sempre declare a visibilidade explicitamente: é ao mesmo tempo uma prática de segurança e um ganho de legibilidade.

solidity

`contract VulnerableBank { mapping(address => uint) balances;

solidity
Copied
contract VulnerableBank { mapping(address => uint) balances; // BAD: accidentally public in old Solidity function resetBalance(address user) { balances[user] = 0; // anyone can call this! } // GOOD: explicit visibility function deposit() external payable { balances[msg.sender] += msg.value; } function _updateInternal() internal { // only this contract + children } }

Use ferramentas de análise estática como o Slither para detectar modificadores de visibilidade ausentes ou incorretos antes da implantação.

5. Evite a manipulação de block timestamp

Validadores podem manipular block.timestamp dentro de uma pequena janela (cerca de ±15 segundos na Ethereum), tornando-o pouco confiável para timing preciso ou como fonte de aleatoriedade. Miners ou validadores podem ajustar timestamps a seu favor, disparando pagamentos, ganhando loterias ou contornando verificações baseadas em tempo.

Por isso, nunca use block.timestamp para lógica crítica em que segundos importam, e jamais o use para gerar números aleatórios. Ele é adequado para estimativas de tempo aproximadas (como "já se passaram 24 horas?"), mas perigoso para condições exatas.

Exemplo vulnerável:

solidity
Copied
contract TimestampLottery { uint public lastPlay; fallback() external payable { require(msg.value == 10 ether); require(block.timestamp != lastPlay); lastPlay = block.timestamp; // BAD: validator can manipulate timestamp to win if (block.timestamp % 15 == 0) { payable(msg.sender).transfer(address(this).balance); } } }

Abordagem melhor para timing:

solidity
Copied
contract BlockBasedTiming { uint public startBlock = block.number; // Use block numbers instead (~12s per block on Ethereum) function isTimePassed(uint blocks) public view returns (bool) { return block.number >= startBlock + blocks; } }

Para aleatoriedade, nunca crie sua própria solução. Em vez disso, use o Chainlink VRF ou oráculos de função aleatória verificável similares, que fornecem aleatoriedade criptograficamente segura e à prova de manipulação.

6. Evite overflow e underflow aritmético

Em versões do Solidity anteriores à 0.8, inteiros dão a volta silenciosamente quando excedem seus valores máximo ou mínimo. Ou seja, um uint8 em 255 incrementa para 0, ou 0 decrementa para 255.

Isso já causou exploits catastróficos em que atacantes cunharam tokens infinitos, criaram saldos negativos que davam a volta para valores enormes, ou contornaram verificações críticas.

Exemplo vulnerável (pré-0.8):

solidity
Copied
pragma solidity 0.7.0; contract UnsafeToken { mapping(address => uint8) public balances; function transfer(address to, uint8 amount) public { balances[msg.sender] -= amount; // Underflow: 0 - 1 = 255 balances[to] += amount; // Overflow: 255 + 1 = 0 } }

Faça upgrade para Solidity >=0.8 para proteção automática contra overflow:

solidity
Copied
pragma solidity ^0.8.0; contract SafeToken { mapping(address => uint8) public balances; function transfer(address to, uint8 amount) public { balances[msg.sender] -= amount; // Reverts on underflow balances[to] += amount; // Reverts on overflow } }

Se estiver preso a uma versão mais antiga do Solidity, use a biblioteca SafeMath da OpenZeppelin. Na 0.8+, as verificações já são embutidas e revertem automaticamente, mas você pode usar blocos unchecked \{\} quando quiser intencionalmente um comportamento de wrapping para otimização de gas.

7. Implemente um controle de acesso robusto

Controle de acesso quebrado permite que usuários não autorizados executem funções privilegiadas; é a vulnerabilidade #1 na Web3 Top 10 da OWASP e causou mais de $1,6 bilhão em perdas somente no primeiro semestre de 2025. Sem as proteções adequadas, atacantes podem drenar fundos, cunhar tokens, pausar contratos ou mudar a ownership.

Erros comuns incluem modificadores ausentes, depender de tx.origin para autenticação, ou usar verificações simples de require\(msg.sender == owner\) que acabam esquecidas durante upgrades. A solução é o controle de acesso baseado em papéis (role-based) com o princípio do menor privilégio: dê a cada endereço apenas as permissões que ele realmente precisa.

Implementação segura com OpenZeppelin:

solidity
Copied
pragma solidity ^0.8.20; import "@openzeppelin/contracts/access/AccessControl.sol"; contract SecureVault is AccessControl { bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); constructor() { _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); _grantRole(ADMIN_ROLE, msg.sender); } function withdrawFunds() public onlyRole(ADMIN_ROLE) { // Only admins can withdraw } function updateConfig() public onlyRole(OPERATOR_ROLE) { // Operators handle config, not full admin } }

Audite regularmente as atribuições de papéis, implemente ações administrativas com atraso de tempo para mudanças de alto risco, e use carteiras multi-sig para papéis críticos. Nunca use tx.origin para autenticação. Aproveite bibliotecas já testadas em batalha, como o OpenZeppelin AccessControl, em vez de criar a sua própria solução.

8. Proteja integrações com oracles contra manipulação

Oracles unem a blockchain a dados do mundo real, mas se forem centralizados ou facilmente manipuláveis, atacantes podem alimentar informações falsas para explorar seus contratos. Esse tipo de exploit está classificado em #2 nos riscos Web3 da OWASP e já drenou centenas de milhões de protocolos DeFi.

Ataques de flash loan frequentemente manipulam price oracles onchain (como usar uma única DEX como fonte de preço), permitindo que hackers inflem ou derrubem preços artificialmente para liquidar posições, drenar liquidity pools ou cunhar empréstimos subcolateralizados. Nunca dependa de uma única fonte de preço ou de preços spot que possam ser manipulados dentro de uma única transação.

Uso seguro de oracle com Chainlink:

solidity
Copied
pragma solidity ^0.8.20; import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; contract SecurePriceFeed { AggregatorV3Interface internal priceFeed; uint256 private constant STALENESS_THRESHOLD = 3600; // 1 hour constructor(address _aggregator) { priceFeed = AggregatorV3Interface(_aggregator); } function getLatestPrice() public view returns (int) { ( uint80 roundId, int price, , uint256 updatedAt, uint80 answeredInRound ) = priceFeed.latestRoundData(); require(price > 0, "Invalid price"); require(answeredInRound >= roundId, "Stale price"); require(block.timestamp - updatedAt < STALENESS_THRESHOLD, "Price too old"); return price; } }

Boas práticas: use redes de oracles descentralizadas como a Chainlink com múltiplas fontes de dados, implemente Time-Weighted Average Prices (TWAPs) para operações críticas, valide a atualidade e verifique os limites dos preços, e agregue múltiplos oracles quando possível. Consulte a documentação da Chainlink para feeds específicos de cada rede e considerações de segurança.

9. Valide todas as entradas cuidadosamente

Deixar de validar entradas do usuário permite que atacantes injetem dados maliciosos, disparem comportamentos inesperados ou explorem casos extremos no código. É a #4 nas vulnerabilidades Web3 da OWASP e um vetor comum para drenar fundos ou quebrar a lógica do contrato.

Sem verificações adequadas, usuários podem passar valores zero para burlar taxas, valores negativos para explorar aritmética, endereços apontando para zero ou contratos maliciosos, ou índices de array que causam acesso fora dos limites. Toda entrada externa é não confiável até que se prove segura. Defesa em profundidade significa validar em cada fronteira: verificar intervalos, valores nulos, tamanhos de arrays e restrições de lógica de negócio antes de processar qualquer dado fornecido pelo usuário.

Validação de entrada adequada:

solidity
Copied
pragma solidity ^0.8.20; contract SecureDeposit { mapping(address => uint256) public balances; uint256 public constant MAX_DEPOSIT = 1000 ether; function deposit(uint256 amount) public payable { require(amount > 0, "Amount must be positive"); require(amount == msg.value, "Amount mismatch"); require(amount <= MAX_DEPOSIT, "Exceeds max deposit"); require( amount <= type(uint256).max - balances[msg.sender], "Balance overflow" ); balances[msg.sender] += amount; } function transfer(address to, uint256 amount) public { require(to != address(0), "Invalid recipient"); require(to != address(this), "Cannot transfer to contract"); require(amount <= balances[msg.sender], "Insufficient balance"); balances[msg.sender] -= amount; balances[to] += amount; } }

}`

Sempre valide se os valores são diferentes de zero e estão dentro dos limites, se os endereços não são nulos ou inválidos, se os índices de array estão dentro do intervalo, e se as restrições de lógica de negócio são atendidas.

Use custom errors no Solidity 0.8.4+ para reverts eficientes em gas com mensagens detalhadas.

10. Trate chamadas externas com segurança

Chamadas externas para outros contratos podem falhar silenciosamente se você não verificar seus valores de retorno. Esse problema é #6 nos riscos Web3 da OWASP e já levou a fundos ficarem presos, ou a contratos assumirem sucesso quando as operações na verdade falharam.

Chamadas de baixo nível como call\(\), delegatecall\(\) e send\(\) retornam indicadores booleanos de sucesso em vez de reverter automaticamente. Se você ignorar esses valores de retorno, seu contrato pode continuar executando com suposições falsas, pensando que fundos foram transferidos quando não foram, ou que uma operação crítica foi concluída quando na verdade falhou. O transfer\(\) do ERC-20 também tem esse problema em algumas implementações que retornam false em vez de reverter.

Padrão vulnerável:

solidity
Copied
function unsafeTransfer(address payable recipient) public { recipient.send(1 ether); // Returns false on failure, but continues!// Contract thinks transfer succeeded }

Implementação segura:

solidity
Copied
contract SecureCaller { event CallExecuted(address target, bool success); function safeCall(address target, bytes memory data) public returns (bytes memory) { (bool success, bytes memory result) = target.call(data); require(success, "External call failed"); emit CallExecuted(target, success); return result; } function safeTransfer(address payable recipient, uint256 amount) public { (bool success, ) = recipient.call{value: amount}(""); require(success, "Transfer failed"); } // For ERC-20 tokens, use SafeERC20 from OpenZeppelin }

}`

Sempre capture e verifique os valores de retorno de chamadas externas. Para transferências de Ether, prefira call\{value: x\}\(""\) em vez do já descontinuado send\(\) ou transfer\(\). Para transferências de token, use o wrapper SafeERC20 da OpenZeppelin, que lida com implementações não padronizadas de ERC-20 que não revertem em caso de falha.

11. Mitigue ataques de flash loan

Flash loans permitem tomar emprestadas quantias enormes sem garantia, desde que você pague de volta dentro da mesma transação. Atacantes podem explorar essa lógica para manipular preços, drenar pools ou explorar a lógica do protocolo, o que faz disso o #7 nos riscos Web3 da OWASP, com bilhões perdidos em todo o DeFi.

Hackers usam capital de flash loan para distorcer artificialmente preços de oracle, explorar erros de arredondamento em escala, ou criar condições de mercado temporárias que disparam a lógica vulnerável do contrato. Um padrão clássico que se repete em hacks: pegar emprestado milhões, manipular um price oracle ou liquidity pool, explorar a distorção de preço no seu protocolo, pagar o empréstimo e embolsar a diferença – tudo isso de forma atômica em uma única transação.

Padrão vulnerável:

solidity
Copied
contract VulnerablePool { function getPrice() public view returns (uint) { // BAD: using spot price that can be manipulated return tokenReserve / ethReserve; } function borrow(uint amount) public { uint collateral = getPrice() * userCollateral[msg.sender]; require(collateral >= amount, "Insufficient collateral"); // Attacker manipulates price in same transaction! } }

Abordagem melhor:

solidity
Copied
contract SecureLending { mapping(address => uint256) public lastBorrow; function borrow(uint256 amount) public { // Add cooldown between borrows require(block.number > lastBorrow[msg.sender] + 2, "Too soon"); lastBorrow[msg.sender] = block.number; // Use TWAP or Chainlink instead of spot price uint256 price = chainlinkOracle.getPrice(); uint256 collateral = price * userCollateral[msg.sender]; require(collateral * 150 / 100 >= amount, "Need 150% collateral"); } }

Estratégias de defesa: use Time Weighted Average Prices (TWAPs) ou Chainlink em vez de preços spot. Adicione cooldowns de vários blocos para operações críticas, exija sobrecolateralização e verifique a saúde do seu protocolo após mudanças de estado. Se você não precisa de flash loans, considere bloqueá-los completamente.

12. Evite erros de lógica em funções críticas

Erros de lógica em funções críticas quebram as premissas de segurança fundamentais do seu contrato e podem ser catastróficos. Essa categoria de erro é a #3 nos riscos Web3 da OWASP porque essas falhas comprometem o sistema inteiro apesar de serem, tecnicamente, código "correto".

Diferente de bugs de sintaxe detectados pelo compilador, erros de lógica passam por todas as verificações, mas produzem resultados errados: erros off-by-one em loops, ordem incorreta de operações, tratamento ausente de casos extremos, ou condições falhas. Exemplos clássicos incluem esquecer de atualizar saldos após transferências, usar &gt;= em vez de &gt;, ou calcular taxas na ordem errada, causando perda de precisão.

Erros de lógica comuns:

solidity
Copied
contract LogicErrors { mapping(address => uint256) public balances; // ERROR 1: Balance updated AFTER transfer (reentrancy risk!) function withdraw(uint256 amount) public { payable(msg.sender).transfer(amount); balances[msg.sender] -= amount; // Too late! } // ERROR 2: Off-by-one lets loop access invalid index function distribute(address[] memory users) public { for(uint i = 0; i <= users.length; i++) { // Should be // Crashes on last iteration! } } }

Versão corrigida:

solidity
Copied
contract SecureLogic { mapping(address => uint256) public balances; function withdraw(uint256 amount) public { require(balances[msg.sender] >= amount, "Insufficient balance"); // Update state BEFORE external call balances[msg.sender] -= amount; payable(msg.sender).transfer(amount); } function distribute(address[] memory users) public { for(uint i = 0; i < users.length; i++) { // Correct boundary// Safe iteration } } }

Estratégias de defesa: siga o padrão checks-effects-interactions (validar → atualizar estado → chamadas externas), escreva testes unitários para casos extremos (zero, valores máximos, arrays vazios), use fuzzing com o Foundry para testar entradas aleatórias, e defina invariantes como "o total distribuído nunca deve exceder o saldo do pool". Sempre faça revisões por pares em funções críticas.

6 ferramentas populares de segurança para smart contracts

As boas práticas acima ajudam a aumentar a resiliência, mas você também precisa de ferramentas capazes de detectar erros que o olho pode não perceber. Aqui está uma mistura de ferramentas clássicas e modernas para auditar seu código:

  1. Slither: um analisador estático com mais de 40 detectores de falhas. Ótimo para varreduras rápidas e imprime detalhes do contrato. GitHub.
  2. Mythril: um analisador de bytecode EVM para múltiplas chains que consegue identificar problemas simbólicos. Faz parte do MythX. GitHub.
  3. Securify: um scanner apoiado pela Ethereum Foundation para mais de 37 falhas, com análise estática precisa. Website (não disponível mais).
  4. Foundry: um framework moderno de testes/fuzzing para Solidity que oferece testes de invariantes, ideal para bugs de lógica. Book.
  5. Certora: uma ferramenta de verificação formal que prova matematicamente que o código corresponde às especificações. Website.
  6. Echidna: um fuzzer baseado em propriedades para vulnerabilidades. GitHub.

Além dessas 6 ferramentas, você também pode fazer parceria com plataformas como a Code4rena para auditorias com recompensas.

Proteja seus smart contracts: considerações finais

Como dev, seu objetivo é simples: construir contratos que não sejam hackeados, mantendo os fundos dos usuários seguros e seu app funcionando bem. Nesse processo, você deve adotar as boas práticas acima e abraçar a mentalidade de ser seguro por design.

Use proxies atualizáveis (via OpenZeppelin Upgrades), código modular e account abstraction (como o ERC-4337 para melhor UX/segurança – veja nosso guia sobre ERC-4337 para mais detalhes). Rode testes unitários, verificações formais, auditorias profissionais, monitoramento em tempo de execução (por exemplo, via Fortress), e bug bounties na Immunefi. Seus usuários (e o sucesso do seu projeto) dependem disso.

Para mais recursos sobre segurança de contratos, confira o seguinte:

Perguntas frequentes

Quais são os padrões de segurança mais importantes a seguir ao escrever smart contracts em Solidity?

Os padrões mais críticos incluem usar checks-effects-interactions, implementar reentrancy guards, validar todas as entradas, definir modificadores de visibilidade explícitos e seguir um controle de acesso adequado com permissões baseadas em papéis.

Como posso prevenir ataques de reentrância nos meus contratos?

Use o modifier ReentrancyGuard da OpenZeppelin, siga o padrão checks-effects-interactions atualizando o estado do contrato antes de fazer chamadas externas, e considere padrões pull-over-push em que os próprios usuários saquem os fundos.

Por que devo usar msg.sender em vez de tx.origin para autenticação?

tx.origin pode ser explorado por contratos intermediários maliciosos que enganam seu código, fazendo-o pensar que o usuário original está autorizado, enquanto msg.sender sempre se refere a quem chamou diretamente, o que o torna confiável para controle de acesso.

Como me proteger contra overflow e underflow aritmético no Solidity?

Faça upgrade para Solidity 0.8.x ou posterior, que tem verificações de overflow/underflow embutidas que revertem automaticamente em caso de erro. Para versões mais antigas, use bibliotecas já testadas como o SafeMath da OpenZeppelin.

Qual o papel de testes e auditorias de segurança na segurança de smart contracts?

Testes unitários abrangentes, fuzzing com ferramentas como o Foundry, e análise estática com o Slither ajudam a descobrir casos extremos e vulnerabilidades, enquanto auditorias de segurança independentes identificam falhas de lógica antes da implantação na mainnet.

Como devo lidar com delegatecall de forma segura no Solidity?

Use delegatecall apenas com implementações confiáveis e auditadas, nunca com endereços controlados pelo usuário, mantenha storage layouts idênticos entre os contratos, e prefira padrões de proxy consolidados, como os contratos upgradeable da OpenZeppelin.

Como posso proteger meus smart contracts contra ataques de flash loan?

Use Time-Weighted Average Prices (TWAPs) ou oracles da Chainlink em vez de preços spot, implemente cooldowns de vários blocos para operações críticas, exija sobrecolateralização e verifique a saúde do protocolo após mudanças de estado.

Quais ferramentas devo usar para testes de segurança de smart contracts?

Ferramentas populares incluem o Slither para análise estática, o Foundry para testes e fuzzing, o Mythril para análise de bytecode, e o Securify para varredura abrangente de vulnerabilidades antes da implantação.

Background gradient

Construa magia blockchain

A Alchemy combina os produtos e ferramentas de desenvolvimento Web3 mais poderosos com recursos, comunidade e suporte lendário.