ефір

19 липня 2017 року Parity піддався хакерській атаці, в ході якої зловмисникам вдалося вкрасти 153,037 ефірів, що на той момент складало близько $ 30 мільйонів. Її назвали «другою найбільшою атакою в історії мережі Ethereum за кількістю вкраденого ефірів». Вразливість, що дозволила здійснити атаку, містилася в коді гаманця з мультипідписом, тому атака також відома як «MultiSig Hack». Взагалі, злочини в криптосвіті завдають дуже великої репутаційної шкоди технології, що розвивається. Про це ми писали в статтях:

Сантьяго Палладіно, дослідник з безпеки в компанії, що займається рішеннями для створення і управління смарт-контрактами, Zeppelin, коротко описав процес атаки: хакер відправив по дві транзакції в кожен потерпілий контракт. Перша з них, що містила виклик функції initWallet, забезпечувала йому виключне право на володіння гаманцем з мультипідписом, а друга дозволяла перевести все, що зберігається на цьому гаманці на потрібну адресу.

Далі, через пристрої MultiSig-гаманців Parity, гаманець перенаправляє всі виклики на інший контракт - контракт бібліотеки. Як пояснює Едуард Каріон, експерт з токеноміки, блокчейну і смарт-контрактів, саме в бібліотеці «реалізована вся функціональність», що дозволяє не писати окремий код для кожного гаманця: «Кожен окремий parity-гаманець був просто такою оболонкою, яка делегує всі виклики в основний контракт бібліотеки через delegatecall в його резервні функції», - пише Каріон.

При цьому суть механізму delegatecall полягає в тому, що він працює з даними самого гаманця - тобто застосовує функціональність бібліотеки (її код) до конкретного гаманця, його балансу і даних власника. Відповідно, всі викликані функції повинні бути загальнодоступними, тому метод initWallet, що відповідає за ініціалізацію (підготовку до використання) гаманця, був також загальнодоступним: «Передбачається, що метод буде викликатися тільки один раз під час створення контракту гаманця. Тобто я хочу завести новий гаманець, я відправляю в Ethereum транзакцію, де написано "Заведи новий гаманець, який посилається ось на цю бібліотеку", і автоматично в момент створення мого контракту, мого гаманця спрацьовує функція initWallet в бібліотеці, яка призначає мене в якості власника цього гаманця», - пише Каріон.

Однак жодних механізмів для перевірки того, що цей процес виконується тільки один раз, не було передбачено. Тобто, по суті, будь-який користувач міг запустити ініціалізацію (initWallet) повторно і стати власником гаманця, що і зробив хакер.

Parity визнали, що злам став можливим саме через цю помилку в коді: «Баг знаходився в двох вкрай чутливих функціях, розроблених для установки гаманців з мультипідписами в ПЗ гаманця Parity. Функції повинні були бути захищені так, щоб вони могли використовуватися тільки в одному випадку - при створенні контракту. Однак, вони були не повністю захищені, що дозволило хакеру довільно перевстановити параметри власника і використання», - пояснила команда Parity.

Після цього хакеру залишалося тільки викликати функцію execute, яка виконала переказ коштів на потрібний йому аккаунт.

На наступний день після атаки команда Parity опублікувала нову версію коду, але в ній також містилася вразливість, яка була активована 6 листопада. У цей день розробник під ніком devops199, не бувши членом команди і маючи порожній аккаунт на GitHub, написав, що «випадково вбив» контракт. Деякий час він брав участь в обговоренні під своїм постом, і на питання про те, навіщо він це зробив, devops199 відповів, що він «новачок в Ethereum» і «лише вивчає» систему.

Однак цьому вірять не всі. Засновник Thetta - фреймворка для створення DAO - вважає, що «ненавмисно» виконати всі ті кроки, що виконав devops199, неможливо: «@devops199 "випадково "викликав метод initWallet (), щоб заволодіти бібліотекою https://etherscan.io/tx/0x05f71e1b2cb4f03e547739db15d080fd30c989eda04d37ce6264c5686e0722c9, @ devops199 "випадково" викликав метод kill (), щоб вона самознищилася https://etherscan.io/tx/0x47f7cff7a5e671884629c93b368cb18f58a993f4b19c2a53a8662e3f1482f690 », - написав він. В обговоренні на GitHub він провів таку аналогію:

