Как использовать Echidna для тестирования смарт-контрактов
Установка
Echidna можно установить через docker или с помощью предварительно скомпилированного двоичного файла.
Echidna через docker
docker pull trailofbits/eth-security-toolboxdocker run -it -v "$PWD":/home/training trailofbits/eth-security-toolboxПоследняя команда запускает eth-security-toolbox в контейнере Docker, который имеет доступ к вашему текущему каталогу. Вы можете изменять файлы со своего хоста и запускать инструменты для работы с этими файлами из контейнера Docker_
Внутри docker выполните:
solc-select 0.5.11cd /home/trainingДвоичный файл
https://github.com/crytic/echidna/releases/tag/v1.4.0.0 (opens in a new tab)
Введение в фаззинг на основе свойств
Echidna — это фаззер на основе свойств, как мы описывали в наших предыдущих статьях в блоге (1 (opens in a new tab), 2 (opens in a new tab), 3 (opens in a new tab)).
Фаззинг
Фаззинг (opens in a new tab) — это хорошо известная техника в сообществе безопасности. Она заключается в генерировании более или менее случайных входных данных для поиска ошибок в программе. Фаззеры для традиционного программного обеспечения (такие как AFL (opens in a new tab) или LibFuzzer (opens in a new tab)) известны как эффективные инструменты для поиска ошибок.
Помимо чисто случайной генерации входных данных, существует множество методов и стратегий для создания качественных входных данных, в том числе:
- Получение обратной связи от каждого выполнения и использование ее для направления генерации. Например, если вновь сгенерированные входные данные приводят к обнаружению нового пути, имеет смысл сгенерировать новые входные данные, близкие к ним.
- Генерация входных данных с соблюдением структурных ограничений. Например, если ваши входные данные содержат заголовок с контрольной суммой, имеет смысл позволить фаззеру генерировать входные данные, проверяющие контрольную сумму.
- Использование известных входных данных для генерации новых: если у вас есть доступ к большому набору действительных входных данных, ваш фаззер может генерировать новые входные данные из них, а не начинать генерацию с нуля. Обычно их называют затравками.
Фаззинг на основе свойств
Echidna принадлежит к особому семейству фаззеров: фаззинг на основе свойств, в значительной степени вдохновленный QuickCheck (opens in a new tab). В отличие от классического фаззера, который пытается найти сбои, Echidna пытается нарушить определенные пользователем инварианты.
В смарт-контрактах инварианты — это функции Solidity, которые могут представлять любое неверное или недопустимое состояние, в которое может перейти контракт, в том числе:
- Неправильный контроль доступа: злоумышленник стал владельцем контракта.
- Неправильный конечный автомат: токены могут быть переданы, пока контракт приостановлен.
- Неправильная арифметика: пользователь может вызвать целочисленное опустошение (underflow) своего баланса и получить неограниченное количество бесплатных токенов.
Тестирование свойства с помощью Echidna
Мы рассмотрим, как тестировать смарт-контракт с помощью Echidna. Целью является следующий смарт-контракт 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}Показать всеМы сделаем предположение, что этот токен должен обладать следующими свойствами:
- Любой может иметь не более 1000 токенов
- Токен не может быть передан (это не токен ERC20)
Написание свойства
Свойства Echidna — это функции Solidity. Свойство должно:
- Не иметь аргументов
- Возвращать
trueв случае успеха - Иметь имя, начинающееся с
echidna
Echidna будет:
- Автоматически генерировать произвольные транзакции для проверки свойства.
- Сообщать о любых транзакциях, которые заставляют свойство возвращать
falseили вызывать ошибку. - Отбрасывать побочные эффекты при вызове свойства (то есть если свойство изменяет переменную состояния, это изменение отменяется после теста)
Следующее свойство проверяет, что у вызывающего не более 1000 токенов:
1function echidna_balance_under_1000() public view returns(bool){2 return balances[msg.sender] <= 1000;3}Используйте наследование, чтобы отделить ваш контракт от ваших свойств:
1contract TestToken is Token{2 function echidna_balance_under_1000() public view returns(bool){3 return balances[msg.sender] <= 1000;4 }5 }token.sol (opens in a new tab) реализует свойство и наследуется от токена.
Инициализация контракта
Echidna требуется конструктор без аргументов. Если вашему контракту требуется особая инициализация, ее необходимо выполнить в конструкторе.
В Echidna есть несколько особых адресов:
0x00a329c0648769A73afAc7F9381E08FB43dBEA72, который вызывает конструктор.0x10000,0x20000и0x00a329C0648769a73afAC7F9381e08fb43DBEA70, которые случайным образом вызывают другие функции.
В нашем текущем примере не требуется никакой особой инициализации, поэтому наш конструктор пуст.
Запуск Echidna
Echidna запускается с помощью:
echidna-test contract.solЕсли contract.sol содержит несколько контрактов, вы можете указать целевой контракт:
echidna-test contract.sol --contract MyContractИтог: тестирование свойства
Ниже приведены итоги запуска Echidna на нашем примере:
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()...Показать всеEchidna обнаружила, что свойство нарушается, если вызывается backdoor.
Фильтрация функций для вызова во время фаззинга
Мы рассмотрим, как фильтровать функции, подлежащие фаззингу. Целью является следующий смарт-контракт:
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}Показать всеЭтот небольшой пример заставляет Echidna найти определенную последовательность транзакций, чтобы изменить переменную состояния. Это сложно для фаззера (рекомендуется использовать инструмент символьного выполнения, такой как Manticore (opens in a new tab)). Мы можем запустить Echidna, чтобы проверить это:
echidna-test multi.sol...echidna_state4: passed! 🎉Seed: -3684648582249875403Фильтрация функций
Echidna испытывает трудности с поиском правильной последовательности для тестирования этого контракта, потому что две функции сброса (reset1 и reset2) установят все переменные состояния в false.
Однако мы можем использовать специальную функцию Echidna, чтобы либо добавить функции сброса в черный список, либо добавить в белый список только функции f, g,
h и i.
Чтобы добавить функции в черный список, мы можем использовать этот файл конфигурации:
1filterBlacklist: true2filterFunctions: ["reset1", "reset2"]Другой подход к фильтрации функций — перечислить функции из белого списка. Для этого мы можем использовать этот файл конфигурации:
1filterBlacklist: false2filterFunctions: ["f", "g", "h", "i"]filterBlacklistпо умолчанию имеет значениеtrue.- Фильтрация будет выполняться только по имени (без параметров). Если у вас есть
f()иf(uint256), фильтр"f"будет соответствовать обеим функциям.
Запуск Echidna
Чтобы запустить Echidna с файлом конфигурации blacklist.yaml:
echidna-test multi.sol --config blacklist.yaml...echidna_state4: failed!💥 Call sequence: f(12) g(8) h(42) i()Echidna почти сразу найдет последовательность транзакций, чтобы опровергнуть это свойство.
Итог: фильтрация функций
Echidna может либо добавлять функции в черный, либо в белый список для вызова во время фаззинга, используя:
1filterBlacklist: true2filterFunctions: ["f1", "f2", "f3"]echidna-test contract.sol --config config.yaml...Echidna начинает кампанию фаззинга, либо добавляя в черный список f1, f2 и f3, либо вызывая только их, в зависимости от значения логической переменной filterBlacklist.
Как тестировать утверждения (assert) Solidity с помощью Echidna
В этом коротком руководстве мы покажем, как использовать Echidna для тестирования проверки утверждений в контрактах. Предположим, у нас есть такой контракт:
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}Показать всеНаписание утверждения
Мы хотим убедиться, что tmp меньше или равно counter после возврата их разницы. Мы могли бы написать свойство
Echidna, но нам нужно было бы где-то хранить значение tmp. Вместо этого мы могли бы использовать такое утверждение:
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}Показать всеЗапуск Echidna
Чтобы включить тестирование сбоев утверждений, создайте файл конфигурации Echidna (opens in a new tab) config.yaml:
1checkAsserts: trueКогда мы запускаем этот контракт в Echidna, мы получаем ожидаемые результаты:
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: 1806480648350826486Показать всеКак вы можете видеть, Echidna сообщает о сбое утверждения в функции inc. Можно добавить более одного утверждения на функцию, но Echidna не сможет сказать, какое именно утверждение не удалось.
Когда и как использовать утверждения
Утверждения могут использоваться как альтернатива явным свойствам, особенно если условия для проверки напрямую связаны с правильным использованием некоторой операции f. Добавление утверждений после некоторого кода обеспечит, что проверка произойдет сразу после его выполнения:
1function f(..) public {2 // какой-то сложный код3 ...4 assert (condition);5 ...6}7Напротив, использование явного свойства Echidna приведет к случайному выполнению транзакций, и нет простого способа точно определить, когда будет выполнена проверка. Тем не менее, можно использовать такой обходной путь:
1function echidna_assert_after_f() public returns (bool) {2 f(..);3 return(condition);4}Однако есть некоторые проблемы:
- Это не сработает, если
fобъявлена какinternalилиexternal. - Непонятно, какие аргументы следует использовать для вызова
f. - Если
fотменяет транзакцию, свойство не будет выполнено.
В целом мы рекомендуем следовать рекомендациям Джона Регера (opens in a new tab) о том, как использовать утверждения:
- Не вызывайте никаких побочных эффектов во время проверки утверждения. Например:
assert(ChangeStateAndReturn() == 1) - Не утверждайте очевидные вещи. Например
assert(var >= 0), гдеvarобъявлена какuint.
Наконец, пожалуйста, не используйте require вместо assert, так как Echidna не сможет это обнаружить (но контракт все равно отменит транзакцию).
Итог: проверка утверждений
Ниже приведены итоги запуска Echidna на нашем примере:
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}Показать все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: 1806480648350826486Показать всеEchidna обнаружила, что утверждение в inc может не сработать, если эта функция вызывается несколько раз с большими аргументами.
Сбор и изменение корпуса Echidna
Мы рассмотрим, как собирать и использовать корпус транзакций с помощью Echidna. Целью является следующий смарт-контракт 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}Показать всеЭтот небольшой пример заставляет Echidna найти определенные значения для изменения переменной состояния. Это сложно для фаззера (рекомендуется использовать инструмент символьного выполнения, такой как Manticore (opens in a new tab)). Мы можем запустить Echidna, чтобы проверить это:
echidna-test magic.sol...echidna_magic_values: passed! 🎉Seed: 2221503356319272685Однако мы все еще можем использовать Echidna для сбора корпуса при запуске этой кампании фаззинга.
Сбор корпуса
Чтобы включить сбор корпуса, создайте каталог корпуса:
mkdir corpus-magicИ файл конфигурации Echidna (opens in a new tab) config.yaml:
1coverage: true2corpusDir: "corpus-magic"Теперь мы можем запустить наш инструмент и проверить собранный корпус:
echidna-test magic.sol --config config.yamlEchidna все еще не может найти правильные магические значения, но мы можем взглянуть на собранный ею корпус. Например, одним из этих файлов был:
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]Показать всеОчевидно, что эти входные данные не вызовут сбоя в нашем свойстве. Однако на следующем шаге мы увидим, как изменить его для этого.
Заполнение корпуса
Echidna нужна помощь, чтобы справиться с функцией magic. Мы скопируем и изменим входные данные, чтобы использовать для них подходящие
параметры:
cp corpus/2712688662897926208.txt corpus/new.txtМы изменим new.txt, чтобы вызывать magic(42,129,333,0). Теперь мы можем снова запустить Echidna:
echidna-test magic.sol --config config.yaml...echidna_magic_values: failed!💥 Call sequence: magic(42,129,333,0)Unique instructions: 142Unique codehashes: 1Seed: -7293830866560616537Показать всеНа этот раз он сразу обнаружил, что свойство нарушено.
Поиск транзакций с высоким потреблением газа
Мы рассмотрим, как найти транзакции с высоким потреблением газа с помощью Echidna. Целью является следующий смарт-контракт:
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}Показать всеЗдесь expensive может иметь большое потребление газа.
В настоящее время Echidna всегда требует свойство для тестирования: здесь echidna_test всегда возвращает true.
Мы можем запустить Echidna, чтобы проверить это:
1echidna-test gas.sol2...3echidna_test: passed! 🎉45Seed: 2320549945714142710Измерение потребления газа
Чтобы включить измерение потребления газа с помощью Echidna, создайте файл конфигурации config.yaml:
1estimateGas: trueВ этом примере мы также уменьшим размер последовательности транзакций, чтобы результаты было легче понять:
1seqLen: 22estimateGas: trueЗапуск Echidna
После создания файла конфигурации мы можем запустить Echidna следующим образом:
echidna-test gas.sol --config config.yaml...echidna_test: passed! 🎉f использовала максимум 1333608 газа Call sequence: f(42,123,249) Gas price: 0x10d5733f0a Time delay: 0x495e5 Block delay: 0x88b2Unique instructions: 157Unique codehashes: 1Seed: -325611019680165325Показать все- Показанный газ является оценкой, предоставленной HEVM (opens in a new tab).
Отфильтровывание вызовов, снижающих потребление газа
Приведенное выше руководство по фильтрации функций для вызова во время фаззинга показывает, как
удалить некоторые функции из вашего тестирования.
Это может быть критически важно для получения точной оценки газа.
Рассмотрим следующий пример:
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}Показать всеЕсли Echidna может вызывать все функции, она не сможет легко найти транзакции с высокой стоимостью газа:
1echidna-test pushpop.sol --config config.yaml2...3pop использовала максимум 10746 газа4...5check использовала максимум 23730 газа6...7clear использовала максимум 35916 газа8...9push использовала максимум 40839 газаПоказать всеЭто потому, что стоимость зависит от размера addrs, а случайные вызовы, как правило, оставляют массив почти пустым.
Однако добавление pop и clear в черный список дает нам гораздо лучшие результаты:
1filterBlacklist: true2filterFunctions: ["pop", "clear"]1echidna-test pushpop.sol --config config.yaml2...3push использовала максимум 40839 газа4...5check использовала максимум 1484472 газаИтог: поиск транзакций с высоким потреблением газа
Echidna может находить транзакции с высоким потреблением газа, используя опцию конфигурации estimateGas:
1estimateGas: trueechidna-test contract.sol --config config.yaml...Echidna сообщит о последовательности с максимальным потреблением газа для каждой функции после завершения кампании фаззинга.
Последнее обновление страницы: 21 октября 2025 г.