Como usar o Echidna para testar contratos inteligentes
Instalação
O Echidna pode ser instalado através do docker ou usando o binário pré-compilado.
Echidna através do docker
docker pull trailofbits/eth-security-toolboxdocker run -it -v "$PWD":/home/training trailofbits/eth-security-toolboxO último comando executa o eth-security-toolbox em um docker que tem acesso ao seu diretório atual. Você pode alterar os arquivos do seu host e executar as ferramentas nos arquivos do docker
Dentro do docker, execute:
solc-select 0.5.11cd /home/trainingBinário
https://github.com/crytic/echidna/releases/tag/v1.4.0.0 (opens in a new tab)
Introdução ao fuzzing baseado em propriedade
O Echidna é um fuzzer baseado em propriedade, que descrevemos em nossas postagens de blog anteriores (1 (opens in a new tab), 2 (opens in a new tab), 3 (opens in a new tab)).
Fuzzing
Fuzzing (opens in a new tab) é uma técnica bem conhecida na comunidade de segurança. Consiste em gerar entradas que são mais ou menos aleatórias para encontrar bugs no programa. Fuzzers para software tradicional (como AFL (opens in a new tab) ou LibFuzzer (opens in a new tab)) são conhecidos por serem ferramentas eficientes para encontrar bugs.
Além da geração puramente aleatória de entradas, existem muitas técnicas e estratégias para gerar boas entradas, incluindo:
- Obter feedback de cada execução e usá-lo para guiar a geração. Por exemplo, se uma entrada recém-gerada levar à descoberta de um novo caminho, pode fazer sentido gerar novas entradas próximas a ela.
- Gerar a entrada respeitando uma restrição estrutural. Por exemplo, se sua entrada contiver um cabeçalho com um checksum, fará sentido deixar que o fuzzer gere uma entrada que valide o checksum.
- Usar entradas conhecidas para gerar novas entradas: se você tiver acesso a um grande conjunto de dados de entrada válida, seu fuzzer pode gerar novas entradas a partir deles, em vez de iniciar sua geração do zero. Normalmente, são chamadas de seeds.
Fuzzing baseado em propriedade
O Echidna pertence a uma família específica de fuzzer: fuzzing baseado em propriedade, fortemente inspirado pelo QuickCheck (opens in a new tab). Em contraste com o fuzzer clássico, que tentará encontrar falhas, o Echidna tentará quebrar invariantes definidos pelo usuário.
Em contratos inteligentes, as invariantes são funções do Solidity que podem representar qualquer estado incorreto ou inválido que o contrato possa alcançar, incluindo:
- Controle de acesso incorreto: o invasor tornou-se o proprietário do contrato.
- Máquina de estado incorreta: os tokens podem ser transferidos enquanto o contrato está pausado.
- Aritmética incorreta: o usuário pode causar um underflow em seu saldo e obter tokens gratuitos ilimitados.
Testando uma propriedade com o Echidna
Veremos como testar um contrato inteligente com o Echidna. O alvo é o seguinte contrato inteligente token.sol (opens in a new tab):
1contract Token{2 mapping(address => uint) public balances;3 function airdrop() public{4 balances[msg.sender] = 1000;5 }6 function consume() public{7 require(balances[msg.sender]>0);8 balances[msg.sender] -= 1;9 }10 function backdoor() public{11 balances[msg.sender] += 1;12 }13}Exibir tudoVamos supor que este token deva ter as seguintes propriedades:
- Qualquer pessoa pode ter no máximo 1.000 tokens
- O token não pode ser transferido (não é um token ERC20)
Escreva uma propriedade
As propriedades do Echidna são funções do Solidity. Uma propriedade deve:
- Não ter argumentos
- Retornar
truese for bem-sucedida - Ter seu nome começando com
echidna
O Echidna irá:
- Gerar automaticamente transações arbitrárias para testar a propriedade.
- Relatar quaisquer transações que levem uma propriedade a retornar
falseou a lançar um erro. - Descartar o efeito colateral ao chamar uma propriedade (ou seja, se a propriedade alterar uma variável de estado, ela será descartada após o teste)
A propriedade a seguir verifica se o chamador não tem mais de 1.000 tokens:
1function echidna_balance_under_1000() public view returns(bool){2 return balances[msg.sender] <= 1000;3}Use herança para separar seu contrato de suas propriedades:
1contract TestToken is Token{2 function echidna_balance_under_1000() public view returns(bool){3 return balances[msg.sender] <= 1000;4 }5 }O token.sol (opens in a new tab) implementa a propriedade e herda do token.
Iniciar um contrato
O Echidna precisa de um construtor sem argumentos. Se seu contrato precisar de uma inicialização específica, você precisará fazê-la no construtor.
Existem alguns endereços específicos no Echidna:
0x00a329c0648769A73afAc7F9381E08FB43dBEA72, que chama o construtor.0x10000,0x20000e0x00a329C0648769a73afAC7F9381e08fb43DBEA70, que chamam aleatoriamente as outras funções.
Não precisamos de nenhuma inicialização específica em nosso exemplo atual, como resultado, nosso construtor está vazio.
Executar o Echidna
O Echidna é iniciado com:
echidna-test contract.solSe contract.sol contiver vários contratos, você pode especificar o alvo:
echidna-test contract.sol --contract MyContractResumo: testando uma propriedade
O texto a seguir resume a execução do Echidna em nosso exemplo:
1contract TestToken is Token{2 constructor() public {}3 function echidna_balance_under_1000() public view returns(bool){4 return balances[msg.sender] <= 1000;5 }6 }echidna-test testtoken.sol --contract TestToken...echidna_balance_under_1000: falhou!💥 Sequência de chamadas, encolhendo (1205/5000): airdrop() backdoor()...Exibir tudoO Echidna descobriu que a propriedade é violada se backdoor for chamado.
Filtrando funções para chamar durante uma campanha de fuzzing
Veremos como filtrar as funções que sofrerão fuzzing. O alvo é o seguinte contrato inteligente:
1contract C {2 bool state1 = false;3 bool state2 = false;4 bool state3 = false;5 bool state4 = false;67 function f(uint x) public {8 require(x == 12);9 state1 = true;10 }1112 function g(uint x) public {13 require(state1);14 require(x == 8);15 state2 = true;16 }1718 function h(uint x) public {19 require(state2);20 require(x == 42);21 state3 = true;22 }2324 function i() public {25 require(state3);26 state4 = true;27 }2829 function reset1() public {30 state1 = false;31 state2 = false;32 state3 = false;33 return;34 }3536 function reset2() public {37 state1 = false;38 state2 = false;39 state3 = false;40 return;41 }4243 function echidna_state4() public returns (bool) {44 return (!state4);45 }46}Exibir tudoEste pequeno exemplo força o Echidna a encontrar uma determinada sequência de transações para alterar uma variável de estado. Isso é difícil para um fuzzer (recomenda-se usar uma ferramenta de execução simbólica como o Manticore (opens in a new tab)). Podemos executar o Echidna para verificar isso:
echidna-test multi.sol...echidna_state4: passou! 🎉Seed: -3684648582249875403Funções de filtragem
O Echidna tem dificuldade em encontrar a sequência correta para testar este contrato porque as duas funções de redefinição (reset1 e reset2) definirão todas as variáveis de estado como false.
No entanto, podemos usar um recurso especial do Echidna para adicionar as funções de redefinição à lista de bloqueio ou para adicionar apenas as funções f, g,
h e i à lista de permissões.
Para adicionar funções à lista de bloqueio, podemos usar este arquivo de configuração:
1filterBlacklist: true2filterFunctions: ["reset1", "reset2"]Outra abordagem para filtrar funções é listar as funções permitidas. Para fazer isso, podemos usar este arquivo de configuração:
1filterBlacklist: false2filterFunctions: ["f", "g", "h", "i"]filterBlacklistétruepor padrão.- A filtragem será realizada apenas por nome (sem parâmetros). Se você tiver
f()ef(uint256), o filtro"f"corresponderá a ambas as funções.
Executar o Echidna
Para executar o Echidna com um arquivo de configuração blacklist.yaml:
echidna-test multi.sol --config blacklist.yaml...echidna_state4: falhou!💥 Sequência de chamadas: f(12) g(8) h(42) i()O Echidna encontrará a sequência de transações para falsear a propriedade quase imediatamente.
Resumo: funções de filtragem
O Echidna pode adicionar funções a uma lista de bloqueio ou a uma lista de permissões para chamar durante uma campanha de fuzzing usando:
1filterBlacklist: true2filterFunctions: ["f1", "f2", "f3"]echidna-test contract.sol --config config.yaml...O Echidna inicia uma campanha de fuzzing adicionando f1, f2 e f3 à lista de bloqueio ou chamando apenas essas funções, de acordo com o valor do booleano filterBlacklist.
Como testar a asserção do Solidity com o Echidna
Neste breve tutorial, vamos mostrar como usar o Echidna para testar a verificação de asserções em contratos. Vamos supor que tenhamos um contrato como este:
1contract Incrementor {2 uint private counter = 2**200;34 function inc(uint val) public returns (uint){5 uint tmp = counter;6 counter += val;7 // tmp <= counter8 return (counter - tmp);9 }10}Exibir tudoEscrever uma asserção
Queremos ter certeza de que tmp é menor ou igual a counter depois de retornar sua diferença. Poderíamos escrever uma
propriedade do Echidna, mas precisaríamos armazenar o valor tmp em algum lugar. Em vez disso, poderíamos usar uma asserção como esta:
1contract Incrementor {2 uint private counter = 2**200;34 function inc(uint val) public returns (uint){5 uint tmp = counter;6 counter += val;7 assert (tmp <= counter);8 return (counter - tmp);9 }10}Exibir tudoExecutar o Echidna
Para habilitar o teste de falha de asserção, crie um arquivo de configuração do Echidna (opens in a new tab) config.yaml:
1checkAsserts: trueQuando executamos este contrato no Echidna, obtemos os resultados esperados:
echidna-test assert.sol --config config.yamlAnalyzing contract: assert.sol:Incrementorassertion in inc: failed!💥 Call sequence, shrinking (2596/5000): inc(21711016731996786641919559689128982722488122124807605757398297001483711807488) inc(7237005577332262213973186563042994240829374041602535252466099000494570602496) inc(86844066927987146567678238756515930889952488499230423029593188005934847229952)Seed: 1806480648350826486Exibir tudoComo você pode ver, o Echidna relata alguma falha de asserção na função inc. Adicionar mais de uma asserção por função é possível, mas o Echidna não consegue dizer qual asserção falhou.
Quando e como usar asserções
As asserções podem ser usadas como alternativas a propriedades explícitas, especialmente se as condições a serem verificadas estiverem diretamente relacionadas ao uso correto de alguma operação f. Adicionar asserções após algum código garantirá que a verificação ocorra imediatamente após sua execução:
1function f(..) public {2 // some complex code3 ...4 assert (condition);5 ...6}7Pelo contrário, o uso de uma propriedade explícita do Echidna executará transações aleatoriamente e não há uma maneira fácil de forçar exatamente quando ela será verificada. Ainda é possível fazer esta solução alternativa:
1function echidna_assert_after_f() public returns (bool) {2 f(..);3 return(condition);4}No entanto, há alguns problemas:
- Falha se
ffor declarada comointernalouexternal. - Não está claro quais argumentos devem ser usados para chamar
f. - Se
freverter, a propriedade falhará.
Em geral, recomendamos seguir a recomendação de John Regehr (opens in a new tab) sobre como usar asserções:
- Não force nenhum efeito colateral durante a verificação de asserção. Por exemplo:
assert(ChangeStateAndReturn() == 1) - Não faça asserções óbvias. Por exemplo,
assert(var >= 0)ondevaré declarado comouint.
Finalmente, não use require em vez de assert, pois o Echidna não será capaz de detectá-lo (mas o contrato será revertido de qualquer maneira).
Resumo: verificação de asserção
O texto a seguir resume a execução do Echidna em nosso exemplo:
1contract Incrementor {2 uint private counter = 2**200;34 function inc(uint val) public returns (uint){5 uint tmp = counter;6 counter += val;7 assert (tmp <= counter);8 return (counter - tmp);9 }10}Exibir tudoechidna-test assert.sol --config config.yamlAnalyzing contract: assert.sol:Incrementorassertion in inc: failed!💥 Call sequence, shrinking (2596/5000): inc(21711016731996786641919559689128982722488122124807605757398297001483711807488) inc(7237005577332262213973186563042994240829374041602535252466099000494570602496) inc(86844066927987146567678238756515930889952488499230423029593188005934847229952)Seed: 1806480648350826486Exibir tudoO Echidna descobriu que a asserção em inc pode falhar se essa função for chamada várias vezes com argumentos grandes.
Coletando e modificando um corpus do Echidna
Veremos como coletar e usar um corpus de transações com o Echidna. O alvo é o seguinte contrato inteligente magic.sol (opens in a new tab):
1contract C {2 bool value_found = false;3 function magic(uint magic_1, uint magic_2, uint magic_3, uint magic_4) public {4 require(magic_1 == 42);5 require(magic_2 == 129);6 require(magic_3 == magic_4+333);7 value_found = true;8 return;9 }1011 function echidna_magic_values() public returns (bool) {12 return !value_found;13 }1415}Exibir tudoEste pequeno exemplo força o Echidna a encontrar determinados valores para alterar uma variável de estado. Isso é difícil para um fuzzer (recomenda-se usar uma ferramenta de execução simbólica como o Manticore (opens in a new tab)). Podemos executar o Echidna para verificar isso:
echidna-test magic.sol...echidna_magic_values: passou! 🎉Seed: 2221503356319272685No entanto, ainda podemos usar o Echidna para coletar o corpus ao executar esta campanha de fuzzing.
Coletando um corpus
Para habilitar a coleta de corpus, crie um diretório de corpus:
mkdir corpus-magicE um arquivo de configuração do Echidna (opens in a new tab) config.yaml:
1coverage: true2corpusDir: "corpus-magic"Agora podemos executar nossa ferramenta e verificar o corpus coletado:
echidna-test magic.sol --config config.yamlO Echidna ainda não consegue encontrar os valores mágicos corretos, mas podemos dar uma olhada no corpus que ele coletou. Por exemplo, um desses arquivos era:
1[2 {3 "_gas'": "0xffffffff",4 "_delay": ["0x13647", "0xccf6"],5 "_src": "00a329c0648769a73afac7f9381e08fb43dbea70",6 "_dst": "00a329c0648769a73afac7f9381e08fb43dbea72",7 "_value": "0x0",8 "_call": {9 "tag": "SolCall",10 "contents": [11 "magic",12 [13 {14 "contents": [15 256,16 "93723985220345906694500679277863898678726808528711107336895287282192244575836"17 ],18 "tag": "AbiUInt"19 },20 {21 "contents": [256, "334"],22 "tag": "AbiUInt"23 },24 {25 "contents": [26 256,27 "68093943901352437066264791224433559271778087297543421781073458233697135179558"28 ],29 "tag": "AbiUInt"30 },31 {32 "tag": "AbiUInt",33 "contents": [256, "332"]34 }35 ]36 ]37 },38 "_gasprice'": "0xa904461f1"39 }40]Exibir tudoClaramente, essa entrada não acionará a falha em nossa propriedade. No entanto, na próxima etapa, veremos como modificá-lo para isso.
Semeando um corpus
O Echidna precisa de ajuda para lidar com a função magic. Vamos copiar e modificar a entrada para usar parâmetros
adequados para ela:
cp corpus/2712688662897926208.txt corpus/new.txtModificaremos o new.txt para chamar magic(42,129,333,0). Agora, podemos executar novamente o Echidna:
echidna-test magic.sol --config config.yaml...echidna_magic_values: failed!💥 Call sequence: magic(42,129,333,0)Unique instructions: 142Unique codehashes: 1Seed: -7293830866560616537Exibir tudoDesta vez, descobriu que a propriedade é violada imediatamente.
Encontrando transações com alto consumo de gás
Veremos como encontrar as transações com alto consumo de gás com o Echidna. O alvo é o seguinte contrato inteligente:
1contract C {2 uint state;34 function expensive(uint8 times) internal {5 for(uint8 i=0; i < times; i++)6 state = state + i;7 }89 function f(uint x, uint y, uint8 times) public {10 if (x == 42 && y == 123)11 expensive(times);12 else13 state = 0;14 }1516 function echidna_test() public returns (bool) {17 return true;18 }1920}Exibir tudoAqui, expensive pode ter um grande consumo de gás.
Atualmente, o Echidna sempre precisa de uma propriedade para testar: aqui, echidna_test sempre retorna true.
Podemos executar o Echidna para verificar isso:
1echidna-test gas.sol2...3echidna_test: passou! 🎉45Seed: 2320549945714142710Medindo o consumo de gás
Para habilitar o consumo de gás com o Echidna, crie um arquivo de configuração config.yaml:
1estimateGas: trueNeste exemplo, também reduziremos o tamanho da sequência de transações para facilitar a compreensão dos resultados:
1seqLen: 22estimateGas: trueExecutar o Echidna
Depois que o arquivo de configuração for criado, podemos executar o Echidna desta forma:
echidna-test gas.sol --config config.yaml...echidna_test: passed! 🎉f used a maximum of 1333608 gas Call sequence: f(42,123,249) Gas price: 0x10d5733f0a Time delay: 0x495e5 Block delay: 0x88b2Unique instructions: 157Unique codehashes: 1Seed: -325611019680165325Exibir tudo- O gás mostrado é uma estimativa fornecida pelo HEVM (opens in a new tab).
Filtrando chamadas que reduzem o gás
O tutorial sobre como filtrar funções a serem chamadas durante uma campanha de fuzzing, acima, mostra como
remover algumas funções do seu teste.
Isso pode ser crítico para obter uma estimativa de gás precisa.
Considere o seguinte exemplo:
1contract C {2 address [] addrs;3 function push(address a) public {4 addrs.push(a);5 }6 function pop() public {7 addrs.pop();8 }9 function clear() public{10 addrs.length = 0;11 }12 function check() public{13 for(uint256 i = 0; i < addrs.length; i++)14 for(uint256 j = i+1; j < addrs.length; j++)15 if (addrs[i] == addrs[j])16 addrs[j] = address(0x0);17 }18 function echidna_test() public returns (bool) {19 return true;20 }21}Exibir tudoSe o Echidna puder chamar todas as funções, ele não encontrará facilmente transações com alto custo de gás:
1echidna-test pushpop.sol --config config.yaml2...3pop used a maximum of 10746 gas4...5check used a maximum of 23730 gas6...7clear used a maximum of 35916 gas8...9push used a maximum of 40839 gasExibir tudoIsso porque o custo depende do tamanho de addrs e chamadas aleatórias tendem a deixar o array quase vazio.
Isso ocorre porque o custo depende do tamanho de addrs e as chamadas aleatórias tendem a deixar o array quase vazio. Adicionar pop e clear à lista de bloqueio, no entanto, nos dá resultados muito melhores:
1filterBlacklist: true2filterFunctions: ["pop", "clear"]1echidna-test pushpop.sol --config config.yaml2...3push used a maximum of 40839 gas4...5check used a maximum of 1484472 gasResumo: encontrando transações com alto consumo de gás
O Echidna pode encontrar transações com alto consumo de gás usando a opção de configuração estimateGas:
1estimateGas: trueechidna-test contract.sol --config config.yaml...O Echidna relatará uma sequência com o consumo máximo de gás para cada função, uma vez que a campanha de fuzzing tenha terminado.
Última atualização da página: 21 de outubro de 2025