«1. Ви йдете і бачите, що двері банку відкриті
2. потім ви заходите всередину (виклик першого методу)
3. потім ви спалюєте всі гроші (виклик другого методу)
Чи повірить ФБР, що це було "ненавмисно"?»

Внутрішнє розслідування проекту Cappasity, який на момент атаки проводив токенсейл, також виявило навмисність дій хакера. На сьогодні аккаунт devops199 видалений.

Було також відзначено, що під час другої атаки проблема полягала в некоректній ініціалізації контракту бібліотеки, через що будь-який користувач міг стати його власником і знищити його.

В офіційному оголошенні команда Parity повідомила, що постраждали ті гаманці з мультипідписами, які були створені після 20 липня, тобто використовували модифікований після першого злому код: «На жаль, цей код містив ще одну вразливість... - він дозволяв перетворити контракт бібліотеки гаманця Parity в звичайний гаманець з мультипідписами і стати його власником, викликавши функцію initWallet, - написали Parity. - Користувач знищив код бібліотеки, що в свою чергу зробило всі контракти [гаманці] з мультипідписами невикористовуваними і заморозило кошти, оскільки їхня логіка (всі функції для зміни стану) перебувала всередині бібліотеки». В результаті власники цих гаманців втратили можливість виводити маркери. Замороженими виявилися 513,744 ефірів.

У грудні CEO Parity Ютта Штайнер обіцяла, що доступ до засобів буде відкритий після планового апргейду через 4-6 місяців, проте цього не сталося. Проблема так і не вирішена, а суперечки щодо способу повернення коштів викликали розкол в суспільстві Ethereum, в тому числі серед розробників: в лютому Йоіті Хіра покинув пост редактора коду EIP 867 - пропозиції щодо вдосконалення Ethereum, яке також торкалося способів повернення коштів.

А в кінці квітня, на зустрічі розробників Ethereum, суперечки розгорілися вже навколо іншої пропозиції - EIP 999: багато учасників зустрічі були впевнені, що прийняття EIP 999 спровокує розкол мережі, тобто відбудеться хардфорк, так як два основних клієнта Ethereum - Parity і Geth - дотримуються різних поглядів щодо цієї пропозиції: «Ми говоримо про одну й ту ж мережу і, по суті, починаємо міжплемінну війну. Я не думаю, що ми зможемо домовитися», - сказав провідний розробник Geth Петер Шілагій.

Сьогодні велика частина обговорень зводиться до листопадового заморожування коштів, оскільки - навмисним воно було чи ні - кошти не були викрадені: вони так і дражнять співтовариство, перебуваючи на тих самих адресах. Однак, джерело цієї «міжплемінної війни» - 19 липень 2017 року, коли було здійснен першу атаку: саме це призвело до необхідності міняти код, баг в якому навів до нового інциденту і - без великого перебільшення - сформував спільноту Ethereum в її сьогоднішньому вигляді.

У березні співзасновник проекту токенізованих ланцюжків поставок Nuclo Девон Уеслі відтворив код липневого злому Parity, і CryptoHacker дає адаптацію цього матеріалу. Оригінал передбачає, що читач має базові знання в програмуванні, а також в блокчейн-програмуванні і мові написання смарт-контрактів Ethereum - Solidity, тому адаптованій версії передує невеликий словник:

Консоль, або CLI (англ. Command line interface, інтерфейс командного рядка) - текстовий інтерфейс, в якому можна ввести команду на мові програмування (наприклад, JavaScript), і вона буде виконана. Для необізнаного користувача - це віконце з чорним екраном і рядками коду, але консольний інтерфейс може ніколи вам не зустрітися, так як сьогодні всі програми і додатки, націлені на широку аудиторію, мають красивий і зручний графічний інтерфейс - тобто картинки, кнопки та інші атрибути дизайну, що забезпечують інтуїтивну взаємодію.

init (скорочення від initialization) - система ініціалізації в ряді операційних систем, що відповідає за приведення програми або пристрою в стан готовності до використання. У контексті статті «initWallet» буде командою для ініціалізації гаманця.

Фреймворк, дослівно «каркас» - це ПЗ, що спрощує створення продуктів, наприклад, смарт-контрактів або децентралізованих додатків.

