Cómo utilizar Echidna para probar contratos inteligentes
Instalación
Echidna se puede instalar a través de docker o por medio del binario precompilado.
Echidna a través de Docker
docker pull trailofbits/eth-security-toolboxdocker run -it -v "$PWD":/home/training trailofbits/eth-security-toolboxEl comando anterior ejecuta eth-security-toolbox en un Docker que tiene acceso a su directorio actual. Puede cambiar los archivos desde su host y ejecutar las herramientas en los archivos desde el Docker
Dentro de Docker, ejecute:
solc-select 0.5.11cd /home/trainingBinario
https://github.com/crytic/echidna/releases/tag/v1.4.0.0opens in a new tab
Introducción al fuzzing basado en propiedades
Echidna es un fuzzer basado en propiedades, que describimos en nuestras entradas de blog anteriores (1opens in a new tab, 2opens in a new tab, 3opens in a new tab).
Fuzzing
Fuzzingopens in a new tab es una técnica muy conocida en la comunidad de la seguridad. Consiste en generar entradas que son más o menos aleatorias para encontrar errores en el programa. Los fuzzers para software tradicional (como AFLopens in a new tab o LibFuzzeropens in a new tab) son conocidos por ser herramientas eficientes para encontrar errores.
Más allá de la generación de entradas puramente aleatorias, existen muchas técnicas y estrategias para generar buenas entradas, que incluyen:
- Obtener feedback de cada ejecución y guiar la generación usándolo. Por ejemplo, si una entrada recién generada conduce al descubrimiento de una nueva ruta, puede tener sentido generar nuevas entradas cercanas a ella.
- Generar la entrada respetando una restricción estructural. Por ejemplo, si su entrada contiene una cabecera con una suma de verificación (checksum), tendrá sentido dejar que el fuzzer genere una entrada que valide la suma de verificación.
- Utilizar entradas conocidas para generar nuevas entradas: si tiene acceso a un gran conjunto de datos de entradas válidas, su fuzzer puede generar nuevas entradas a partir de ellas, en lugar de comenzar la generación desde cero. Estas se suelen llamar seeds.
Fuzzing basado en propiedades
Echidna pertenece a una familia específica de fuzzer: el fuzzing basado en propiedades, muy inspirado en QuickCheckopens in a new tab. A diferencia del fuzzer clásico, que intentará encontrar fallos, Echidna intentará romper las invariantes definidas por el usuario.
En los contratos inteligentes, las invariantes son funciones de Solidity, que pueden representar cualquier estado incorrecto o inválido que el contrato puede alcanzar, incluyendo:
- Control de acceso incorrecto: el atacante se convirtió en el propietario del contrato.
- Máquina de estado incorrecta: los tokens pueden transferirse mientras el contrato está en pausa.
- Aritmética incorrecta: el usuario puede provocar un subdesbordamiento en su saldo y obtener tokens gratis ilimitados.
Probar una propiedad con Echidna
Veremos cómo probar un contrato inteligente con Echidna. El objetivo es el siguiente contrato inteligente token.solopens 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}Mostrar todoAsumiremos que este token debe tener las siguientes propiedades:
- Cualquiera puede tener un máximo de 1000 tokens
- El token no se puede transferir (no es un token ERC20)
Escribir una propiedad
Las propiedades de Echidna son funciones de Solidity. Una propiedad debe:
- No tener argumentos
- Devolver
truesi tiene éxito - Tener un nombre que empiece con
echidna
Echidna hará lo siguiente:
- Generar automáticamente transacciones arbitrarias para probar la propiedad.
- Informar de cualquier transacción que lleve a una propiedad a devolver
falseo a lanzar un error. - Descartar los efectos secundarios al llamar a una propiedad (es decir, si la propiedad cambia una variable de estado, se descarta después de la prueba)
La siguiente propiedad comprueba que quien realiza la llamada no tiene más de 1000 tokens:
1function echidna_balance_under_1000() public view returns(bool){2 return balances[msg.sender] <= 1000;3}Utilice la herencia para separar su contrato de sus propiedades:
1contract TestToken is Token{2 function echidna_balance_under_1000() public view returns(bool){3 return balances[msg.sender] <= 1000;4 }5 }token.solopens in a new tab implementa la propiedad y hereda del token.
Iniciar un contrato
Echidna necesita un constructor sin argumentos. Si su contrato necesita una inicialización específica, debe hacerlo en el constructor.
Hay algunas direcciones específicas en Echidna:
0x00a329c0648769A73afAc7F9381E08FB43dBEA72, que llama al constructor.0x10000,0x20000y0x00a329C0648769a73afAC7F9381e08fb43DBEA70, que llaman aleatoriamente a las otras funciones.
No necesitamos ninguna inicialización particular en nuestro ejemplo actual; como resultado, nuestro constructor está vacío.
Ejecutar Echidna
Echidna se inicia con:
echidna-test contract.solSi contract.sol contiene varios contratos, puede especificar el objetivo:
echidna-test contract.sol --contract MyContractResumen: Probar una propiedad
A continuación se resume la ejecución de Echidna en nuestro ejemplo:
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: failed!💥 Call sequence, shrinking (1205/5000): airdrop() backdoor()...Mostrar todoEchidna encontró que la propiedad se viola si se llama a backdoor.
Filtrar funciones para llamar durante una campaña de fuzzing
Veremos cómo filtrar las funciones a las que aplicar fuzzing. El objetivo es el siguiente 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}Mostrar todoEste pequeño ejemplo obliga a Echidna a encontrar una secuencia determinada de transacciones para cambiar una variable de estado. Esto es difícil para un fuzzer (se recomienda usar una herramienta de ejecución simbólica como Manticoreopens in a new tab). Podemos ejecutar Echidna para verificar esto:
echidna-test multi.sol...echidna_state4: passed! 🎉Seed: -3684648582249875403Filtrado de funciones
Echidna tiene problemas para encontrar la secuencia correcta para probar este contrato porque las dos funciones de reseteo (reset1 y reset2) establecerán todas las variables de estado a false.
Sin embargo, podemos usar una característica especial de Echidna para incluir en la lista negra la función de reseteo o para incluir en la lista blanca solo las funciones f, g,
h e i.
Para incluir funciones en la lista negra, podemos usar este archivo de configuración:
1filterBlacklist: true2filterFunctions: ["reset1", "reset2"]Otro enfoque para filtrar funciones es listar las funciones incluidas en la lista blanca. Para ello, podemos usar este archivo de configuración:
1filterBlacklist: false2filterFunctions: ["f", "g", "h", "i"]filterBlacklistestruepor defecto.- El filtrado se realizará solo por nombre (sin parámetros). Si tiene
f()yf(uint256), el filtro"f"coincidirá con ambas funciones.
Ejecutar Echidna
Para ejecutar Echidna con un archivo de configuración blacklist.yaml:
echidna-test multi.sol --config blacklist.yaml...echidna_state4: failed!💥 Call sequence: f(12) g(8) h(42) i()Echidna encontrará la secuencia de transacciones para falsear la propiedad casi de inmediato.
Resumen: Filtrado de funciones
Echidna puede incluir funciones en una lista negra o en una lista blanca para llamar durante una campaña de fuzzing usando:
1filterBlacklist: true2filterFunctions: ["f1", "f2", "f3"]echidna-test contract.sol --config config.yaml...Echidna inicia una campaña de fuzzing, ya sea incluyendo en la lista negra a f1, f2 y f3 o solo llamando a estas, según el valor del booleano filterBlacklist.
Cómo probar el assert de Solidity con Echidna
En este breve tutorial, vamos a mostrar cómo usar Echidna para probar la comprobación de aserciones en los contratos. Supongamos que tenemos un 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}Mostrar todoEscribir una aserción
Queremos asegurarnos de que tmp es menor o igual que counter después de devolver su diferencia. Podríamos escribir una propiedad de Echidna, pero necesitaremos almacenar el valor tmp en algún lugar. En su lugar, podríamos usar una aserción 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}Mostrar todoEjecutar Echidna
Para habilitar la prueba de fallos de aserción, cree un archivo de configuración de Echidnaopens in a new tab config.yaml:
1checkAsserts: trueCuando ejecutamos este contrato en Echidna, obtenemos los 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: 1806480648350826486Mostrar todoComo puede ver, Echidna informa de un fallo de aserción en la función inc. Es posible añadir más de una aserción por función, pero Echidna no puede decir qué aserción ha fallado.
Cuándo y cómo usar aserciones
Las aserciones se pueden utilizar como alternativas a las propiedades explícitas, especialmente si las condiciones a comprobar están directamente relacionadas con el uso correcto de alguna operación f. Añadir aserciones después de un código forzará que la comprobación se produzca inmediatamente después de que se haya ejecutado:
1function f(..) public {2 // some complex code3 ...4 assert (condition);5 ...6}7Por el contrario, usar una propiedad explícita de Echidna ejecutará transacciones aleatoriamente y no hay una forma fácil de forzar exactamente cuándo se comprobará. Aún es posible usar esta solución:
1function echidna_assert_after_f() public returns (bool) {2 f(..);3 return(condition);4}Sin embargo, hay algunos problemas:
- Falla si
fse declara comointernaloexternal. - No está claro qué argumentos se deben usar para llamar a
f. - Si
fse revierte, la propiedad fallará.
En general, recomendamos seguir la recomendación de John Regehropens in a new tab sobre cómo usar las aserciones:
- No fuerce ningún efecto secundario durante la comprobación de la aserción. Por ejemplo:
assert(ChangeStateAndReturn() == 1) - No asegure declaraciones obvias. Por ejemplo,
assert(var >= 0)dondevarse declara comouint.
Finalmente, por favor, no use require en lugar de assert, ya que Echidna no podrá detectarlo (pero el contrato se revertirá de todos modos).
Resumen: comprobación de aserciones
A continuación se resume la ejecución de Echidna en nuestro ejemplo:
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}Mostrar todoechidna-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: 1806480648350826486Mostrar todoEchidna encontró que la aserción en inc puede fallar si esta función se llama varias veces con argumentos grandes.
Recopilar y modificar un corpus de Echidna
Veremos cómo recopilar y usar un corpus de transacciones con Echidna. El objetivo es el siguiente contrato inteligente magic.solopens 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}Mostrar todoEste pequeño ejemplo obliga a Echidna a encontrar ciertos valores para cambiar una variable de estado. Esto es difícil para un fuzzer (se recomienda usar una herramienta de ejecución simbólica como Manticoreopens in a new tab). Podemos ejecutar Echidna para verificar esto:
echidna-test magic.sol...echidna_magic_values: passed! 🎉Seed: 2221503356319272685Sin embargo, aún podemos usar Echidna para recopilar un corpus al ejecutar esta campaña de fuzzing.
Recopilando un corpus
Para habilitar la recopilación del corpus, cree un directorio de corpus:
mkdir corpus-magicY un archivo de configuración de Echidnaopens in a new tab config.yaml:
1coverage: true2corpusDir: "corpus-magic"Ahora podemos ejecutar nuestra herramienta y comprobar el corpus recopilado:
echidna-test magic.sol --config config.yamlEchidna todavía no puede encontrar los valores mágicos correctos, pero podemos echar un vistazo al corpus que ha recopilado. Por ejemplo, uno de estos archivos 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]Mostrar todoClaramente, esta entrada no activará el fallo en nuestra propiedad. Sin embargo, en el siguiente paso veremos cómo modificarlo para ello.
Sembrar un corpus
Echidna necesita ayuda para lidiar con la función magic. Vamos a copiar y modificar la entrada para usar parámetros adecuados para ella:
cp corpus/2712688662897926208.txt corpus/new.txtModificaremos new.txt para llamar a magic(42,129,333,0). Ahora, podemos volver a ejecutar Echidna:
echidna-test magic.sol --config config.yaml...echidna_magic_values: failed!💥 Call sequence: magic(42,129,333,0)Unique instructions: 142Unique codehashes: 1Seed: -7293830866560616537Mostrar todoEsta vez, encontró que la propiedad es violada inmediatamente.
Encontrar transacciones con alto consumo de gas
Veremos cómo encontrar las transacciones con un alto consumo de gas utilizando Echidna. El objetivo es el siguiente 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}Mostrar todoAquí expensive puede tener un gran consumo de gas.
Actualmente, Echidna siempre necesita una propiedad para probar: aquí echidna_test siempre devuelve true.
Podemos ejecutar Echidna para verificar esto:
1echidna-test gas.sol2...3echidna_test: passed! 🎉45Seed: 2320549945714142710Medición del consumo de gas
Para habilitar el consumo de gas con Echidna, cree un archivo de configuración config.yaml:
1estimateGas: trueEn este ejemplo, también reduciremos el tamaño de la secuencia de transacciones para que los resultados sean más fáciles de entender:
1seqLen: 22estimateGas: trueEjecutar Echidna
Una vez que hemos creado el archivo de configuración, podemos ejecutar Echidna de esta manera:
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: -325611019680165325Mostrar todo- El gas que se muestra es una estimación proporcionada por HEVMopens in a new tab.
Filtrar llamadas que reducen el gas
El tutorial anterior sobre el filtrado de funciones a las que llamar durante una campaña de fuzzing muestra cómo eliminar algunas funciones de sus pruebas.
Esto puede ser fundamental para obtener una estimación precisa del gas.
Considere el siguiente ejemplo:
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}Mostrar todoSi Echidna puede llamar a todas las funciones, no encontrará fácilmente transacciones con un alto coste de gas:
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 gasMostrar todoEsto se debe a que el coste depende del tamaño de addrs y las llamadas aleatorias tienden a dejar el array casi vacío.
Sin embargo, incluir en la lista negra pop y clear nos da resultados mucho mejores:
1filterBlacklist: true2filterFunctions: ["pop", "clear"]1echidna-test pushpop.sol --config config.yaml2...3push used a maximum of 40839 gas4...5check used a maximum of 1484472 gasResumen: Encontrar transacciones con alto consumo de gas
Echidna puede encontrar transacciones con un alto consumo de gas usando la opción de configuración estimateGas:
1estimateGas: trueechidna-test contract.sol --config config.yaml...Echidna informará de una secuencia con el consumo máximo de gas para cada función, una vez que la campaña de fuzzing haya terminado.
Última actualización de la página: 21 de octubre de 2025