Esse artigo é uma tradução da equipe Secureum feita por Rodrigo Faria. Você pode encontrar o artigo original aqui.
O Manifesto da Checklist
“Checklist - Como fazer as coisas benfeitas” é um livro de Atul Gawande, um notável cirurgião, escritor e líder em saúde pública. Em sua resenha deste livro, Malcolm Gladwell escreve que:
“Gawande começa fazendo uma distinção entre erros de ignorância (erros que cometemos porque não sabemos o suficiente) e erros de inépcia (erros que cometemos porque não fazemos bom uso do que sabemos). O fracasso no mundo moderno, ele escreve, é realmente por causa do segundo desses erros, e ele nos conduz através de uma série de exemplos da medicina mostrando como as tarefas rotineiras dos cirurgiões se tornaram tão incrivelmente complicadas que erros de um tipo ou de outro são virtualmente inevitáveis: é muito fácil para um médico competente perder um passo, ou esquecer de fazer uma pergunta importante ou, no estresse e pressão do momento, deixar de planejar adequadamente para cada eventualidade. Gawande então visita os pilotos e as pessoas que constroem arranha-céus e volta com uma solução. Os especialistas precisam de listas de verificação (checklists) – guias literalmente escritos que os orientam nas principais etapas de qualquer procedimento complexo. Na última seção do livro, Gawande mostra como sua equipe de pesquisa pegou essa ideia, desenvolveu uma checklist de cirurgia segura e a aplicou em todo o mundo, com sucesso impressionante.”
Dadas as complexidades incompreensíveis da infraestrutura Ethereum em rápida evolução (novas plataformas, novas linguagens, novas ferramentas e novos protocolos) e os riscos associados à implantação de smart contracts gerenciando milhões de dólares, acredito que o trabalho dos desenvolvedores e auditores de smart contracts é um pouco análogo ao dos cirurgiões/pilotos/arquitetos de arranha-céus mencionados acima.
Embora possa não haver vidas em risco (ainda), há tantas coisas para acertar com smart contracts que é fácil perder algumas verificações, fazer suposições incorretas ou deixar de considerar possíveis situações. O resultado é uma exploração que drena os fundos do smart contract, o que reduz a confiança das pessoas nessa infraestrutura descentralizada e trustless(não baseada em confiança) do futuro. Especialistas em smart contracts, portanto, também precisam de listas de verificação.
Esta postagem compila uma checklist de várias fontes de 101 armadilhas de segurança de smart contracts e práticas recomendadas. Não exaustiva de forma alguma, esta lista inclui recomendações de fontes amplamente referenciadas, como a documentação do detector Slither, do Registro de Classificação de Fraquezas de Contrato Inteligente (Smart Contract Weakness Classification Registry), da lista de bugs conhecidos do Solidity, do Sigma Prime, de ataques conhecidos e melhores práticas da ConsenSys, do DASP, e verificações de atualização do OpenZeppelin e do Trail de Bits.
A checklist descreve brevemente as armadilhas/recomendações sem explicar o raciocínio em detalhes e assume que o leitor já sabe disso ou fará referência às fontes listadas. Acredito que este útil “checklist de smart contract seguro” será útil para desenvolvedores/auditores na construção de smart contracts mais seguros e robustos com Solidity em Ethereum.
Checklist de segurança de smart contract
- Versões do Solidity: O uso de versões muito antigas do Solidity impede os benefícios de correções de bugs e verificações mais recentes de segurança. O uso das versões mais recentes pode tornar os contratos suscetíveis a erros não descobertos do compilador. Considere usar uma destas versões: 0.5.11-0.5.13, 0.5.15-0.5.17, 0.6.8 ou 0.6.10-0.6.11. (veja aqui)
-
Pragma desbloqueado: Os contratos devem ser implantados usando a mesma versão/flags do compilador com os quais foram testados. Bloquear o pragma (por exemplo, não usando "^" no
pragma solidity 0.5.10
) garante que os contratos não sejam implantados acidentalmente usando uma versão mais antiga do compilador com bugs não corrigidos. (veja aqui) - Múltiplos pragma Solidity: É melhor usar uma versão única do compilador Solidity em todos os contratos ao invés de versões diferentes com diferentes bugs e verificações de segurança. (veja aqui)
- Controle de acesso incorreto: As funções do contrato que executam lógica crítica devem ter o controle de acesso apropriado aplicado por meio de verificações do endereço (por exemplo, proprietário, controlador etc.) normalmente nos modificadores (modifiers). A ausência de checagens permite que os invasores controlem esta lógica crítica (veja aqui e aqui).
- Funções desprotegidas de retirada (withdraw): Chamadas de funções desprotegidas (externas/públicas) que enviem Ether/tokens para endereços controlados pelo usuário podem permitir que usuários façam retiradas não autorizadas de fundos (veja aqui).
- Funções desprotegidas de autodestruição (selfdestruct): Um usuário/invasor pode destruir o contrato por engano, ou intencionalmente. Proteja o acesso a tais funções (veja aqui).
- Efeitos colaterais de modificadores: Modificadores devem apenas implementar verificações e não fazer mudanças de estado e chamadas externas que violem o padrão verificação-efeitos-interações (checks-effects-interactions). Esses efeitos colaterais podem passar despercebidos pelos desenvolvedores/auditores porque o código do modificador geralmente está longe da implementação da função (veja aqui).
- Modificador incorreto: Se um modificador não executar _ ou revert, a função que usa esse modificador retornará o valor padrão, causando um comportamento inesperado (veja aqui).
-
Nomes de construtores: Antes do solc 0.4.22, os nomes dos construtores tinham que possuir o mesmo nome da classe do contrato que o continha. Nomeá-los incorretamente faria com que não fossem considerados construtores, o que tem implicações de segurança. O solc 0.4.22 introduziu a palavra-chave
constructor
. Até o solc 0.5.0, os contratos podiam ter nomes de construtores no estilo antigo e no novo, com o primeiro definido tendo precedência sobre o segundo se ambos existissem, o que também levava a problemas de segurança. O solc 0.5.0 passou a forçar o uso da palavra-chaveconstrutor
(veja aqui e aqui). - Construtor Void: Chamadas para construtores do contrato base que não são implementados levam a suposições equivocadas. Verifique se o construtor está implementado ou remova a chamada se ele não estiver (veja aqui).
-
Verificação do construtor implícito callValue: O código de criação de um contrato que não define um construtor, mas que possui um contrato base que o define, não reverteu para chamadas com
callValue
diferente de zero quando tal construtor não era explicitamente pagável. Isso se deve a um bug do compilador introduzido na v0.4.5 e corrigido na v0.6.8 (veja aqui). -
Função delegatecall controlada: chamadas a
delegatecall()
oucallcode()
para um endereço controlado pelo usuário permitem a execução de contratos maliciosos no contexto do estado do chamador. Garanta endereços de destino confiáveis para essas chamadas (veja aqui). - Vulnerabilidades de reentrância: Chamadas externas não confiáveis ao contrato podem gerar callbacks levando a resultados inesperados, como por exemplo várias retiradas ou eventos fora de ordem. Use o padrão check-effects-interactions ou guardas de reentrância (reentrancy guards) (veja aqui).
- Callbacks ERC777 e reentrância: Tokens ERC777 permitem callbacks arbitrários através de hooks que são chamados durante as transferências de token. Endereços de contrato maliciosos podem causar reentrada em tais retornos de chamada se as proteções de reentrada não forem usadas (veja aqui).
-
Evite o uso de transfer()/send() como mitigações de reentrância: Embora
transfer()
esend()
tenham sido recomendados como melhores práticas de segurança para evitar ataques de reentrância porque eles apenas repassam 2300 de gas, a reprecificação de gas de opcodes pode quebrar contratos implantados. Ao invés disso, usecall()
, sem limites fixos de gas e com o padrão checks-effects-interactions ou com guardas de reentrância para proteção de reentrância (veja aqui e aqui). - Dados privados na blockchain: Marcar variáveis como privadas não significa que elas não possam ser lidas na blockchain. Dados privados não devem ser armazenados sem criptografia no código ou no estado do contrato, mas armazenados criptografados ou fora da blockchain (veja aqui).
-
PRNG Fraco: PRNG (pseudorandom number generator - gerador de número pseudo-randômico) baseado em
block.timestamp
,now
ou na blockhash pode ser influenciado por mineradores até certo ponto e deve ser evitado (veja aqui). -
Valores de bloco como proxies de tempo:
block.timestamp
eblock.number
não são bons proxies para tempo devido a problemas com sincronização, manipulação de mineradores e alteração de tempos de bloco. (veja aqui) - Integer overflow/underflow: Não usar o
SafeMath
do OpenZeppelin (ou bibliotecas semelhantes) que verificam overflows/underflows pode levar a vulnerabilidades ou comportamento inesperado se o usuário/invasor puder controlar os operandos inteiros de tais operações aritméticas. O solc v0.8.0 introduziu verificações padrão de estouro/subfluxo para todas as operações aritméticas. (veja aqui e aqui). - Dividir antes de multiplicar: Executar a multiplicação antes da divisão é geralmente melhor para evitar perda de precisão porque a divisão inteira do Solidity pode truncar (veja aqui).
-
Dependência da ordem da transação: As condições de corrida (race conditions) podem ser forçadas em transações específicas do Ethereum monitorando o mempool. Por exemplo, a clássica alteração da função ERC20
accept()
pode ser executada antecipadamente (front-run) usando esse método. Não faça suposições sobre a dependência de ordem da transação (veja aqui). -
Condição de corrida na função ERC20 approve(): Use
safeIncreaseAllowance()
esafeDecreaseAllowance()
da implementação SafeERC20 do OpenZeppelin para evitar que condições de corrida manipulem os valores de permissão (allowance) (veja aqui). -
Maleabilidade da assinatura: A função
ecrecover()
é suscetível à maleabilidade da assinatura, o que pode levar a ataques de repetição (replay attacks). Considere usar a biblioteca ECDSA da OpenZeppelin (veja aqui, aqui e aqui). - A função ERC20 transfer() não retorna boolean: Contratos compilados com solc > 0.4.22 interagindo com tais funções irão reverter. Use os wrappers SafeERC20 do OpenZeppelin (veja aqui e aqui).
-
Valores de retorno incorretos na função ERC721 ownerOf(): Os contratos compilados com solc > 0.4.22 interagindo com a função ERC721
ownerOf()
que retornem um bool em vez de um tipoaddress
serão revertidos. Use os contratos ERC721 do OpenZeppelin (veja aqui). -
Ether e this.balance inesperados: Um contrato pode receber Ether através de funções "pagáveis" (payable), funções
selfdestruct(),
transações coinbase ou pré-envio (pre-sent) antes da criação. Lógicas de contrato que dependam dethis.balance
, portanto, podem ser manipuladas (veja aqui e aqui). -
fallback vs receive(): Verifique se todas as precauções e sutilezas das funções
fallback
/receive
relacionadas à visibilidade, mutabilidade de estado e transferências Ether foram consideradas (veja aqui e aqui). -
Perigo da igualdade estrita: O uso de comparações de igualdade estrita com tokens/Ether pode causar acidentalmente/maliciosamente um comportamento inesperado. Considere usar
>=
ou<=
em vez de==
para essas variáveis, dependendo da lógica do contrato (veja aqui). -
Ether bloqueado: Contratos que aceitam Ether através de funções pagáveis (payable), mas sem mecanismos de retirada, bloquearão esse Ether. Remova o atributo
payable
ou adicione a função de retirada (veja aqui). -
Uso perigoso de tx.origin: O uso de
tx.origin
para autorização pode ser abusado por um contrato malicioso realizando ataque MITM (man-in-the-middle) ao encaminhar chamadas do usuário legítimo que interage com ele. Usemsg.sender
ao invés disso (veja aqui). -
Validação de contrato: a verificação de se uma chamada foi feita de uma conta de propriedade externa (EOA - Externally Owned Account) ou de uma conta de contrato, normalmente, é feita usando a validação de
extcodesize
, que pode ser contornada por um contrato durante a construção quando não há código-fonte disponível. Verificar setx.origin == msg.sender
é outra opção. Ambos têm implicações que precisam ser consideradas (veja aqui). -
Deletando um mapeamento dentro de um struct: Excluir um
struct
que contém uma estrutura de dados de mapping não excluirá o mapping, o que pode levar a consequências não intencionais (veja aqui). -
Tautologia ou contradição: Tautologias (sempre verdadeiras) ou contradições (sempre falsas) indicam potenciais falhas de lógica ou verificações redundantes. Por exemplo,
x >= 0
sempre é verdadeiro sex
foruint
(veja aqui). -
Constante booleana: O uso de constantes booleanas (
true
/false
) no código (por exemplo, condicionais) é indicativo de lógica falha (veja aqui). -
Igualdade booleana: As variáveis booleanas podem ser verificadas dentro de condicionais diretamente sem o uso de operadores de igualdade para
true
/false
(veja aqui). -
Funções que modificam estado: Funções que modificam o estado (em assembly ou não) mas são rotuladas como
constant
/pure
/view
revertem em solc >=0.5.0 (funciona em versões anteriores) devido ao uso do opcodeSTATICCALL
(veja aqui). -
Valores de retorno de chamadas baixo nível (low-level): Certifique-se de que os valores de retorno de chamadas low-level (
call
/callcode
/delegatecall
/send
/ etc.) sejam verificados para evitar falhas inesperadas (veja aqui). -
Verificação de existência de conta para chamadas low-level: Chamadas low-level
call
/delegatecall
/staticcall
retornamtrue
mesmo se a conta chamada não existir (por design EVM). A existência da conta deve ser verificada antes da chamada, se necessário (veja aqui). -
O perigo do shadowing (sombreamento): Variáveis locais, variáveis de estado, funções, modificadores ou eventos com nomes que "sombreiam" (ou seja, substituem/fazem shadow) em símbolos built-in de Solidity, por exemplo, declarações
now
ou outras declaraçõesdo escopo atual são enganosas e podem levar a usos e comportamentos inesperados (veja aqui). - O perigo do shadowing de variáveis de estado: O shadowing de variáveis de estado em contratos derivados pode ser perigoso para variáveis críticas, como "proprietário do contrato" (por exemplo, quando os modificadores nos contratos base verificam as variáveis base, mas variáveis shadow são definidas erroneamente) e os contratos usam incorretamente as variáveis base/shadowed. Não faça shadow de variáveis de estado (veja aqui).
- Uso de pré-declaração de variáveis locais: O uso de uma variável antes de sua declaração (declarada posteriormente ou em outro escopo) leva a um comportamento inesperado em solc < 0.5.0, mas solc >= 0.5.0 implementa regras de escopo no estilo C99, em que variáveis só podem ser usadas depois de serem declaradas e apenas no mesmo escopo ou em escopos aninhados (veja aqui).
- Operações custosas (caras) dentro de um loop: Operações como atualizações de variáveis de estado (use SSTOREs) dentro de um loop custam muito gas, são caras e podem levar a erros de falta de gas. Otimizações usando variáveis locais são preferidas (veja aqui).
- Chamadas dentro de um loop: Chamadas para contratos externos dentro de um loop são perigosas (especialmente se o índice do loop puder ser controlado pelo usuário) porque pode levar a DoS (Denial of Service) se uma das chamadas for revertida ou a execução ficar sem gas. Evite chamadas dentro de loops, verifique se o índice de loop não pode ser controlado pelo usuário ou se está limitado (veja aqui).
- DoS com limite de gas de bloco: Padrões de programação como loop sobre arrays de tamanho desconhecido podem levar a DoS quando o custo de execução do gas excede o limite de gas de bloco (veja aqui).
- Eventos faltantes: Mudanças de estado críticas (por exemplo, mudança de proprietário e de outros parâmetros críticos) devem ser feitas via emissão de eventos que sejam rastreadas off-chain (veja aqui).
- Parâmetros de eventos não indexados: Espera-se que os parâmetros de certos eventos sejam indexados (por exemplo, eventos de transferência/aprovação ERC20) para que sejam incluídos no filtro bloom do bloco para acesso mais rápido. Uma falha em fazer isso pode confundir as ferramentas off-chain que procuram esses eventos indexados (veja aqui).
-
Assinatura de evento incorreta em bibliotecas: Tipos de contrato usados em eventos em bibliotecas causam um hash incorreto da assinatura do evento. Ao invés de usar o tipo
address
no hash da assinatura, o nome real do contrato foi usado, levando a um hash errado nos logs. Isso se deve a um bug do compilador introduzido na v0.5.0 e corrigido na v0.5.8 (veja aqui). - Expressões unárias perigosas: Expressões unárias como x =+ 1 são prováveis erros, em que o programador realmente pretendia usar x += 1. O operador unário "+" foi depreciado no solc v0.5.0 (veja aqui).
- Ausência de validação de endereço zero: Setters de parâmetros de tipo address devem incluir uma verificação de endereço zero (zero-address), caso contrário, a funcionalidade do contrato pode se tornar inacessível ou os tokens queimados para sempre (veja aqui).
- Mudanças críticas de endereço: A alteração de endereços críticos em contratos deve ser um processo de duas etapas, em que a primeira transação (da mudança do endereço antigo para atual) registra o novo endereço (ou seja, concede a propriedade) e a segunda transação (do novo endereço) substitui o endereço antigo pelo novo (ou seja, reivindica a propriedade). Isso oferece a oportunidade de recuperar endereços incorretos usados erroneamente na primeira etapa. Caso contrário, a funcionalidade do contrato pode ficar inacessível (veja aqui e aqui).
-
Mudanças de estado em assert(): Invariantes em declarações
assert()
não devem modificar o estado, de acordo com as melhores práticas.require()
deve ser usado para tais verificações (veja aqui). -
require() vs assert():
require()
deve ser usado para verificar condições de erro em dados de entrada e valores de retorno, enquantoassert()
deve ser usado para checagens invariáveis. Entre as versões solc 0.4.10 e 0.8.0,require()
usava o opcode REVERT (0xfd) que devolvia o gas restante em caso de falha, enquantoassert()
usava o opcode INVALID (0xfe) que consumia todo o gas fornecido (veja aqui). -
Palavras-chave obsoletas: Uso de funções/operadores obsoletos, como
block.blockhash()
parablockhash(),
msg.gas
paragasleft(),
throw
pararevert(), sha3()
parakeccak256(),
callcode()
paradelegatecall(),
suicida()
paraselfdestruct(),
constant
paraview
ouvar
para o nome real do tipo devem ser evitados para evitar erros não intencionais com versões mais recentes do compilador (veja aqui). - Funções com visibilidade padrão: Funções sem um especificador de tipo de visibilidade são públicas por padrão em solc < 0.5.0. Isso pode levar a uma vulnerabilidade em que um usuário mal-intencionado pode fazer alterações não autorizadas de estado. solc >= 0.5.0 requer especificadores explícitos de visibilidade de função (veja aqui).
- Ordem incorreta da herança: Contratos que herdam de vários contratos com funções idênticas devem especificar a ordem correta da herança, ou seja, da mais geral para a mais específica para evitar herdar a implementação incorreta da função (veja aqui).
- Herança ausente: Um contrato pode parecer (com base no nome ou nas funções implementadas) que herda de outra interface ou contrato abstrato sem realmente fazê-lo (veja aqui).
- Sofrimento do gas insuficiente: retransmissores de transações precisam ser confiáveis para fornecer gas suficiente para que a transação seja bem-sucedida (veja aqui).
-
Modificando parâmetros de tipo de referência: Structs/Arrays/Mappings passados como argumentos para uma função podem ser por valor (memória) ou por referência (armazenamento), conforme especificado pela localização dos dados (opcional antes do solc 0.5.0). Garanta o uso correto de
memory
estorage
nos parâmetros das funções e torne todos os locais de dados explícitos (veja aqui). - Salto arbitrário com variável de tipo de função: As variáveis de tipo de função devem ser tratadas com cuidado e evitadas em manipulações com assembly para impedir saltos para locais arbitrários de código (veja aqui).
-
Colisões de hash com múltiplos argumentos de comprimento variável: Usar
abi.encodePacked()
com vários argumentos de comprimento variável pode, em certas situações, levar a uma colisão de hash. Não permita que os usuários acessem os parâmetros usados emabi.encodePacked(),
use arrays de tamanho fixo ou useabi.encode()
(veja aqui e aqui). - Risco de maleabilidade por sujeira nos bits de ordem alta (high-order bits): Tipos que não ocupam os 32 bytes completos podem conter "sujeira nos bits de ordem alta" que não afetam a operação em tipos, mas dão resultados diferentes com
msg.data
(veja aqui). -
Deslocamento incorreto no processo de montagem (assembly): Operadores de deslocamento (
shl(x, y), shr(x, y), sar(x, y)
) na montagem do Solidity aplicam a operação de deslocamento de x bits em y e não ao contrário, o que pode ser confuso. Verifique se os valores em uma operação de deslocamento estão invertidos (veja aqui). - Uso do assembly: O uso do assembly EVM é propenso a erros e deve ser evitado ou revisitado/validado quanto à sua exatidão (veja aqui).
- Caractere de controle Right-To-Left-Override (U+202E): Atores maliciosos podem usar o caractere unicode Right-To-Left-Override para forçar a renderização de texto RTL (right to left) e confundir os usuários quanto à real intenção de um contrato. O caractere U+202E não deve aparecer no código-fonte de um contrato inteligente (veja aqui).
-
Variáveis de estado constantes: As variáveis de estado constante devem ser declaradas
constant
para economizar gas (veja aqui). - Nomes de variáveis semelhantes: Variáveis com nomes semelhantes podem ser confundidas umas com as outras e, portanto, devem ser evitadas (veja aqui).
- Variáveis de estado/local não inicializadas: As variáveis de estado/local não inicializadas recebem valores zero pelo compilador e podem causar resultados não intencionais, por exemplo, transferência de tokens para endereço zero. Inicialize explicitamente todas as variáveis de estado/local (consulte aqui e aqui).
- Ponteiros de armazenamento não inicializados: As variáveis de armazenamento local não inicializadas podem apontar para locais de armazenamento inesperados no contrato, o que pode levar a vulnerabilidades. Solc 0.5.0 e acima não permitem tais ponteiros (veja aqui).
- Ponteiros de função não inicializados em construtores: Chamar ponteiros de função não inicializados em construtores de contratos compilados com solc versões 0.4.5-0.4.25 e 0.5.0-0.5.7 levam a um comportamento inesperado devido a um bug do compilador (veja aqui).
- Números literais longos: Os literais de números com muitos dígitos devem ser cuidadosamente verificados, pois são propensos a erros (veja aqui).
-
Enum fora do intervalo: Solc < 0.4.5 produziu um comportamento inesperado com
enums
fora do intervalo. Verifique a conversão deenum
ou use um compilador mais recente (veja aqui). - Funções públicas não chamadas: funções públicas que nunca são chamadas de dentro dos contratos devem ser declaradas externas (external) para economizar gas (veja aqui).
- Código morto/inacessível: código morto pode ser um indicativo de erro do programador, lógica ausente ou potencial oportunidade de otimização, que precisa ser sinalizada para remoção ou tratada adequadamente (veja aqui).
- Valores de retorno não utilizados: Valores de retorno que não são utilizados após chamadas de função são indicativos de erros do programador e podem ter um comportamento inesperado (veja aqui).
- Variáveis não utilizadas: variáveis de estado/local não utilizadas podem ser indicativos de erro do programador, falta de lógica ou potencial oportunidade de otimização, que precisam ser sinalizadas para remoção ou endereçadas apropriadamente (veja aqui).
- Declarações redundantes: Declarações sem efeitos que não produzem código podem ser indicativas de erro do programador ou falta de lógica, que precisa ser sinalizada para remoção ou endereçada apropriadamente (veja aqui).
- Arrays storage de signed integers com ABIEncoderV2: Atribuir um array de inteiros com sinal (signed integer) a um array "storage" de tipo diferente pode levar à corrupção de dados daquele array. Isso se deve a um bug do compilador introduzido na v0.4.7 e corrigido na v0.5.10 (veja aqui).
- Argumentos dinâmicos do construtor recortados com ABIEncoderV2: O construtor de um contrato que recebe structs ou arrays que contêm arrays de tamanho dinâmico, reverte ou decodifica para dados inválidos quando ABIEncoderV2 é usado. Isso se deve a um bug do compilador introduzido na v0.4.16 e corrigido na v0.5.9 (veja aqui).
-
Arrays storage com elemento multiSlot com ABIEncoderV2: Arrays storage contendo structs ou outros arrays de tamanho estático não são lidos corretamente quando codificados diretamente em chamadas de funções externas ou em
abi.encode().
Isso se deve a um bug do compilador introduzido na v0.4.16 e corrigido na v0.5.10 (veja aqui). - Structs calldata com membros estaticamente dimensionados e codificados dinamicamente com ABIEncoderV2: A leitura de structs calldata que contêm membros codificados dinamicamente, mas estaticamente dimensionados pode resultar em valores incorretos. Isso se deve a um bug do compilador introduzido na v0.5.6 e corrigido na v0.5.11 (veja aqui).
- Armazenamento empacotado com ABIEncoderV2: Estruturas de armazenamento e arrays com tipos menores que 32 bytes podem causar corrupção de dados se codificados diretamente do armazenamento usando ABIEncoderV2. Isso se deve a um bug do compilador introduzido na v0.5.0 e corrigido na v0.5.7 (veja aqui).
- Carregamentos incorretos com o otimizador Yul e ABIEncoderV2: O otimizador Yul substitui incorretamente os opcodes MLOAD e SLOAD por valores que foram gravados anteriormente no local de carregamento. Isso só pode acontecer se o ABIEncoderV2 estiver ativado e o otimizador Yul experimental tiver sido ativado manualmente, além do otimizador regular nas configurações do compilador. Isso se deve a um bug do compilador introduzido na v0.5.14 e corrigido na v0.5.15 (veja aqui).
- Fatias de array de tipos-base dinamicamente codificados com ABIEncoderV2: Acessar fatias (slices) de arrays de tipos-base dinamicamente codificados (por exemplo, matrizes multidimensionais) pode resultar na leitura de dados inválidos. Isso se deve a um bug do compilador introduzido na v0.6.0 e corrigido na v0.6.8 (veja aqui).
- Ausência de escaping na formatação com ABIEncoderV2: Literais de string contendo caracteres de barra invertida dupla passados diretamente para chamadas de função externas ou funções de codificação podem levar a uma string diferente sendo usada quando ABIEncoderV2 está habilitado. Isso se deve a um bug do compilador introduzido na v0.5.14 e corrigido na v0.6.8 (veja aqui).
- Overflow de tamanho com deslocamento duplo: Deslocamentos duplos de bit a bit por constantes grandes cuja soma transborda 256 bits podem resultar em valores inesperados. Operações de deslocamento lógico aninhadas cujo tamanho total de deslocamento é 2**256 ou mais são otimizadas incorretamente. Isso se aplica apenas a deslocamentos por números de bits que são expressões constantes de tempo de compilação. Isso acontece quando o otimizador é usado e evmVersion >= Constantinople. Isso se deve a um bug do compilador introduzido na v0.5.5 e corrigido na v0.5.6 (veja aqui).
- Otimização incorreta de byte de instrução: O otimizador manipula incorretamente opcodes de byte cujo segundo argumento é 31 ou uma expressão constante que é avaliada como 31. Isso pode resultar em valores inesperados. Isso pode acontecer ao executar o acesso de índice em bytesNN com um valor constante de tempo de compilação (não índice) de 31 ou ao usar o opcode byte no assembly embutido no código (inline). Isso se deve a um bug do compilador introduzido na v0.5.5 e corrigido na v0.5.7 (veja aqui).
-
Atribuições essenciais removidas com Yul Optimizer: O otimizador Yul pode remover atribuições essenciais para variáveis declaradas dentro de loops
for
quandocontinue
oubreak
são usados, principalmente na utilização de assembly inline com loopsfor
,continue
ebreak
. Isso se deve a um bug do compilador introduzido na v0.5.8/v0.6.0 e corrigido na v0.5.16/v0.6.1 (veja aqui). - Sobrescrita de métodos privados: Embora os métodos privados dos contratos-base não sejam visíveis e não possam ser chamados diretamente do contrato derivado, ainda é possível declarar uma função dos mesmos nome e tipo e, assim, alterar o comportamento das funções dos contratos-base. Isso se deve a um bug do compilador introduzido na v0.3.0 e corrigido na v0.5.17 (veja aqui).
- Atribuição de tuplas de componentes com múltiplos slots de pilhas (stack): Atribuições de tuplas com componentes que ocupam vários slots de pilha, ou seja, tuplas aninhadas, ponteiros para funções externas ou referências a arrays calldata dimensionados dinamicamente, podem resultar em valores inválidos. Isso se deve a um bug do compilador introduzido na v0.1.6 e corrigido na v0.6.6 (veja aqui).
- Limpeza de array dinâmico: Ao atribuir um array de tamanho dinâmico com tipos de tamanho de no máximo 16 bytes no armazenamento, fazendo com que o array atribuído diminua, algumas partes dos slots excluídos não são zerados. Isso se deve a um bug do compilador corrigido na v0.7.3 (veja aqui).
- Cópia de arrays de bytes vazios: Copiar um array de bytes vazio (ou string) da memória ou calldata para armazenamento pode resultar na corrupção de dados se o comprimento do array de destino for aumentado subsequentemente sem armazenar novos dados. Isso se deve a um bug do compilador corrigido na v0.7.4 (veja aqui).
- Overflow na criação de arrays em memória: A criação de arrays muito grandes em memória pode resultar em regiões de memória sobrepostas e, portanto, corrupção de memória. Isso se deve a um bug do compilador introduzido na v0.2.0 e corrigido na v0.6.5 (veja aqui).
- Calldata "using for": Chamadas de função para funções de biblioteca internas com parâmetros calldata chamados via "using for" podem resultar na leitura de dados inválidos. Isso se deve a um bug do compilador introduzido na v0.6.9 e corrigido na v0.6.10 (veja aqui).
- Redefinição de função livre (free function): O compilador não sinaliza um erro quando duas ou mais funções livres (funções fora de um contrato) com o mesmo nome e tipos de parâmetro são definidas em uma unidade de código ou quando um alias importado de função livre oculta outro função livre com um nome diferente, mas tipos de parâmetros idênticos. Isso se deve a um bug do compilador introduzido na v0.7.1 e corrigido na v0.7.2 (veja aqui).
-
Inicializadores desprotegidos em contratos atualizáveis baseados em proxy: Os contratos atualizáveis baseados em proxy precisam usar funções inicializadoras public ao invés de construtores, que precisam ser explicitamente chamados apenas uma vez. É uma obrigação prevenir múltiplas invocações de tais funções inicializadoras (por exemplo, através do modificador
initializer
da biblioteca Initializable do OpenZeppelin) (veja aqui e aqui). - Inicializando variáveis de estado em contratos atualizáveis baseados em proxy: Isso deve ser feito em funções inicializadoras e não como parte das declarações de variáveis de estado, caso em que elas não serão definidas (veja aqui).
- Importar contratos atualizáveis em contratos atualizáveis baseados em proxy: Os contratos importados de contratos atualizáveis baseados em proxy também devem ser atualizáveis onde tais contratos foram modificados para usar inicializadores em vez de construtores (veja aqui).
- Evite selfdestruct ou delegatecall em contratos atualizáveis baseados em proxy: Isso fará com que o contrato lógico seja destruído e todas as instâncias do contrato acabarão delegando chamadas para um endereço sem nenhum código (veja aqui).
- Variáveis de estado em contratos atualizáveis baseados em proxy: A ordem/layout da declaração e o tipo/mutabilidade das variáveis de estado em tais contratos devem ser preservados exatamente durante a atualização para evitar erros críticos de incompatibilidade de layout de armazenamento (veja aqui).
- Colisão de ID de função entre proxy/contrato em contratos atualizáveis baseados em proxy: Contratos de proxy maliciosos podem explorar a colisão de ID de função para invocar funções de proxy não intencionais ao invés de funções do contrato. Verifique se há colisões de ID de função (veja aqui e aqui).
- Função shadow entre proxy/contrato em contratos atualizáveis baseados em proxy: Funções shadow no contrato proxy impedem que funções no contrato lógico sejam invocadas (veja aqui).
Resumo
Este post compilou uma checklist de 101 armadilhas fundamentais de segurança de smart contracts e práticas recomendadas de fontes amplamente citadas. Ele aborda os aspectos mais comuns relacionados a Solidity e EVM. Os itens da checklist foram agrupados com base em recursos ou impacto subjacentes compartilhados. Em breve, isso será colocado no Github para que possa ser corrigido, atualizado e aprimorado com a participação e discussão da comunidade.
Interações complexas, perspectivas econômicas e vulnerabilidades específicas de lógica de protocolo foram excluídas aqui. No entanto, dado que este é o lugar onde as vulnerabilidades/exploits sutis de DeFi aconteceram e as ferramentas automatizadas ainda não são capazes de sinalizá-las, definitivamente há espaço para outra checklist que se estende especificamente ao controle de acesso, oráculos, flash loans, tokens de rebase/deflacionários, padrões populares de token, famílias de protocolo, front-running/back-running/sandwich, lançamentos protegidos etc.
A carga de segurança para desenvolvedores/auditores/protocolos de smart contracts é imensa. Ferramentas automatizadas, como Slither, que verificam muitas dessas armadilhas são obrigatórias durante o desenvolvimento/auditoria, mas podem ter falsos negativos/positivos em certos casos e requerem intervenção humana para maior confiança/cobertura. As checklists ajudam a reduzir essa carga cognitiva e podem ajudar na construção de smart contracts mais seguros e robustos no Ethereum.
Espero que você tenha achado isso um pouco útil. Obrigado por ler e aguardamos seus comentários e feedback.
Latest comments (0)