Резервна функція, або fallback function, в Solidity - це функція, яка виконується, коли при виклику до контракту невиявлений відповідний ідентифікатор. Наприклад, якщо код виглядає так: address.call (bytes4 (bytes32 (sha3 ( "thisShouldBeAFunction (uint, bytes32)"))), 1, "test"), то ідентифікатор - thisShouldBeAFunction. Віртуальна машина Ethereum (EVM) спробує викликати з контракту функцію з таким ідентифікатором. Якщо її не існує, то викликається резервна функція. У контексті статті йтиметься про резервну функцію з модифікатором «payable»: даний модифікатор означає, що функція може приймати ефір. У коді вона буде позначатися так:

функція

де msg.value позначає, скільки саме прийшло ефіру.

Функція і метод - це підпрограми, які викликаються основною програмою для виконання якихось дій. Являють собою фрагменти коду, які можуть працювати з вихідними даними і повертати результат своєї роботи у вигляді певного значення.

Менеджер пакетів - ПЗ для управління пакетами, тобто наборами файлів.

Середовище виконання - набір інструкцій, які виконуються для перекладу написаного програмістом коду в код, зрозумілий комп'ютеру (в даній статті - в код, зрозумілий віртуальній машині V8).

Адаптація матеріалу Medium

Баги в Solidity дорого обходяться, піддаючи ризику вас самих і багатьох інших, тому важливо вжити заходів обережності при написанні і розгортанні смарт-контрактів. Ми розглянемо один з таких багів, «експлойт для гаманця з мультипідписом», і напишемо код його спрощеного сценарію, використовуючи два смарт-контракти: контракт гаманця і контракт бібліотеки гаманця.

Примітки:

Передбачається, що у вас є базове розуміння технологій в основі блокчейну Ethereum і мови програмування для написання смарт-контрактів Solidity, який компілюється в байт-код EVM.

Я використовую Mac - вибачте, і я вас попередив.

NodeJS - Виконавча, що використовує JavaScript-движок V8, вам знадобиться версія 6.9.1 або пізніші.

Пакети NPM (менеджер пакетів, що входить до складу Node.js.):

NPM

Перший пакет, який ми встановлюємо, - Ganache, «Ethereum-клієнт на базі NodeJS для тестування і розробки». Ganache - це приватний блокчейн з власним генезисним блоком, який повністю повторює функціонал основного блокчейну Ethereum (використовується розробниками для тестування).

NodeJS

Другий пакет - Truffle - це фреймворк для тестування і розгортання, створений з метою полегшити для розробників розгортання і управління смарт-контрактами. Ми будемо використовувати цей фреймворк і консоль, яку він надає: та ж консоль, що в NodeJS, але з парою вбудованих додаткових пакетів.

Два контракти, які ми використовуємо, є спрощеними прикладами, а НЕ справжніми контрактами, на які була здійснена атака. Ці два приклади взяті з блогу «Hacking, Distributed», де дано відмінне докладне пояснення липневої атаки.

Починаємо:

Ми виконаємо кілька команд для скаффолдингу (в даному випадку «структуризації») нашого проекту.

скаффолдинг

Вищенаведені команди створюють нашу папку проекту і потім зберігають зміни, внесені в проект.

папка_проекту

Виконання цієї команди створить в проекті структуру каталогів, наведену нижче:

структура_каталогів контракт

У корені нашого поточного проекту файл з заголовком truffle.js (передостанній рядок на зображенні вище), помістіть цей фрагмент коду в даний файл і збережіть. Це конфігураційний файл - він вкаже інструментам truffle, з яким блокчейном їм працювати.

конфігураційний_файл

Ця команда створить файл package.json в нашому кореневому каталозі. Це дозволить нам встановити пакети з NPM сюди в кореневий каталог.

кореневий_каталог

Це класний, дуже корисний пакет. Це простий модуль для створення, управління і підписання Ethereum-транзакцій.

Запускаємо наш приватний блокчейн для тестування

У новому командному вікні виконуємо таку команду:

командне_вікно

