Резюме управління
FatCats зв’язалися з Sayfer Security, щоб провести аудит смарт-контрактів для їх контракту NFT у мережі Ethereum
За 3 тижні дослідження ми виявили 6 вразливостей у контракті. Одна вразливість була класифікована як висока, що дозволило зловмиснику отримати доступ до метаданих NFT до того, як вони були виявлені, і зробити розумні прогнози щодо airdrops або певних трансакцій карбування для рідкісних NFT.
Більшість уразливостей актуальні лише під час створення нових NFT, тому ми настійно рекомендуємо виправити вразливості, знайдені в цьому звіті, перш ніж створювати нові.
Уразливості за ризиком
Критичний – Експлуатація безпосередньої або поточної частини бізнесу з прямими ключовими збитками для бізнесу.
Високий – Пряма загроза ключовим бізнес-процесам.
Medium – Непряма загроза ключовим бізнес-процесам або часткова загроза бізнес-процесам.
низький – Прямої загрози немає. Уразливість може бути використана за допомогою інших уразливостей.
Інформаційний – Цей висновок не вказує на вразливість, але містить коментар, який сповіщає про недоліки дизайну та неправильну реалізацію, які можуть спричинити проблему в довгостроковій перспективі.
Підхід
Огляд протоколу
Вступ до протоколу
Fat Cats — це колекція з 5,000 унікальних NFT, які одночасно є маркером членства та часткою всіх холдингів Fat Cats. Володіння Fat Cats NFT дає вам доступ до частки всіх активів DAO за доступною ціною.
Усі ліквідні кошти зберігатимуться в кошику стабільних монет та інших криптоактивів, як члени вважають за потрібне. У разі ухвалення термінових рішень Рада може витратити до 20% усіх умовних коштів.
Контракт FatCats – це контракт NFT-токенів, який має етапи карбування – крок 1, крок 2 і публічний монетний двір. Етапи карбування встановлює власник. Користувачі з білого списку можуть карбувати NFT на кроці 1 і 2. Користувачі з білого списку перевіряються перевіркою Merkle.
Контракт FatCats успадковує MerkleProof, Ownable, Address, VRFCoordinatorV2Interface, VRFConsumerBaseV2, IERC721Receiver, Context, IERC721Metadata, Strings, ERC165, IERC721, стандартні смарт-контракти з бібліотеки OpenZeppelin. Ці контракти OpenZeppelin вважаються перевіреними спільнотою та часом, а отже, не входять до сфери аудиту.
Графік протоколу
Оцінка безпеки
Наступні тестові приклади були орієнтиром під час аудиту системи. Цей контрольний список є модифікованою версією SCSVS v1.2, з покращеною граматикою, ясністю, лаконічністю та додатковими критеріями. Там, де є прогалина в нумерації, вихідний критерій було видалено. Критерії, позначені зірочкою, були додані нами.
Архітектура, дизайн і моделювання загроз
Архітектура, дизайн і моделювання загроз | Назва тесту |
G1.2 | Кожній введеній зміні дизайну передує моделювання загроз. |
G1.3 | Документація чітко та точно визначає всі межі довіри в контракті (довірчі відносини з іншими контрактами та значні потоки даних). |
G1.4 | SCSVS, вимоги безпеки або політика доступні для всіх розробників і тестувальників. |
G1.5 | Визначено події для операцій (зміна стану/важливих для бізнесу). |
G1.6 | Проект містить механізм, який може тимчасово зупиняти конфіденційні функції у разі атаки. Цей механізм не повинен блокувати доступ користувачів до їхніх активів (наприклад, токенів). |
G1.7 | Кількість невикористаних криптовалют, що зберігаються на контракті, контролюється і знаходиться на мінімально прийнятному рівні, щоб не стати потенційною мішенню для атаки. |
G1.8 | Якщо будь-хто може викликати резервну функцію, вона включена в модель загрози. |
G1.9 | Бізнес-логіка послідовна. Важливі зміни в логіці мають бути застосовані до всіх контрактів. |
G1.10 | Для виявлення вразливостей використовуються засоби автоматичного аналізу коду. |
G1.11 | Використовується останній великий випуск Solidity. |
G1.12 | У разі використання зовнішньої реалізації контракту використовується найновіша версія. |
G1.13 | Коли функції замінюються для розширення функціональності, ключове слово super використовується для підтримки попередньої функціональності. |
G1.14 | Порядок успадкування ретельно уточнюється. |
G1.15 | Існує компонент, який відстежує діяльність контракту за допомогою подій. |
G1.16 | Модель загроз включає операції з китами. |
G1.17 | Витік одного закритого ключа не ставить під загрозу безпеку всього проекту. |
Політика та процедури
Політика та процедури | Назва тесту |
G2.2 | Безпека системи знаходиться під постійним моніторингом (наприклад, очікуваний рівень коштів). |
G2.3 | Існує політика відстеження нових вразливостей безпеки та оновлення бібліотек до останньої захищеної версії. |
G2.4 | З відділом безпеки можна публічно зв’язатися, а процедура обробки повідомлених помилок (наприклад, ретельна винагорода за помилку) чітко визначена. |
G2.5 | Процес додавання нових компонентів до системи чітко визначений. |
G2.6 | Процес основних системних змін передбачає моделювання загроз зовнішньою компанією. |
G2.7 | Процес додавання та оновлення компонентів системи включає аудит безпеки сторонньою компанією. |
G2.8 | У разі злому існує чітка та добре відома процедура запобігання. |
G2.9 | Процедура у разі злому чітко визначає, які особи мають виконати необхідні дії. |
G2.10 | Процедура включає сповіщення інших проектів про злом через надійні канали. |
G2.11 | Визначено процедуру запобігання витоку приватного ключа. |
Можливість модернізації
Можливість модернізації | Назва тесту |
G3.2 | Перед оновленням виконується емуляція у форку основної мережі, і все працює належним чином на локальній копії. |
G3.3 | Процес оновлення виконується за багатопідписним контрактом, де кілька осіб повинні схвалити операцію. |
G3.4 | Часові блокування використовуються для важливих операцій, щоб користувачі мали час поспостерігати за майбутніми змінами (зверніть увагу, що усунення потенційних уразливостей у цьому випадку може бути складнішим). |
G3.5 | ініціалізувати() можна викликати лише один раз. |
G3.6 | ініціалізувати() може викликатися лише авторизованою роллю за допомогою відповідних модифікаторів (наприклад, ініціалізатор, тільки власник). |
G3.7 | Процес оновлення виконується за одну транзакцію, тому ніхто не може його запустити. |
G3.8 | Контракти з можливістю оновлення мають зарезервований проміжок у слотах для запобігання перезапису. |
G3.9 | Кількість зарезервованих (як проміжок) слотів було відповідно зменшено, якщо додано нові змінні. |
G3.10 | Немає змін ні в порядку оголошення змінних стану контракту, ні в їх типах. |
G3.11 | Нові значення, які повертаються функціями, такі ж, як і в попередніх версіях контракту (наприклад, власник(), баланс(адреса)). |
G3.12 | Реалізацію ініціалізовано. |
G3.13 | Реалізація не може бути знищена. |
Бізнес-логіка
Бізнес-логіка | Назва тесту |
G4.2 | Логіка контракту та реалізація параметрів протоколу відповідають документації. |
G4.3 | Бізнес-логіка виконується в порядку послідовних кроків, і неможливо пропустити кроки або виконати їх у порядку, відмінному від запланованого. |
G4.4 | У контракті належним чином встановлені обмеження щодо діяльності. |
G4.5 | Бізнес-логіка не покладається на значення, отримані з ненадійних контрактів (особливо, коли є кілька викликів одного контракту в одному потоці). |
G4.6 | Бізнес-логіка не покладається на баланс контракту (наприклад, баланс == 0). |
G4.7 | Конфіденційні операції не залежать від даних блоку (наприклад, хеш блоку, мітка часу). |
G4.8 | Контракт використовує механізми, які пом’якшують атаки на замовлення транзакцій (передні) (наприклад, схеми попередньої фіксації). |
G4.9 | Контракт не надсилає кошти автоматично, а дозволяє користувачам знімати кошти окремими транзакціями. |
Управління доступом
Управління доступом | Назва тесту |
G5.2 | Дотримується принцип найменших привілеїв. Інші контракти повинні мати доступ лише до функцій і даних, для яких вони мають спеціальні повноваження. |
G5.3 | Нові контракти з доступом до перевіреного контракту за умовчанням дотримуються принципу мінімальних прав. Контракти повинні мати мінімальні дозволи або не мати дозволів, доки доступ до нових функцій не буде явно надано. |
G5.4 | Творець контракту дотримується принципу найменших привілеїв, і його права суворо відповідають викладеним у документації. |
G5.5 | Контракт забезпечує дотримання правил контролю доступу, визначених у довіреному контракті, особливо якщо клієнтський контроль доступу dApp присутній і його можна обійти. |
G5.6 | Дзвінки на зовнішні контракти дозволені лише за необхідності. |
G5.7 | Код модифікатора зрозумілий і простий. Логіка не повинна містити зовнішніх викликів ненадійних контрактів. |
G5.8 | Усі атрибути користувача та даних, які використовуються елементами керування доступом, зберігаються в довірених контрактах, і ними не можна маніпулювати за допомогою інших контрактів, якщо немає спеціального дозволу. |
G5.9 | Елементи керування доступом безпечно виходять з ладу, зокрема, коли відбувається повернення. |
G5.10 | Якщо вхідні дані (параметри функції) перевірено, підхід позитивної перевірки (білий список) використовується, де це можливо. |
Комунікація
Комунікація | Назва тесту |
G6.2 | Визначаються бібліотеки, які не є частиною програми (але смарт-контракт покладається на роботу). |
G6.3 | Делегований виклик не використовується з недовіреними контрактами. |
G6.4 | Контракти третіх сторін не затьмарюють спеціальні функції (наприклад, повернення). |
G6.5 | Контракт не перевіряє, чи є адреса контрактом, використовуючи код операції extcodesize. |
G6.6 | Атаки повторного входу пом’якшуються блокуванням рекурсивних викликів з інших контрактів і дотриманням шаблону перевірки-ефекти-взаємодії. Не використовуйте функцію надсилання, якщо це не обов’язково. |
G6.7 | Результат викликів функцій низького рівня (наприклад, надсилати, делегувати виклик, телефонувати) з інших договорів перевіряється. |
G6.8 | Контракт спирається на дані, надані правильним відправником, і не покладається на значення tx.origin. |
Арифметика
Арифметика | Назва тесту |
G7.2 | Значення та математичні операції стійкі до цілочисельних переповнень. Використовуйте бібліотеку SafeMath для арифметичних операцій до solidity 0.8.*. |
G7.3 | Неперевірені фрагменти коду з Solidity ≥ 0.8.* не вводять цілочисельні переповнення/переповнення. |
G7.4 | Екстремальні значення (наприклад, максимальне та мінімальне значення типу змінної) розглядаються та не змінюють логіку контракту |
G7.5 | Для балансової рівності використовується нестрога нерівність. |
G7.6 | У розрахунках використовуються правильні порядки величин. |
G7.7 | У обчисленнях для точності множення виконується перед діленням. |
G7.8 | Контракт не передбачає точності з фіксованою комою та використовує множник або зберігає як чисельник, так і знаменник. |
Відмова в обслуговуванні
Відмова в обслуговуванні | Назва тесту |
G8.2 | Контракт не повторює необмежені цикли. |
G8.3 | Функція самознищення використовується лише за необхідності. Якщо це включено в договір, це має бути чітко описано в документації. |
G8.4 | Бізнес-логіка не блокується, якщо актор (наприклад, контракт, обліковий запис, оракул) відсутній. |
G8.5 | Бізнес-логіка не перешкоджає користувачам використовувати контракти (наприклад, вартість транзакції вища за прибуток). |
G8.6 | Вирази функцій assert або require мають перехідний варіант. |
G8.7 | Якщо ніхто не може викликати резервну функцію, це не блокує функції контракту. |
G8.8 | У циклі немає дорогих операцій. |
G8.9 | У циклі немає викликів ненадійних контрактів. |
G8.10 | Якщо є можливість призупинення дії договору, його також можна відновити. |
G8.11 | Якщо використовуються білі та чорні списки, вони не заважають нормальній роботі системи. |
G8.12 | Немає DoS, спричиненого переповненням і недоповненням. |
Дані блокчейну
Дані блокчейну | Назва тесту |
G9.2 | Будь-які дані, збережені в контрактах, не вважаються безпечними або приватними (навіть приватні змінні). |
G9.3 | У блокчейні не зберігаються конфіденційні дані (паролі, персональні дані, токени тощо). |
G9.4 | У контрактах не використовуються рядкові літерали як ключі для зіставлення. Замість цього використовуються глобальні константи, щоб запобігти атаці гомогліфів. |
G9.5 | Контракт не генерує тривіально псевдовипадкові числа на основі інформації з блокчейну (наприклад, заповнення номером блоку). |
Використання газу та обмеження
Використання газу та обмеження | Назва тесту |
G10.1 | Споживання газу передбачено, визначено та має чіткі обмеження, які не можна перевищувати. І структура коду, і зловмисне введення не повинні спричиняти виснаження газу. |
G10.2 | Виконання функцій і функціональність не залежать від жорстко закодованих тарифів на газ (вони можуть змінюватися). |
Чіткість і читабельність
Чіткість і читабельність | Назва тесту |
G11.2 | Логіка чітка та модульна в кількох простих контрактах і функціях. |
G11.3 | До кожного договору є короткий коментар із 1-2 речень, який пояснює його призначення та функції. |
G11.4 | Використовуються готові реалізації, це пояснюється в коментарях. Якщо ці реалізації були змінені, ці зміни зазначаються в угоді. |
G11.5 | Порядок успадкування враховується в контрактах, які використовують множинне успадкування та тіньові функції. |
G11.6 | Там, де це можливо, контракти використовують існуючий перевірений код (наприклад, маркерні контракти або механізми, як у власність) замість впровадження власних. |
G11.7 | Послідовні шаблони іменування дотримуються протягом усього проекту. |
G11.8 | Змінні мають відмінні імена. |
G11.9 | Усі змінні пам’яті ініціалізовано. |
G11.10 | Функції з указаним типом повернення повертають значення цього типу. |
G11.11 | Використовуються всі функції та змінні. |
G11.12 | вимагати використовується замість повернути in if заяви. |
G11.13 | Команда стверджувати функція використовується для перевірки внутрішніх помилок і вимагати функція використовується для забезпечення дійсності умов у введених користувачами та зовнішніх контрактах. |
G11.14 | Код складання використовується лише за необхідності. |
Покриття тесту
Покриття тесту | Назва тесту |
G12.2 | Історії зловживань, детально описані в моделі загроз, охоплюються модульними тестами |
G12.3 | Делікатні функції в перевірених контрактах охоплюються тестами на етапі розробки. |
G12.4 | Виконання перевірених контрактів було перевірено на наявність уразливостей безпеки за допомогою статичного та динамічного аналізу. |
G12.5 | Контрактну специфікацію було офіційно перевірено |
G12.6 | Специфікація та результати формальної перевірки включені до документації. |
Децентралізовані фінанси
Децентралізовані фінанси | Назва тесту |
G13.1 | Договір кредитора не передбачає зміни його балансу (використовується для підтвердження погашення кредиту) лише з його власними функціями. |
G13.2 | Функції, які змінюють баланс кредитора та/або позичають криптовалюту, не підлягають повторному входу, якщо смарт-контракт дозволяє позичати криптовалюту основної платформи (наприклад, Ethereum). Він блокує атаки, які оновлюють баланс позичальника під час оформлення флеш-кредиту. |
G13.3 | Функції флеш-позики можуть викликати лише попередньо визначені функції в договорі отримання. Якщо це можливо, визначте довірену підмножину контрактів, які потрібно викликати. Зазвичай передзвонюється договір відправлення (позики). |
G13.4 | Якщо він включає потенційно небезпечні операції (наприклад, повернення більше ETH/токенів, ніж запозичено), функція одержувача, яка обробляє запозичені ETH або токени, може бути викликана лише пулом і в рамках процесу, ініційованого власником контракту-одержувача або іншим надійним джерелом (наприклад, multisig). |
G13.5 | Розрахунок частки пулу ліквідності виконується з максимально можливою точністю (наприклад, якщо внесок розраховується для ETH, це має бути зроблено з точністю до 18 цифр – для Wei, а не для Ether). Ділене потрібно помножити на 10 у степені кількості десяткових цифр (наприклад, ділене * 10^18 / дільник). |
G13.6 | Винагороди не можна розрахувати та розподілити в межах того самого виклику функції, який депонує токени (його також слід визначити як без повторного входу). Це захищає від миттєвих коливань акцій. |
G13.7 | Управлінні контракти захищені від атак швидкої позики. Одним із можливих методів пом’якшення є вимога процесу депонування токенів управління та пропозиції змін, які мають бути виконані в різних транзакціях, включених до різних блоків. |
G13.8 | При використанні on-chain оракул контракти можуть призупиняти операції на основі результату оракула (у разі скомпрометованого оракула). |
G13.9 | Зовнішні контракти (навіть довірені), яким дозволено змінювати атрибути контракту проекту (наприклад, ціну токена), мають такі обмеження: порогові значення для зміни (наприклад, не більше/менше 5%) та обмеження оновлень (наприклад, одне оновлення на день). |
G13.10 | Атрибути контракту, які можуть бути оновлені зовнішніми контрактами (навіть надійними), відстежуються (наприклад, за допомогою подій), і реалізується процедура реагування на інцидент (наприклад, під час поточної атаки). |
G13.11 | Складні математичні операції, які складаються з операцій множення та ділення, спочатку виконують множення, а потім ділення. |
G13.12 | Під час розрахунку біржових цін (наприклад, ETH на токен або навпаки), чисельник і знаменник множаться на резерви (див. getInputPrice функція в UniswapExchange договір). |
Замовити аудит у Сайфер
Висновки аудиту
URI, які можна вгадати
Статус | відкритий |
Risk | Критичний |
Місце розташування | FatCats.sol |
Tools | Ручне тестування |
Опис
Наявність передових знань про метадані NFT може дати зловмисникові знати, який NFT йому слід карбувати до оприлюднення. Це потенційно може дозволити зловмиснику отримати контроль над найціннішими NFT у проекті.
Наступна вразливість дозволяє зловмиснику вирішити, який NFT купити на основі метаданих до оприлюднення. Цю вразливість відносно легко використовувати через інші вразливості, які ми знайшли. У проміжку часу між shuffle
прапор повертається requestRandomWords()
і NFT, які розкриваються в revealNFT(),
зловмисник може вгадати URI маркера.
У той час як revealed
прапор все ще хибний tokenURI()
Умови повернення hideUri
, що означає, що середній користувач бачитиме лише фіктивний/прихований URI без повних метаданих.
Щоб повністю створити рядок URI маркера, виконується інша транзакція для виконання requestRandomWords()
метод, який встановлює s_randomWords
через fulfillRandomWords()
. Це робиться через VRF Chianlink.
Лише тоді виконується наступний код, який повертає повний рядок URI маркера:
Вразливість полягає в тому baseURI
, maxSupply
та s_randomWords
є загальнодоступними, і зловмисник може передбачити URI маркера без потреби revealed
змінна. У V1 проекту ми бачили, що перша виконується транзакція requestRandomWords()
:
А потім друга транзакція, яка розкриває NFT шляхом виконання revealNFT()
:
Ми бачимо, що протягом 3 годин зловмисник міг переглянути будь-які метадані NFT до того, як вони були виявлені, і зробити розумні прогнози щодо airdrops або конкретних трансакцій карбування для рідкісних NFT.
Пом'якшення
Розкриття NFT не має універсального розміру. Це дуже залежить від стратегії та досвіду користувача, який повинен мати кінцевий користувач.
Для кращої безпеки ми рекомендуємо відкрити, встановити базовий URI та змінити стан s_randomWords
змінна стану з найкоротшою різницею в часі між ними. Незважаючи на те, що вразливість не зменшується повністю, скорочення часу між цими діями може зменшити ризик використання зловмисниками.
Кращий спосіб — зробити всі зміни стану в тій самій транзакції під час розкриття, часто це технічно неможливо.
Щоб глибше зануритися в те, як реалізувати випадковий аірдроп NFT і більш безпечні способи, ми рекомендуємо прочитати Стратегії рандомізації для випадання NFT.
Адреси з білого списку можуть бути контрактами
Статус | відкритий |
Risk | Високий |
Місце розташування | FatCats.sol |
Tools | Ручне тестування |
Опис
Під час карбування NFT контракт на карбування може блокувати або дозволяти карбування на адреси контракту. Дозвол карбування контракту або наявність уразливості, яка дозволяє карбування контракту, може призвести до ситуації, коли зловмисник зможе скасувати транзакцію, якщо він попередньо знає, які NFT йому слід карбувати.
Модифікатор isAUser()
використовується лише для publicMint()
.
для mintStep1()
та mintStep2()
методи isWhitelisted()
використовується модифікатор.
Це означає, що код не перевіряє неконтрактні адреси в білому списку доказових хешів Merkle.
Хоча ми не можемо знати, які процеси стояли за створенням білого списку та наскільки він захищений, сам код вразливий до цієї атаки. Перевірка неконтрактних адрес поза ланцюгом або не в тій самій транзакції не рекомендується, оскільки вона вразлива до кількох векторів атак.
Зловмисник може отримати NFT і скасувати транзакцію, якщо NFT недостатньо рідкісний. У поєднанні з Guessable Token URI зловмисник може легко отримати попередні знання про метадані NFT і скасувати транзакції на основі рідкості.
Пом'якшення
Додати isAUser
модифікатор до mintStep1()
та mintStep2()
методи
Слабка незахищена випадкова реалізація VRF Chainlink
Статус | відкритий |
Risk | Високий |
Місце розташування | FatCats.sol |
Tools | Ручне тестування |
Опис
Помилки незахищеної випадковості виникають, коли функція, яка може створювати передбачувані значення, використовується як джерело випадковості в чутливому до безпеки контексті.
Розумні контракти повинні покладатися на рішення поза мережею для генерації захищених випадкових чисел. Fatcats використовує систему VRF Chainlink для генерації випадкового числа, яке пізніше використовується як випадковий ідентифікатор для URI маркера за допомогою fulfillRandomWords
метод:
Метод зберігає випадкове число у змінній стану, що викликається s_randomWords
яке з назви означає, що це випадкове слово. На практиці число за модулем з maxSupply
(5000), тож це псевдослабке випадкове число від 1 до 5000. Це може спонукати розробників до думки, що вони можуть покладатися на це число для безпечної випадковості, але це не так.
Пом'якшення
Призначте значення, що повертається з VRF, глобальній змінній стану.
Централізація контракту та командного гаманця
Статус | відкритий |
Risk | Високий |
Місце розташування | FatCats.sol |
Tools | Ручне тестування |
Опис
Проекти, які покладаються на один ключ, уразливі до втрати ключів, фішингових атак, внутрішніх маніпуляцій акторів, смерті власника тощо.
Коли проект залежить тільки від одного ключа. Ці види атак є найпоширенішими з найбільших хакерів.
Проект перевіряє isOwner
у кількох місцях. Втрата ключів від цієї адреси розгортання скомпрометує весь проект.
Крім того, командний гаманець, який отримає eth за допомогою withdraw
функція також піддається тому ж вектору атаки
Якщо вищевказані гаманці використовують multisig, це відкриття буде вирішено.
Пом'якшення
Залежно від сценарію використання, рівня безпеки та взаємодії з користувачем, яку хоче застосувати проект, є кілька способів пом’якшити цю атаку, наприклад, використовуючи гаманець із кількома підписами, розумний гаманець або застосувавши до проекту кількох власників.
Функція openPublicBurn перемикає, а не відкриває
Статус | відкритий |
Risk | низький |
Місце розташування | FatCats.sol |
Tools | Ручне тестування |
Опис
Метод openPublicBurn
не просто відкриває публічне записування, воно вмикає та вимикає його залежно від поточного стану змінної.
Це може змусити розробника або оператора проекту подумати, що вони відкривають етап публічного запису, але насправді вимикають його, що спричиняє погану репутацію проекту.
Пом'якшення
Встановіть змінну стану publicBurnFlag
на true або змініть назву методу на
switchPublicBurn
.
Східчастий механізм важко обслуговувати
Статус | відкритий |
Risk | інформація |
Місце розташування | FatCats.sol |
Tools | Ручне тестування |
Опис
Код використовує логічні змінні для кожного кроку, це означає, що щоразу, коли розробник додає ще один крок, він/вона має додати логіку для перемикання скидання прапорів. Це може призвести до плутанини та потенційних помилок безпеки.
Кращим підходом було б використовувати enum як поточний крок, якщо це дає переваги чіткої назви кроку, наприклад EARLY_ADAPTERS
, PUBLINK_MINT
І т.д.
Якщо це надто багатослівно або заплутано, це можна реалізувати за допомогою простого цілого числа, це має математичну перевагу if step > 2
or require(newStep < oldStep, “can not go back”.
Пом'якшення
Використовуйте вбудований синтаксис enum або простий цілочисельний синтаксис.