Ця команда запустить абсолютно новий приватний тестовий блокчейн з власним генезисним блоком. Запускаючи блокчейн Ganache, ви отримуєте HD-гаманець (ієрархічно-детермінований гаманець, який має seed-фразу і послідовно генерує нескінченну кількість адрес, прив'язаних до цього гаманця). Цей гаманець матиме 10 акаунтів, прив'язаних до нього і доступних для використання.

Частина команди -u0 розблокує перший аккаунт, і ми зможемо створювати і підписувати транзакції.

транзакції

Коли команду буде виконано, ви побачите 10 акаунтів.

Розгортаємо наші контракти

Раніше ми створили файл contracts/WalletLibrary.sol. Тепер помістимо в нього цей фрагмент коду нашого контракту.

Це контракт, з яким можуть взаємодіяти інші контракти. Це не сам гаманець, і він не зберігає ніяких коштів. Він створений тільки для того, щоб контракти могли делегувати йому певний набір функціональності (через delegatecall). Він являє собою скорочену версію оригінального контракту, в якому була вразливість. Оригінального контракту більше не існує через іншу вразливість, яка його знищила (мова про листопадове заморожування коштів). Цей контракт має два методи, які можуть використовуватися будь-якими контрактами, як звертаються до нього - це метод виведення (withdraw) і зміни власника (changeOwner). Обидва ці методи можуть бути активовані тільки власником контракту, що має робити їх безпечними, правильно? Не зовсім. Подивимося, в чому їхня вразливість.

Контракт гаманця

Раніше ми створили файл contracts/Wallet.sol. Тепер помістимо в нього цей фрагмент коду контракту.

У цьому контракті багато чого відбувається. Оголошення перших двох змінних:

owner: Це власник контракту гаманця. Він задається параметром _owner (рядок 7 в наведеному фрагменті коду).

_walletLibrary: Це адреса контракту бібліотеки гаманця. Він потрібен нам, щоб ми знали, куди делегувати наші виклики.

Ми викликаємо функцію-конструктор Wallet (рядок 7) при розгортанні контракту. При виклику конструктора ми задаємо змінні owner і _walletLibrary. Конструктор задає змінну owner, перенаправляючи виклик контрактом WalletLibrary через опкод _walletLibrary.delegatecall (PARAMS).

Delegatecall

Вразливість була не тільки в контракті бібліотеки гаманця, але і в контракті самого гаманця, і вона була пов'язана з тим, як використовується DELEGATECALL в резервній функції контракту гаманця.

«Схоже на ідею CALLCODE (один з різновидів виклику до контракту бібліотеки для виконання її коду відповідно до певного контракту), за винятком того, що він передає відправника і вартість (кількість пересланого ефіру) від батьківської сутності до дочірньої, тобто виклик має того ж відправника і ту ж вартість, що і оригінальний виклик. Це означає, що контракт може зберігати і передавати інформацію, використовуючи msg.sender (дані про відправника) і msg.value (дані про кількість ефіру) свого батьківського контракту. Це добре для контрактів, які створюють контракти, але не повторюють додаткову інформацію, що економить газ. Див. коментарі до EIP 7». - Homestead Docs

DELEGATECALL не тільки поширює свої властивості на msg (повідомлення, передане системою), але і ділиться вмістом сховища тих контрактів, які здійснюють виклик. Це означає, що контракти, які отримують DELEGATECALL, можуть маніпулювати внутрішнім вмістом контрактів, які здійснюють виклик. Це не завжди погано, іноді нам саме це і потрібно (як в рядку 9), але також це може мати негативні наслідки.

Розгортання Truffle

Тут наш скрипт розгортання. Всього 10 рядків, і ми готові! Truffle - достатньо зручний інструмент, який робить за нас більшу частину роботи. Ми імпортуємо наші контракти бібліотеки гаманця і гаманця. Перший deployer (рядок 5-6) розгортає контракт бібліотеки гаманця. Другий (рядок 8) - контракт гаманця з двома параметрами: щойно розгорнутою адресою контракту бібліотеки і адресою власника контракту гаманця. Це лише невелика частина того, що відбувається під капотом.

Команда розгортання

У нас уже має бути відкрите консольне вікно з запущеним блокчейном Ganache. Тепер відкриємо нове консольне вікно і запустимо команду розгортання Truffle.

команда
розгортання

Після того, як ми запустили команду розгортання, ми побачимо висновок (результати обчислень) на зразок того, що ми бачимо нагорі. Ви можете отримати кілька попереджень від компілятора (програма, яка перетворює написаний програмістом код в код, зрозумілий комп'ютеру, у випадку Ethereum - перетворює Solidity в байт-код, зрозумілий EVM; може попереджати про потенційні проблеми в коді), які не порушать контракт , але ви повинні самостійно їх вивчити, тому що ви отримаєте важливу інформацію. Під попередженнями ми бачимо повідомлення про те, що контракти були успішно розгорнуті. Прямо над кожним ім'ям контракту стоїть хеш транзакції цього контракту, а поруч з ім'ям контракту - його адреса.

В даному випадку адреса контракту гаманця - 0x6f0147644dfbd1b335f6a5de432b4de566a8d69d, адреса бібліотеки - 0xdbcd830c1ec91a003f6475c63b4391ce73abe2af, але у вас вони будуть вже іншими.

Значення змінної _walletLibrary в контракті гаманця буде таким же, як і адреса контракту бібліотеки гаманця. Скопіюйте та збережіть де-небудь це значення - воно нам знадобиться для здійснення атаки.

Взаємодія з гаманцем:

Відкрийте третє консольне вікно і запустіть команду:

команда консоль

Виконання цієї команди приводить нас до NodeJS repl (цикл REPL, read-eval-print loop, «читання - обчислення - висновок», дозволяє запускати код покомандно і миттєво бачити результат його виконання), і Truffle надає нам доступ до двох глобальних змінних (змінні, які видно всій програмі і можуть використовуватися будь-якою ділянкою коду (на противагу локальним змінним)).

код

Ця команда бере тільки що розгорнутий контракт і вставляє його зразок в змінну wallet, щоб ми могли перевірити стан розгорнутого додатка. За підсумком ми побачимо об'єкт з безліччю властивостей:

команда

Цей об'єкт і є нашим контрактом гаманця. Він має двійковий інтерфейс додатків (ABI), байткод, опкоди Рантайм, методи контракту і іншу корисну інформацію.

контракт

У нас ця команда генерує таку адресу: 0x6ba7132c9cc09956785ff7de95b2d410858a94c2

Вищенаведена команда перевіряє, хто є поточним власником контракту, тобто хто його контролює.

гаманець

Ця команда генерує у нас таку адресу: 0x6ba7132c9cc09956785ff7de95b2d410858a94c2

Як ви бачите, адреса одна і та ж сама. Технічно ви володієте цим контрактом, тому що у вас є приватний ключ власника контракту. Але уявімо, що у вас його немає, як би ви тоді зламали цей контракт? Зараз з'ясуємо.

Атаки, вразливості

До теперішнього моменту ми не говорили докладно про атаку або вразливості обох контрактів. Давайте їх обговоримо. Ось список факторів, які привели до атаки:

  1. Довжина і складність контракту;
  2. Складні взаємодії між резервними функціями Solidity;
  3. Прозорість (публічний характер) функцій Solidity за замовчуванням;
  4. Механізм Delegatecall;
  5. Пересилання даних виклику.

У випадку з нашим контрактом, у гаманця є 3 способи виконати команду _walletLibrary.delegatecall (PARAMS). Але якщо два з певних дзвінків контролюються і поводяться, як того очікує розробник, то третій спосіб виконання delegatecall - резервна функція з модифікатором «payable». Ця функція пересилає дані в контракт бібліотеки. Тут і розсипаються очікування розробників. Програміст не може вплинути на те, що буде пересилати користувач.

делегація

Усередині методу withdraw в контракті гаманця ми виконуємо delegatecall. Фрагмент коду, наведений вище, передає цьому виклику два параметра. Перший - 4 байта з 256 байт - це оператор, який повертає результат хешування методу withdraw (uint), другий - це кількість, параметр, який передається методу withdraw (uint). Таким чином, delegatecall повинен викликати в контракті бібліотеки метод withdraw. Це те, чого ми очікуємо.

метод

Це перша вразливість. Коли хтось, власник чи ні, відправляє транзакцію на контракт нашого гаманця і намагається викликати функцію, якої не існує, то викликається резервна функція з модифікатором payable, яка приймає ефір за замовчуванням. Ця функція виконує delegatecall до контракту бібліотеки і направляє йому значення змінної msg.data. Потім буде перевірено, чи існує в контракті бібліотеки оригінальна функція, яку хотів викликати користувач. Тепер ви бачите атаку?

Друга вразливість пов'язана з тим, як був застосований метод контракту бібліотеки: initWallet (address).

функція

Функція initWallet (address) в контракті нашої бібліотеки незахищена:

  • вона не виконує перевірку, щоб подивитися, чи визначено вже власника контракту.
  • вона не містить модифікатор internal, який би наказував їй працювати тільки у власному сховищі контракту при вступі виклику, ретранслює дані (тобто вона є відкритою, публічною).

Комбінація двох цих вразливостей привела до MultiSig-атаки.

Комбінація

Атака по кроках:

  • хакер посилає транзакцію на контракт гаманця за допомогою методу initWallet (address)
  • контракт виконує перевірку, щоб подивитися, чи є такий метод, і виявляє, що його немає
  • функція з модифікатором payable викликана командою msg.data, яка була спрямована контрактом бібліотеки через delegatecall
  • бібліотека перевіряє, чи є в ній метод initWallet ()
  • вона знаходить метод і виконує його, як запрограмовано
  • метод initWallet () встановлює власника, так що хакер робить власником самого себе, вказавши свою адресу
  • тепер хакер може робити все, що захоче, з контрактом гаманця

Пам'ятайте, що коли ми використовуємо delegatecall, ми не тільки пересилаємо дані, але і повідомляємо бібліотеці, яке сховище використовувати, яке сховище відповідає контракту, який визиває. У нашому випадку викликаючий контракт - це гаманець, і він має змінну - owner, і ця змінна задається методом initWallet (address). Готово!

Як ця атака виглядає в коді?

Повертаючись до консолі Truffle, виконаємо кілька завдань, щоб здійснити атаку.

атак

Метод sha3 () хешує 'initWallet (address)' і повертає його у вигляді 256-байтного хешу, з якого ми беремо перші 4 байта, вони використовуються для ідентифікатора методу (method_id).

ідентифікатор

Ми приєднуємо 24 нулі попереду адреси хакера. Адреси в Ethereum складаються з 20 байт, параметри - з 32 байт, так що ми заповнюємо необхідні 12 байт нулями. У випадку з адресою хакера ми прибираємо 0x з його початку.

адреса

Коли ми перевіряємо змінну даних, ми бачимо значення, вказане вище. Перші 4 байта - ідентифікатор методу initWallet (address), а наступні 32 байта - це параметри, які передаються цим методом.

змінна

Тут ми задаємо параметри транзакції, необхідні для того, щоб відправити наші «дані атаки» на контракт гаманця.

параметри

Ми використовуємо пакет ethereumjs-tx, який ми встановили раніше.

пакет

Створюємо новий зразок транзакції.

зразок

Трансформуємо приватний ключ в буфер (область пам'яті для тимчасового зберігання даних, принцип роботи можна представити на прикладі звичайного буфера обміну). Відкриваємо консоль, на якій запущений блокчейн Ganache, і прокручуємо на самий верх. Ви виявите свій приватний ключ в списку приватних ключів. Ми використовуємо аккаунт номер два (виділений блакитним) і приватний ключ, що відповідає цьому аккаунту - другий в списку.

алгоритм ключі

Передаємо буфер приватного ключа методу .sign (KEY), щоб підписати транзакцію, яку ми створили. І транзакція готова.

буфер

Відсилаємо нашу хакерську транзакцію, щоб вона була оброблена (при емітаціі, якою є Ganache, етап обробки відсутній).

транзакція

Коли транзакція виконана, її хеш повернеться у вигляді, наведеному вище.

транзакція

Давайте перевіримо, як справи у нашого власника:

злам

Якщо подивитися на адресу власника, яку ви скопіювали раніше (0x6ba7132c9cc09956785ff7de95b2d410858a94c2), він буде відрізнятися. Тепер хакер контролює контракт гаманця і всі його кошти, і початковий власник не може нічого зробити.

Висновок

Це експлойт гаманця з мультипідписом в дії. Коротко - поєднання двох вразливостей дало можливість здійснити атаку. Функція гаманця, за замовчуванням приймаюча ефір і перенаправляюча дані, і метод контракту бібліотеки initWallet (address) виявилися не захищені. Розробники гаманців, взаємодіючих з контрактом бібліотеки, могли перевіряти значення змінної msg.data і дозволяти передачу тільки певних значень. Вони могли впровадити перевірки або модифікатори, щоб не допустити такої атаки. Але що зроблено - то зроблено, і контракти зламуються постійно. Команда Parity - це група дійсно розумних людей, так що це могло статися з кожним.

Фото: flickr.com
Обробка: Vinci