diff --git a/public/content/translations/uk/developers/tutorials/how-to-mint-an-nft/index.md b/public/content/translations/uk/developers/tutorials/how-to-mint-an-nft/index.md new file mode 100644 index 00000000000..26ea3c24812 --- /dev/null +++ b/public/content/translations/uk/developers/tutorials/how-to-mint-an-nft/index.md @@ -0,0 +1,335 @@ +--- +title: "Як викарбувати NFT (Частина 2/3 із серії підручників про NFT)" +description: "У цьому підручнику описано, як викарбувати NFT на блокчейні Ethereum за допомогою нашого смарт-контракту та Web3." +author: "Sumi Mudgil" +tags: + [ + "ERC-721", + "alchemy", + "мова програмування", + "Смарт-контракти" + ] +skill: beginner +lang: uk +published: 2021-04-22 +--- + +[Beeple](https://www.nytimes.com/2021/03/11/arts/design/nft-auction-christies-beeple.html): 69 мільйонів доларів +[3LAU](https://www.forbes.com/sites/abrambrown/2021/03/03/3lau-nft-nonfungible-tokens-justin-blau/?sh=5f72ef64643b): 11 мільйонів доларів +[Grimes](https://www.theguardian.com/music/2021/mar/02/grimes-sells-digital-art-collection-non-fungible-tokens): 6 мільйонів доларів + +Усі вони викарбували свої NFT за допомогою потужного API від Alchemy. У цьому підручнику ми навчимо вас, як зробити те саме за \<10 хвилин. + +«Карбування NFT» — це публікація унікального екземпляра вашого токена ERC-721 у блокчейні. Використовуючи наш смарт-контракт із [частини 1 цієї серії підручників про NFT](/developers/tutorials/how-to-write-and-deploy-an-nft/), продемонструймо наші навички Web3 та викарбуймо NFT. Наприкінці цього підручника ви зможете карбувати стільки NFT, скільки забажає ваше серце (і гаманець)! + +Розпочнімо! + +## Крок 1. Установлення Web3 {#install-web3} + +Якщо ви дотримувалися вказівок першого підручника зі створення смарт-контракту NFT, у вас уже є досвід роботи з Ethers.js. Web3 схожа на Ethers, оскільки це бібліотека, яка використовується для полегшення створення запитів до блокчейну Ethereum. У цьому підручнику ми використовуватимемо [Alchemy Web3](https://docs.alchemyapi.io/alchemy/documentation/alchemy-web3), розширену бібліотеку Web3, що пропонує автоматичні повторні спроби та надійну підтримку WebSocket. + +У кореневому каталозі вашого проєкту виконайте команду: + +``` +npm install @alch/alchemy-web3 +``` + +## Крок 2. Створення файлу `mint-nft.js` {#create-mintnftjs} + +У каталозі `scripts` створіть файл `mint-nft.js` і додайте до нього такі рядки коду: + +```js +require("dotenv").config() +const API_URL = process.env.API_URL +const { createAlchemyWeb3 } = require("@alch/alchemy-web3") +const web3 = createAlchemyWeb3(API_URL) +``` + +## Крок 3. Отримання ABI вашого контракту {#contract-abi} + +ABI нашого контракту (Application Binary Interface) — це інтерфейс для взаємодії з нашим смарт-контрактом. Дізнатися більше про ABI контрактів можна [тут](https://docs.alchemyapi.io/alchemy/guides/eth_getlogs#what-are-ab-is). Hardhat автоматично генерує для нас ABI і зберігає його у файлі `MyNFT.json`. Щоб використати це, нам потрібно буде проаналізувати вміст, додавши такі рядки коду до нашого файлу `mint-nft.js`: + +```js +const contract = require("../artifacts/contracts/MyNFT.sol/MyNFT.json") +``` + +Якщо ви хочете побачити ABI, ви можете вивести його в консоль: + +```js +console.log(JSON.stringify(contract.abi)) +``` + +Щоб запустити `mint-nft.js` і побачити свій ABI, виведений у консоль, перейдіть до термінала та виконайте: + +```js +node scripts/mint-nft.js +``` + +## Крок 4. Налаштування метаданих для вашого NFT за допомогою IPFS {#config-meta} + +Якщо ви пам’ятаєте з нашого підручника в частині 1, функція нашого смарт-контракту `mintNFT` приймає параметр tokenURI, який має вказувати на документ JSON, що описує метадані NFT. Саме ці метадані вдихають життя в NFT, дозволяючи йому мати такі властивості, що налаштовуються: назву, опис, зображення та інші атрибути. + +> _Міжпланетна файлова система (IPFS) — це децентралізований протокол і однорангова мережа для зберігання та обміну даними в розподіленій файловій системі._ + +Ми будемо використовувати Pinata, зручний API для IPFS та набір інструментів, для зберігання нашого активу NFT і метаданих, щоб переконатися, що наш NFT справді децентралізований. Якщо у вас немає облікового запису Pinata, зареєструйте безплатний обліковий запис [тут](https://app.pinata.cloud) і виконайте кроки для підтвердження своєї електронної пошти. + +Після того, як ви створили обліковий запис: + +- Перейдіть на сторінку «Файли» й натисніть синю кнопку «Завантажити» у верхньому лівому куті сторінки. + +- Завантажте зображення в Pinata — це буде графічний актив для вашого NFT. Називайте активи як забажаєте + +- Після завантаження ви побачите інформацію про файл у таблиці на сторінці «Файли». Ви також побачите стовпець CID. Ви можете скопіювати CID, натиснувши кнопку копіювання поруч із ним. Переглянути завантаження можна за адресою: `https://gateway.pinata.cloud/ipfs/`. Наприклад, зображення, яке ми використовували, можна знайти в IPFS [тут](https://gateway.pinata.cloud/ipfs/QmZdd5KYdCFApWn7eTZJ1qgJu18urJrP9Yh1TZcZrZxxB5). + +Для тих, хто краще сприймає візуальну інформацію, кроки вище підсумовано тут: + +![Як завантажити зображення в Pinata](./instructionsPinata.gif) + +Тепер нам потрібно завантажити ще один документ у Pinata. Але перш ніж зробити це, нам потрібно його створити! + +У кореневому каталозі створіть новий файл `nft-metadata.json` і додайте такий код JSON: + +```json +{ + "attributes": [ + { + "trait_type": "Breed", + "value": "Maltipoo" + }, + { + "trait_type": "Eye color", + "value": "Mocha" + } + ], + "description": "The world's most adorable and sensitive pup.", + "image": "ipfs://QmWmvTJmJU3pozR9ZHFmQC2DNDwi2XJtf3QGyYiiagFSWb", + "name": "Ramses" +} +``` + +Можете змінювати дані в JSON. Ви можете видаляти атрибути або додавати нові. Найголовніше, переконайтеся, що поле зображення вказує на розташування вашого зображення в IPFS, інакше у ваш NFT потрапить фото (дуже милого!) собаки. + +Закінчивши редагувати файл JSON, збережіть його та завантажте в Pinata, виконавши ті самі кроки, що й під час завантаження зображення. + +![Як завантажити nft-metadata.json у Pinata](./uploadPinata.gif) + +## Крок 5. Створення екземпляра вашого контракту {#instance-contract} + +Тепер, щоб взаємодіяти з нашим контрактом, нам потрібно створити його екземпляр у нашому коді. Для цього нам знадобиться адреса нашого контракту, яку ми можемо отримати з даних розгортання або в [Blockscout](https://eth-sepolia.blockscout.com/), знайшовши адресу, яку ви використовували для розгортання контракту. + +![Перегляд адреси вашого контракту на Etherscan](./view-contract-etherscan.png) + +У наведеному вище прикладі адреса нашого контракту — 0x5a738a5c5fe46a1fd5ee7dd7e38f722e2aef7778. + +Далі ми використаємо [метод contract](https://docs.web3js.org/api/web3-eth-contract/class/Contract) Web3, щоб створити наш контракт за допомогою ABI та адреси. У файл `mint-nft.js` додайте таке: + +```js +const contractAddress = "0x5a738a5c5fe46a1fd5ee7dd7e38f722e2aef7778" + +const nftContract = new web3.eth.Contract(contract.abi, contractAddress) +``` + +## Крок 6. Оновлення файлу `.env` {#update-env} + +Тепер, щоб створювати та надсилати транзакції в мережу Ethereum, ми будемо використовувати адресу вашого публічного облікового запису Ethereum, щоб отримати nonce облікового запису (це ми пояснимо нижче). + +Додайте свій публічний ключ у файл `.env` — якщо ви виконали частину 1 підручника, ваш файл `.env` тепер має виглядати так: + +```js +API_URL = "https://eth-sepolia.g.alchemy.com/v2/your-api-key" +PRIVATE_KEY = "your-private-account-address" +PUBLIC_KEY = "your-public-account-address" +``` + +## Крок 7. Створення транзакції {#create-txn} + +Спочатку визначимо функцію `mintNFT(tokenData)` і створимо нашу транзакцію, виконавши такі дії: + +1. Візьміть _PRIVATE_KEY_ та _PUBLIC_KEY_ з файлу `.env`. + +2. Далі нам потрібно визначити nonce облікового запису. Специфікація nonce використовується для відстеження кількості транзакцій, надісланих із вашої адреси, що необхідно для безпеки та запобігання [атакам повторного відтворення](https://docs.alchemyapi.io/resources/blockchain-glossary#account-nonce). Щоб отримати кількість транзакцій, надісланих із вашої адреси, ми використовуємо [getTransactionCount](https://docs.alchemyapi.io/documentation/alchemy-api-reference/json-rpc#eth_gettransactioncount). + +3. Нарешті, ми налаштуємо транзакцію з такою інформацією: + +- `'from': PUBLIC_KEY` — джерело нашої транзакції, наша публічна адреса + +- `'to': contractAddress` — контракт, з яким ми хочемо взаємодіяти та якому надсилаємо транзакцію + +- `'nonce': nonce` — nonce облікового запису з кількістю транзакцій, надісланих з нашої адреси + +- `'gas': estimatedGas` — приблизна кількість газу, необхідна для завершення транзакції + +- `'data': nftContract.methods.mintNFT(PUBLIC_KEY, md).encodeABI()` — обчислення, яке ми хочемо виконати в цій транзакції, у нашому випадку це карбування NFT + +Тепер ваш файл `mint-nft.js` має виглядати так: + +```js + require('dotenv').config(); + const API_URL = process.env.API_URL; + const PUBLIC_KEY = process.env.PUBLIC_KEY; + const PRIVATE_KEY = process.env.PRIVATE_KEY; + + const { createAlchemyWeb3 } = require("@alch/alchemy-web3"); + const web3 = createAlchemyWeb3(API_URL); + + const contract = require("../artifacts/contracts/MyNFT.sol/MyNFT.json"); + const contractAddress = "0x5a738a5c5fe46a1fd5ee7dd7e38f722e2aef7778"; + const nftContract = new web3.eth.Contract(contract.abi, contractAddress); + + async function mintNFT(tokenURI) { + const nonce = await web3.eth.getTransactionCount(PUBLIC_KEY, 'latest'); //отримати останній nonce + + //транзакція + const tx = { + 'from': PUBLIC_KEY, + 'to': contractAddress, + 'nonce': nonce, + 'gas': 500000, + 'data': nftContract.methods.mintNFT(PUBLIC_KEY, tokenURI).encodeABI() + }; + }​ +``` + +## Крок 8. Підписання транзакції {#sign-txn} + +Тепер, коли ми створили транзакцію, нам потрібно її підписати, щоб відправити. Саме тут ми будемо використовувати наш приватний ключ. + +`web3.eth.sendSignedTransaction` надасть нам хеш транзакції, за допомогою якого ми можемо переконатися, що нашу транзакцію було видобуто, а не відхилено мережею. Ви помітите, що в розділі підписання транзакцій ми додали перевірку помилок, щоб знати, чи успішно пройшла наша транзакція. + +```js +require("dotenv").config() +const API_URL = process.env.API_URL +const PUBLIC_KEY = process.env.PUBLIC_KEY +const PRIVATE_KEY = process.env.PRIVATE_KEY + +const { createAlchemyWeb3 } = require("@alch/alchemy-web3") +const web3 = createAlchemyWeb3(API_URL) + +const contract = require("../artifacts/contracts/MyNFT.sol/MyNFT.json") +const contractAddress = "0x5a738a5c5fe46a1fd5ee7dd7e38f722e2aef7778" +const nftContract = new web3.eth.Contract(contract.abi, contractAddress) + +async function mintNFT(tokenURI) { + const nonce = await web3.eth.getTransactionCount(PUBLIC_KEY, "latest") //отримати останній nonce + + //транзакція + const tx = { + from: PUBLIC_KEY, + to: contractAddress, + nonce: nonce, + gas: 500000, + data: nftContract.methods.mintNFT(PUBLIC_KEY, tokenURI).encodeABI(), + } + + const signPromise = web3.eth.accounts.signTransaction(tx, PRIVATE_KEY) + signPromise + .then((signedTx) => { + web3.eth.sendSignedTransaction( + signedTx.rawTransaction, + function (err, hash) { + if (!err) { + console.log( + "Хеш вашої транзакції: ", + hash, + "\nПеревірте Mempool в Alchemy, щоб переглянути статус вашої транзакції!" + ) + } else { + console.log( + "Щось пішло не так під час надсилання транзакції:", + err + ) + } + } + ) + }) + .catch((err) => { + console.log(" Помилка Promise:", err) + }) +} +``` + +## Крок 9. Виклик `mintNFT` і запуск `node mint-nft.js` {#call-mintnft-fn} + +Пам'ятаєте файл `metadata.json`, який ви завантажили в Pinata? Отримайте його хеш-код з Pinata та передайте у функцію `mintNFT` такий параметр: `https://gateway.pinata.cloud/ipfs/` + +Ось як отримати хеш-код: + +![Як отримати хеш-код метаданих вашого NFT на Pinata](./metadataPinata.gif)_Як отримати хеш-код метаданих вашого NFT на Pinata_ + +> Ще раз перевірте, чи скопійований хеш-код веде на ваш **metadata.json**, завантаживши `https://gateway.pinata.cloud/ipfs/` в окремому вікні. Сторінка має виглядати приблизно так, як на знімку екрана нижче: + +![На вашій сторінці мають відображатися метадані JSON](./metadataJSON.png)_На вашій сторінці мають відображатися метадані JSON_ + +Загалом ваш код має виглядати приблизно так: + +```js +require("dotenv").config() +const API_URL = process.env.API_URL +const PUBLIC_KEY = process.env.PUBLIC_KEY +const PRIVATE_KEY = process.env.PRIVATE_KEY + +const { createAlchemyWeb3 } = require("@alch/alchemy-web3") +const web3 = createAlchemyWeb3(API_URL) + +const contract = require("../artifacts/contracts/MyNFT.sol/MyNFT.json") +const contractAddress = "0x5a738a5c5fe46a1fd5ee7dd7e38f722e2aef7778" +const nftContract = new web3.eth.Contract(contract.abi, contractAddress) + +async function mintNFT(tokenURI) { + const nonce = await web3.eth.getTransactionCount(PUBLIC_KEY, "latest") //отримати останній nonce + + //транзакція + const tx = { + from: PUBLIC_KEY, + to: contractAddress, + nonce: nonce, + gas: 500000, + data: nftContract.methods.mintNFT(PUBLIC_KEY, tokenURI).encodeABI(), + } + + const signPromise = web3.eth.accounts.signTransaction(tx, PRIVATE_KEY) + signPromise + .then((signedTx) => { + web3.eth.sendSignedTransaction( + signedTx.rawTransaction, + function (err, hash) { + if (!err) { + console.log( + "Хеш вашої транзакції: ", + hash, + "\nПеревірте Mempool в Alchemy, щоб переглянути статус вашої транзакції!" + ) + } else { + console.log( + "Щось пішло не так під час надсилання транзакції:", + err + ) + } + } + ) + }) + .catch((err) => { + console.log("Помилка Promise:", err) + }) +} + +mintNFT("ipfs://QmYueiuRNmL4MiA2GwtVMm6ZagknXnSpQnB3z2gWbz36hP") +``` + +Тепер запустіть `node scripts/mint-nft.js`, щоб розгорнути свій NFT. Через кілька секунд ви маєте побачити в терміналі таку відповідь: + + ``` + Хеш вашої транзакції: 0x301791fdf492001fcd9d5e5b12f3aa1bbbea9a88ed24993a8ab2cdae2d06e1e8 + + Перевірте Mempool в Alchemy, щоб переглянути статус вашої транзакції! + ``` + +Далі перейдіть до [mempool Alchemy](https://dashboard.alchemyapi.io/mempool), щоб побачити статус вашої транзакції (чи вона очікує на розгляд, чи її видобуто, чи відхилено мережею). Якщо вашу транзакцію було відхилено, також корисно перевірити [Blockscout](https://eth-sepolia.blockscout.com/) і знайти хеш вашої транзакції. + +![Перегляд хешу транзакції вашого NFT на Etherscan](./view-nft-etherscan.png)_Перегляд хешу транзакції вашого NFT на Etherscan_ + +Ось і все! Ви щойно розгорнули ТА викарбували NFT на блокчейні Ethereum + +За допомогою `mint-nft.js` ви можете карбувати стільки NFT, скільки забажає ваше серце (і гаманець)! Просто не забудьте передати новий tokenURI, що описує метадані NFT (інакше ви просто створите купу однакових токенів із різними ідентифікаторами). + +Імовірно, ви хотіли б мати можливість показати свій NFT у гаманці — тож обов'язково перегляньте [Частину 3. Як переглянути свій NFT у гаманці](/developers/tutorials/how-to-view-nft-in-metamask/)! diff --git a/public/content/translations/uk/developers/tutorials/how-to-mock-solidity-contracts-for-testing/index.md b/public/content/translations/uk/developers/tutorials/how-to-mock-solidity-contracts-for-testing/index.md new file mode 100644 index 00000000000..58dc0942019 --- /dev/null +++ b/public/content/translations/uk/developers/tutorials/how-to-mock-solidity-contracts-for-testing/index.md @@ -0,0 +1,59 @@ +--- +title: "Як знущатися над смарт-контрактами Solidity для тестування" +description: "Чому під час тестування варто висміювати свої контракти" +author: Markus Waas +lang: uk +tags: + [ + "мова програмування", + "Смарт-контракти", + "тестування", + "глузливий" + ] +skill: intermediate +published: 2020-05-02 +source: soliditydeveloper.com +sourceUrl: https://soliditydeveloper.com/mocking-contracts +--- + +[Макетні об’єкти](https://wikipedia.org/wiki/Mock_object) є поширеним шаблоном проєктування в об’єктноорієнтованому програмуванні. Походить від старого французького слова «mocquer» зі значенням «висміювати», воно еволюціонувало до «імітувати щось справжнє», що насправді є тим, що ми робимо в програмуванні. Будь ласка, висміюйте свої розумні контракти, лише якщо хочете, але знущайтеся над ними, коли можете. Це полегшує ваше життя. + +## Модульне тестування контрактів за допомогою макетів {#unit-testing-contracts-with-mocks} + +Знущання над контрактом по суті означає створення другої версії цього контракту, яка веде себе дуже подібно до оригінальної, але таким чином, що його легко контролювати розробник. Часто доводиться мати справу зі складними контрактами, де потрібно лише [модульно протестувати невеликі частини контракту](/developers/docs/smart-contracts/testing/). Проблема полягає в тому, що, якщо для тестування цієї маленької частини потрібен дуже специфічний контрактний стан, у якому важко опинитися? + +Ви можете щоразу писати складну логіку налаштування тесту, яка приводить контракт у потрібний стан, або ж написати макет. Знущатися над договором легко зі спадщиною. Просто створіть другий макет контракту, який успадковує оригінальний. Тепер ви можете перевизначати функції свого макету. Розглянемо це на прикладі. + +## Приклад: Private ERC20 {#example-private-erc20} + +Ми використовуємо приклад контракту ERC-20, який має початковий приватний час. Власник може керувати приватними користувачами, і лише їм буде дозволено отримувати токени на початку. Після закінчення певного часу всім буде дозволено використовувати жетони. Якщо вам цікаво, ми використовуємо хук [`_beforeTokenTransfer`](https://docs.openzeppelin.com/contracts/5.x/extending-contracts#using-hooks) з нових контрактів OpenZeppelin v3. + +```solidity +pragma solidity ^0.6.0;\n\nimport "@openzeppelin/contracts/token/ERC20/ERC20.sol";\nimport "@openzeppelin/contracts/access/Ownable.sol";\n\ncontract PrivateERC20 is ERC20, Ownable {\n mapping (address => bool) public isPrivateUser;\n uint256 private publicAfterTime;\n\n constructor(uint256 privateERC20timeInSec) ERC20(\"PrivateERC20\", \"PRIV\") public {\n publicAfterTime = now + privateERC20timeInSec;\n }\n\n function addUser(address user) external onlyOwner {\n isPrivateUser[user] = true;\n }\n\n function isPublic() public view returns (bool) {\n return now >= publicAfterTime;\n }\n\n function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual override {\n super._beforeTokenTransfer(from, to, amount);\n\n require(_validRecipient(to), \"PrivateERC20: invalid recipient\");\n }\n\n function _validRecipient(address to) private view returns (bool) {\n if (isPublic()) {\n return true;\n }\n\n return isPrivateUser[to];\n }\n} +``` + +А тепер познущаймося. + +```solidity +pragma solidity ^0.6.0;\nimport "../PrivateERC20.sol";\n\ncontract PrivateERC20Mock is PrivateERC20 {\n bool isPublicConfig;\n\n constructor() public PrivateERC20(0) {}\n\n function setIsPublic(bool isPublic) external {\n isPublicConfig = isPublic;\n }\n\n function isPublic() public view returns (bool) {\n return isPublicConfig;\n }\n} +``` + +Ви отримаєте одне з таких повідомлень про помилку: + +- `PrivateERC20Mock.sol: TypeError: Overriding function is missing \"override\" specifier.` +- `PrivateERC20.sol: TypeError: Trying to override non-virtual function. Did you forget to add \"virtual\"?.` + +Оскільки ми використовуємо нову версію Solidity 0.6, нам потрібно додати ключове слово `virtual` для функцій, які можна перевизначати, і `override` для функції, що їх перевизначає. Тож додамо їх до обох функцій `isPublic`. + +Тепер у своїх модульних тестах ви можете натомість використовувати `PrivateERC20Mock`. Коли ви хочете перевірити поведінку під час приватного використання, використовуйте `setIsPublic(false)`, і так само `setIsPublic(true)` для тестування під час публічного використання. Звісно, у нашому прикладі ми могли б також просто скористатися [помічниками часу](https://docs.openzeppelin.com/test-helpers/0.5/api#increase), щоб відповідно змінити час. Але ідея насмішки має бути зрозумілою зараз, і ви можете уявити сценарії, де це не так просто, як просто затягнути час. + +## Створення макетів для багатьох контрактів {#mocking-many-contracts} + +Це може стати безладним, якщо вам доведеться створювати інший контракт для кожного окремого макету. Якщо вас це турбує, можете переглянути бібліотеку [MockContract](https://github.com/gnosis/mock-contract). Вона дозволяє вам перевизначати та змінювати поведінку контрактів на льоту. Однак він працює лише для глузливих викликів іншого контракту, тому для нашого прикладу він не працюватиме. + +## Макетування може бути ще потужнішим {#mocking-can-be-even-more-powerful} + +На цьому сила глузування не закінчується. + +- Додавання функцій: корисним є не лише перевизначення певної функції, але й просто додавання додаткових функцій. Хорошим прикладом для токенів є наявність додаткової функції `mint`, яка дозволяє будь-якому користувачеві безкоштовно отримувати нові токени. +- Використання в тестових мережах: коли ви розгортаєте та тестуєте свої контракти в тестових мережах разом із вашим dapp, подумайте про використання фіктивної версії. Уникайте перевизначення функцій, якщо це дійсно не потрібно. Зрештою, ви хочете перевірити справжню логіку. Але може бути корисним додавання, наприклад, функції скидання, яка просто скидає стан контракту на початок, не потребуючи нового розгортання. Очевидно, ви не хотіли б мати це в контракті Mainnet. diff --git a/public/content/translations/uk/developers/tutorials/how-to-use-echidna-to-test-smart-contracts/index.md b/public/content/translations/uk/developers/tutorials/how-to-use-echidna-to-test-smart-contracts/index.md new file mode 100644 index 00000000000..95821fbc625 --- /dev/null +++ b/public/content/translations/uk/developers/tutorials/how-to-use-echidna-to-test-smart-contracts/index.md @@ -0,0 +1,710 @@ +--- +title: "Як використовувати Echidna для тестування смарт-контрактів" +description: "Як використовувати Echidna для автоматичного тестування смарт-контрактів" +author: "Trailofbits" +lang: uk +tags: + [ + "мова програмування", + "Смарт-контракти", + "захист", + "тестування", + "фазинг" + ] +skill: advanced +published: 2020-04-10 +source: Building secure contracts +sourceUrl: https://github.com/crytic/building-secure-contracts/tree/master/program-analysis/echidna +--- + +## Встановлення {#installation} + +Echidna можна встановити через Docker або за допомогою попередньо скомпільованого бінарного файлу. + +### Echidna через Docker {#echidna-through-docker} + +```bash +docker pull trailofbits/eth-security-toolbox +docker run -it -v "$PWD":/home/training trailofbits/eth-security-toolbox +``` + +_Остання команда запускає eth-security-toolbox у контейнері Docker, який має доступ до вашого поточного каталогу. Ви можете змінювати файли з вашого хоста та запускати інструменти для файлів із контейнера Docker_ + +Усередині Docker запустіть: + +```bash +solc-select 0.5.11 +cd /home/training +``` + +### Бінарний файл {#binary} + +[https://github.com/crytic/echidna/releases/tag/v1.4.0.0](https://github.com/crytic/echidna/releases/tag/v1.4.0.0) + +## Вступ до фазингу на основі властивостей {#introduction-to-property-based-fuzzing} + +Echidna — це фазер на основі властивостей, який ми описували в наших попередніх дописах у блозі ([1](https://blog.trailofbits.com/2018/03/09/echidna-a-smart-fuzzer-for-ethereum/), [2](https://blog.trailofbits.com/2018/05/03/state-machine-testing-with-echidna/), [3](https://blog.trailofbits.com/2020/03/30/an-echidna-for-all-seasons/)). + +### Фазинг {#fuzzing} + +[Фазинг](https://wikipedia.org/wiki/Fuzzing) — це добре відома техніка у спільноті безпеки. Він полягає в генеруванні більш-менш випадкових вхідних даних для виявлення помилок у програмі. Фазери для традиційного програмного забезпечення (такі як [AFL](http://lcamtuf.coredump.cx/afl/) або [LibFuzzer](https://llvm.org/docs/LibFuzzer.html)) відомі як ефективні інструменти для пошуку помилок. + +Окрім суто випадкової генерації вхідних даних, існує багато технік і стратегій для генерації хороших вхідних даних, зокрема: + +- Отримання зворотного зв’язку від кожного виконання та використання його для керування генерацією. Наприклад, якщо щойно згенеровані вхідні дані ведуть до відкриття нового шляху, має сенс згенерувати нові вхідні дані, близькі до них. +- Генерування вхідних даних із дотриманням структурних обмежень. Наприклад, якщо ваші вхідні дані містять заголовок із контрольною сумою, є сенс дозволити фазеру генерувати вхідні дані, що перевіряють цю контрольну суму. +- Використання відомих вхідних даних для створення нових: якщо у вас є доступ до великого набору дійсних вхідних даних, ваш фазер може генерувати нові вхідні дані з них, а не починати генерацію з нуля. Зазвичай їх називають _сідами_. + +### Фазинг на основі властивостей {#property-based-fuzzing} + +Echidna належить до специфічної родини фазерів: фазинг на основі властивостей, значною мірою натхненний [QuickCheck](https://wikipedia.org/wiki/QuickCheck). На відміну від класичного фазера, який намагатиметься знайти збої, Echidna намагатиметься порушити визначені користувачем інваріанти. + +У смарт-контрактах інваріанти — це функції Solidity, які можуть представляти будь-який некоректний або недійсний стан, якого може досягти контракт, зокрема: + +- Некоректний контроль доступу: зловмисник став власником контракту. +- Некоректний кінцевий автомат: токени можна передавати, поки контракт призупинено. +- Некоректна арифметика: користувач може викликати спустошення свого балансу (underflow) і отримати необмежену кількість безкоштовних токенів. + +### Тестування властивості за допомогою Echidna {#testing-a-property-with-echidna} + +Розглянемо, як протестувати смарт-контракт за допомогою Echidna. Ціллю є наступний смарт-контракт [`token.sol`](https://github.com/crytic/building-secure-contracts/blob/master/program-analysis/echidna/example/token.sol): + +```solidity +contract Token{ + mapping(address => uint) public balances; + function airdrop() public{ + balances[msg.sender] = 1000; + } + function consume() public{ + require(balances[msg.sender]>0); + balances[msg.sender] -= 1; + } + function backdoor() public{ + balances[msg.sender] += 1; + } +} +``` + +Припустимо, що цей токен повинен мати такі властивості: + +- Будь-хто може мати щонайбільше 1000 токенів +- Токен не можна передати (це не токен ERC20) + +### Написання властивості {#write-a-property} + +Властивості Echidna — це функції Solidity. Властивість повинна: + +- Не мати аргументів +- Повертати `true` у разі успішної перевірки +- Мати назву, що починається з `echidna` + +Echidna: + +- Автоматично генеруватиме довільні транзакції для тестування властивості. +- Повідомляти про будь-які транзакції, що змушують властивість повертати `false` або викликати помилку. +- Ігнорувати побічні ефекти під час виклику властивості (тобто якщо властивість змінює змінну стану, ця зміна скасовується після тесту) + +Наступна властивість перевіряє, що той, хто викликає функцію, має не більше 1000 токенів: + +```solidity +function echidna_balance_under_1000() public view returns(bool){ + return balances[msg.sender] <= 1000; +} +``` + +Використовуйте успадкування, щоб відокремити ваш контракт від ваших властивостей: + +```solidity +contract TestToken is Token{ + function echidna_balance_under_1000() public view returns(bool){ + return balances[msg.sender] <= 1000; + } + } +``` + +[`token.sol`](https://github.com/crytic/building-secure-contracts/blob/master/program-analysis/echidna/example/token.sol) реалізує властивість і успадковує від токена. + +### Ініціалізація контракту {#initiate-a-contract} + +Echidna потребує [конструктор](/developers/docs/smart-contracts/anatomy/#constructor-functions) без аргументів. Якщо ваш контракт потребує спеціальної ініціалізації, ви повинні зробити це в конструкторі. + +У Echidna є кілька спеціальних адрес: + +- `0x00a329c0648769A73afAc7F9381E08FB43dBEA72`, яка викликає конструктор. +- `0x10000`, `0x20000` та `0x00a329C0648769a73afAC7F9381e08fb43DBEA70`, які випадково викликають інші функції. + +У нашому поточному прикладі нам не потрібна якась особлива ініціалізація, тому наш конструктор порожній. + +### Запуск Echidna {#run-echidna} + +Echidna запускається за допомогою: + +```bash +echidna-test contract.sol +``` + +Якщо contract.sol містить кілька контрактів, ви можете вказати цільовий: + +```bash +echidna-test contract.sol --contract MyContract +``` + +### Підсумок: Тестування властивості {#summary-testing-a-property} + +Нижче наведено підсумок запуску Echidna на нашому прикладі: + +```solidity +contract TestToken is Token{ + constructor() public {} + function echidna_balance_under_1000() public view returns(bool){ + return balances[msg.sender] <= 1000; + } + } +``` + +```bash +echidna-test testtoken.sol --contract TestToken +... + +echidna_balance_under_1000: збій!💥 + Послідовність викликів, скорочення (1205/5000): + airdrop() + backdoor() + +... +``` + +Echidna виявила, що властивість порушується, якщо викликається `backdoor`. + +## Фільтрування функцій для виклику під час кампанії фазингу {#filtering-functions-to-call-during-a-fuzzing-campaign} + +Розглянемо, як фільтрувати функції, що підлягають фазингу. +Ціллю є наступний смарт-контракт: + +```solidity +contract C { + bool state1 = false; + bool state2 = false; + bool state3 = false; + bool state4 = false; + + function f(uint x) public { + require(x == 12); + state1 = true; + } + + function g(uint x) public { + require(state1); + require(x == 8); + state2 = true; + } + + function h(uint x) public { + require(state2); + require(x == 42); + state3 = true; + } + + function i() public { + require(state3); + state4 = true; + } + + function reset1() public { + state1 = false; + state2 = false; + state3 = false; + return; + } + + function reset2() public { + state1 = false; + state2 = false; + state3 = false; + return; + } + + function echidna_state4() public returns (bool) { + return (!state4); + } +} +``` + +Цей невеликий приклад змушує Echidna знайти певну послідовність транзакцій, щоб змінити змінну стану. +Це складно для фазера (рекомендується використовувати інструмент символьного виконання, наприклад [Manticore](https://github.com/trailofbits/manticore)). +Ми можемо запустити Echidna, щоб перевірити це: + +```bash +echidna-test multi.sol +... +echidna_state4: пройдено! 🎉 +Seed: -3684648582249875403 +``` + +### Фільтрування функцій {#filtering-functions} + +Echidna має труднощі з пошуком правильної послідовності для тестування цього контракту, оскільки дві функції скидання (`reset1` і `reset2`) встановлюють для всіх змінних стану значення `false`. +Однак ми можемо використати спеціальну функцію Echidna, щоб або додати функцію скидання до чорного списку, або додати до білого списку лише функції `f`, `g`, +`h` та `i`. + +Щоб додати функції до чорного списку, ми можемо використати такий файл конфігурації: + +```yaml +filterBlacklist: true +filterFunctions: ["reset1", "reset2"] +``` + +Інший підхід до фільтрування функцій — це перерахувати функції з білого списку. Для цього ми можемо використати такий файл конфігурації: + +```yaml +filterBlacklist: false +filterFunctions: ["f", "g", "h", "i"] +``` + +- `filterBlacklist` має значення `true` за замовчуванням. +- Фільтрація виконуватиметься лише за назвою (без параметрів). Якщо у вас є `f()` та `f(uint256)`, фільтр `"f"` відповідатиме обом функціям. + +### Запуск Echidna {#run-echidna-1} + +Щоб запустити Echidna з файлом конфігурації `blacklist.yaml`: + +```bash +echidna-test multi.sol --config blacklist.yaml +... +echidna_state4: збій!💥 + Послідовність виклику: + f(12) + g(8) + h(42) + i() +``` + +Echidna майже миттєво знайде послідовність транзакцій, щоб спростувати властивість. + +### Підсумок: Фільтрування функцій {#summary-filtering-functions} + +Echidna може додавати функції до чорного або білого списку для виклику під час кампанії фазингу за допомогою: + +```yaml +filterBlacklist: true +filterFunctions: ["f1", "f2", "f3"] +``` + +```bash +echidna-test contract.sol --config config.yaml +... +``` + +Echidna починає кампанію фазингу, або додаючи до чорного списку `f1`, `f2` і `f3`, або викликаючи тільки їх, залежно +від логічного значення `filterBlacklist`. + +## Як тестувати `assert` Solidity за допомогою Echidna {#how-to-test-soliditys-assert-with-echidna} + +У цьому короткому посібнику ми покажемо, як використовувати Echidna для тестування перевірки тверджень (`assert`) у контрактах. Припустімо, у нас є такий контракт: + +```solidity +contract Incrementor { + uint private counter = 2**200; + + function inc(uint val) public returns (uint){ + uint tmp = counter; + counter += val; + // tmp <= counter + return (counter - tmp); + } +} +``` + +### Написання твердження {#write-an-assertion} + +Ми хочемо переконатися, що `tmp` менше або дорівнює `counter` після повернення їхньої різниці. Ми могли б написати +властивість Echidna, але нам потрібно буде десь зберігати значення `tmp`. Натомість ми могли б використати таке твердження: + +```solidity +contract Incrementor { + uint private counter = 2**200; + + function inc(uint val) public returns (uint){ + uint tmp = counter; + counter += val; + assert (tmp <= counter); + return (counter - tmp); + } +} +``` + +### Запуск Echidna {#run-echidna-2} + +Щоб увімкнути тестування збоїв тверджень, створіть [файл конфігурації Echidna](https://github.com/crytic/echidna/wiki/Config) `config.yaml`: + +```yaml +checkAsserts: true +``` + +Коли ми запускаємо цей контракт у Echidna, ми отримуємо очікувані результати: + +```bash +echidna-test assert.sol --config config.yaml +Analyzing contract: assert.sol:Incrementor +assertion in inc: збій!💥 + Послідовність викликів, скорочення (2596/5000): + inc(21711016731996786641919559689128982722488122124807605757398297001483711807488) + inc(7237005577332262213973186563042994240829374041602535252466099000494570602496) + inc(86844066927987146567678238756515930889952488499230423029593188005934847229952) + +Seed: 1806480648350826486 +``` + +Як бачите, Echidna повідомляє про збій твердження у функції `inc`. Можна додати більше одного твердження на функцію, але Echidna не може сказати, яке саме твердження не виконалося. + +### Коли і як використовувати твердження {#when-and-how-use-assertions} + +Твердження можна використовувати як альтернативу явним властивостям, особливо якщо умови, які потрібно перевірити, безпосередньо пов'язані з правильним використанням деякої операції `f`. Додавання тверджень після певного коду забезпечить, що перевірка відбудеться одразу після його виконання: + +```solidity +function f(..) public { + // some complex code + ... + assert (condition); + ... +} + +``` + +Навпаки, використання явної властивості Echidna призведе до випадкового виконання транзакцій, і немає простого способу точно визначити, коли відбудеться перевірка. Проте, все ще можна зробити це обхідним шляхом: + +```solidity +function echidna_assert_after_f() public returns (bool) { + f(..); + return(condition); +} +``` + +Однак, є деякі проблеми: + +- Це не спрацює, якщо `f` оголошена як `internal` або `external`. +- Незрозуміло, які аргументи слід використовувати для виклику `f`. +- Якщо `f` скасовує транзакцію, властивість не буде виконана. + +Загалом, ми рекомендуємо дотримуватися [рекомендації Джона Регера](https://blog.regehr.org/archives/1091) щодо використання тверджень: + +- Не викликайте жодних побічних ефектів під час перевірки твердження. Наприклад: `assert(ChangeStateAndReturn() == 1)` +- Не робіть тверджень щодо очевидних речей. Наприклад `assert(var >= 0)`, де `var` оголошено як `uint`. + +Нарешті, будь ласка, **не використовуйте** `require` замість `assert`, оскільки Echidna не зможе це виявити (але контракт все одно скасує транзакцію). + +### Підсумок: перевірка тверджень {#summary-assertion-checking} + +Нижче наведено підсумок запуску Echidna на нашому прикладі: + +```solidity +contract Incrementor { + uint private counter = 2**200; + + function inc(uint val) public returns (uint){ + uint tmp = counter; + counter += val; + assert (tmp <= counter); + return (counter - tmp); + } +} +``` + +```bash +echidna-test assert.sol --config config.yaml +Analyzing contract: assert.sol:Incrementor +assertion in inc: збій!💥 + Послідовність викликів, скорочення (2596/5000): + inc(21711016731996786641919559689128982722488122124807605757398297001483711807488) + inc(7237005577332262213973186563042994240829374041602535252466099000494570602496) + inc(86844066927987146567678238756515930889952488499230423029593188005934847229952) + +Seed: 1806480648350826486 +``` + +Echidna виявила, що твердження в `inc` може не виконуватися, якщо ця функція викликається кілька разів із великими аргументами. + +## Збір і зміна корпусу Echidna {#collecting-and-modifying-an-echidna-corpus} + +Розглянемо, як збирати та використовувати корпус транзакцій з Echidna. Ціллю є наступний смарт-контракт [`magic.sol`](https://github.com/crytic/building-secure-contracts/blob/master/program-analysis/echidna/example/magic.sol): + +```solidity +contract C { + bool value_found = false; + function magic(uint magic_1, uint magic_2, uint magic_3, uint magic_4) public { + require(magic_1 == 42); + require(magic_2 == 129); + require(magic_3 == magic_4+333); + value_found = true; + return; + } + + function echidna_magic_values() public returns (bool) { + return !value_found; + } + +} +``` + +Цей невеликий приклад змушує Echidna знайти певні значення для зміни змінної стану. Це складно для фазера +(рекомендується використовувати інструмент символьного виконання, як-от [Manticore](https://github.com/trailofbits/manticore)). +Ми можемо запустити Echidna, щоб перевірити це: + +```bash +echidna-test magic.sol +... + +echidna_magic_values: пройдено! 🎉 + +Seed: 2221503356319272685 +``` + +Однак ми все ще можемо використовувати Echidna для збору корпусу під час запуску цієї кампанії фазингу. + +### Збір корпусу {#collecting-a-corpus} + +Щоб увімкнути збір корпусу, створіть каталог корпусу: + +```bash +mkdir corpus-magic +``` + +І [файл конфігурації Echidna](https://github.com/crytic/echidna/wiki/Config) `config.yaml`: + +```yaml +coverage: true +corpusDir: "corpus-magic" +``` + +Тепер ми можемо запустити наш інструмент і перевірити зібраний корпус: + +```bash +echidna-test magic.sol --config config.yaml +``` + +Echidna все ще не може знайти правильні магічні значення, але ми можемо подивитися на зібраний нею корпус. +Наприклад, один з цих файлів був: + +```json +[ + { + "_gas'": "0xffffffff", + "_delay": ["0x13647", "0xccf6"], + "_src": "00a329c0648769a73afac7f9381e08fb43dbea70", + "_dst": "00a329c0648769a73afac7f9381e08fb43dbea72", + "_value": "0x0", + "_call": { + "tag": "SolCall", + "contents": [ + "magic", + [ + { + "contents": [ + 256, + "93723985220345906694500679277863898678726808528711107336895287282192244575836" + ], + "tag": "AbiUInt" + }, + { + "contents": [256, "334"], + "tag": "AbiUInt" + }, + { + "contents": [ + 256, + "68093943901352437066264791224433559271778087297543421781073458233697135179558" + ], + "tag": "AbiUInt" + }, + { + "tag": "AbiUInt", + "contents": [256, "332"] + } + ] + ] + }, + "_gasprice'": "0xa904461f1" + } +] +``` + +Очевидно, що ці вхідні дані не викличуть збій у нашій властивості. Однак на наступному кроці ми побачимо, як це змінити. + +### Використання початкових даних для корпусу {#seeding-a-corpus} + +Echidna потребує деякої допомоги, щоб впоратися з функцією `magic`. Ми збираємося скопіювати та змінити вхідні дані, щоб використовувати відповідні +параметри для неї: + +```bash +cp corpus/2712688662897926208.txt corpus/new.txt +``` + +Ми змінимо `new.txt`, щоб викликати `magic(42,129,333,0)`. Тепер ми можемо перезапустити Echidna: + +```bash +echidna-test magic.sol --config config.yaml +... +echidna_magic_values: збій!💥 + Послідовність виклику: + magic(42,129,333,0) + + +Unique instructions: 142 +Unique codehashes: 1 +Seed: -7293830866560616537 + +``` + +Цього разу було виявлено, що властивість порушується негайно. + +## Пошук транзакцій з високим споживанням газу {#finding-transactions-with-high-gas-consumption} + +Розглянемо, як за допомогою Echidna знаходити транзакції з високим споживанням газу. Ціллю є наступний смарт-контракт: + +```solidity +contract C { + uint state; + + function expensive(uint8 times) internal { + for(uint8 i=0; i < times; i++) + state = state + i; + } + + function f(uint x, uint y, uint8 times) public { + if (x == 42 && y == 123) + expensive(times); + else + state = 0; + } + + function echidna_test() public returns (bool) { + return true; + } + +} +``` + +Тут `expensive` може мати велике споживання газу. + +Наразі Echidna завжди потребує властивості для тестування: тут `echidna_test` завжди повертає `true`. +Ми можемо запустити Echidna, щоб перевірити це: + +``` +echidna-test gas.sol +... +echidna_test: пройдено! 🎉 + +Seed: 2320549945714142710 +``` + +### Вимірювання споживання газу {#measuring-gas-consumption} + +Щоб увімкнути вимірювання споживання газу за допомогою Echidna, створіть файл конфігурації `config.yaml`: + +```yaml +estimateGas: true +``` + +У цьому прикладі ми також зменшимо розмір послідовності транзакцій, щоб полегшити розуміння результатів: + +```yaml +seqLen: 2 +estimateGas: true +``` + +### Запуск Echidna {#run-echidna-3} + +Після створення файлу конфігурації ми можемо запустити Echidna так: + +```bash +echidna-test gas.sol --config config.yaml +... +echidna_test: пройдено! 🎉 + +f використала максимум 1333608 газу + Послідовність виклику: + f(42,123,249) Gas price: 0x10d5733f0a Time delay: 0x495e5 Block delay: 0x88b2 + +Unique instructions: 157 +Unique codehashes: 1 +Seed: -325611019680165325 + +``` + +- Показаний газ є оцінкою, наданою [HEVM](https://github.com/dapphub/dapptools/tree/master/src/hevm#hevm-). + +### Відфільтровування викликів, що зменшують газ {#filtering-out-gas-reducing-calls} + +Посібник з **фільтрування функцій для виклику під час кампанії фазингу** вище показує, як +видалити деякі функції з тестування. +Це може бути критично важливим для отримання точної оцінки газу. +Розгляньмо такий приклад: + +```solidity +contract C { + address [] addrs; + function push(address a) public { + addrs.push(a); + } + function pop() public { + addrs.pop(); + } + function clear() public{ + addrs.length = 0; + } + function check() public{ + for(uint256 i = 0; i < addrs.length; i++) + for(uint256 j = i+1; j < addrs.length; j++) + if (addrs[i] == addrs[j]) + addrs[j] = address(0x0); + } + function echidna_test() public returns (bool) { + return true; + } +} +``` + +Якщо Echidna може викликати всі функції, вона не зможе легко знайти транзакції з високою вартістю газу: + +``` +echidna-test pushpop.sol --config config.yaml +... +pop used a maximum of 10746 gas +... +check used a maximum of 23730 gas +... +clear used a maximum of 35916 gas +... +push used a maximum of 40839 gas +``` + +Це тому, що вартість залежить від розміру `addrs`, а випадкові виклики, як правило, залишають масив майже порожнім. +Однак додавання `pop` та `clear` до чорного списку дає нам набагато кращі результати: + +```yaml +filterBlacklist: true +filterFunctions: ["pop", "clear"] +``` + +``` +echidna-test pushpop.sol --config config.yaml +... +push used a maximum of 40839 gas +... +check used a maximum of 1484472 gas +``` + +### Підсумок: пошук транзакцій з високим споживанням газу {#summary-finding-transactions-with-high-gas-consumption} + +Echidna може знаходити транзакції з високим споживанням газу за допомогою параметра конфігурації `estimateGas`: + +```yaml +estimateGas: true +``` + +```bash +echidna-test contract.sol --config config.yaml +... +``` + +Echidna повідомить про послідовність із максимальним споживанням газу для кожної функції після завершення кампанії фазингу. diff --git a/public/content/translations/uk/developers/tutorials/how-to-use-manticore-to-find-smart-contract-bugs/index.md b/public/content/translations/uk/developers/tutorials/how-to-use-manticore-to-find-smart-contract-bugs/index.md new file mode 100644 index 00000000000..9199f4c82dd --- /dev/null +++ b/public/content/translations/uk/developers/tutorials/how-to-use-manticore-to-find-smart-contract-bugs/index.md @@ -0,0 +1,526 @@ +--- +title: "Як використовувати Manticore для пошуку помилок у смарт-контрактах" +description: "Як використовувати Manticore для автоматичного пошуку помилок у смарт-контрактах" +author: Trailofbits +lang: uk +tags: + [ + "мова програмування", + "Смарт-контракти", + "захист", + "тестування", + "формальна верифікація" + ] +skill: advanced +published: 2020-01-13 +source: Building secure contracts +sourceUrl: https://github.com/crytic/building-secure-contracts/tree/master/program-analysis/manticore +--- + +Мета цього посібника — показати, як використовувати Manticore для автоматичного пошуку помилок у смарт-контрактах. + +## Встановлення {#installation} + +Для Manticore потрібна версія Python >= 3.6. Його можна інсталювати за допомогою pip або за допомогою Docker. + +### Manticore через Docker {#manticore-through-docker} + +```bash +docker pull trailofbits/eth-security-toolbox +docker run -it -v "$PWD":/home/training trailofbits/eth-security-toolbox +``` + +_Остання команда запускає eth-security-toolbox у контейнері Docker, який має доступ до вашого поточного каталогу. Ви можете змінювати файли з вашого хоста та запускати інструменти для файлів із контейнера Docker_ + +Усередині контейнера Docker запустіть: + +```bash +solc-select 0.5.11 +cd /home/trufflecon/ +``` + +### Manticore через pip {#manticore-through-pip} + +```bash +pip3 install --user manticore +``` + +Рекомендується використовувати solc 0.5.11. + +### Запуск скрипту {#running-a-script} + +Щоб запустити скрипт Python за допомогою Python 3: + +```bash +python3 script.py +``` + +## Вступ до динамічного символьного виконання {#introduction-to-dynamic-symbolic-execution} + +### Коротко про динамічне символьне виконання {#dynamic-symbolic-execution-in-a-nutshell} + +Динамічне символьне виконання (DSE) — це метод аналізу програм, який досліджує простір станів із високим ступенем семантичної обізнаності. Цей метод базується на виявленні "шляхів програми", представлених у вигляді математичних формул, які називаються `предикатами шляху`. Концептуально цей метод оперує предикатами шляху у два етапи: + +1. Вони будуються з використанням обмежень на вхідні дані програми. +2. Вони використовуються для генерації вхідних даних програми, які спричинять виконання пов'язаних шляхів. + +Цей підхід не дає хибнопозитивних спрацьовувань у тому сенсі, що всі виявлені стани програми можуть бути викликані під час конкретного виконання. Наприклад, якщо аналіз знаходить переповнення цілого числа, його гарантовано можна відтворити. + +### Приклад предиката шляху {#path-predicate-example} + +Щоб зрозуміти, як працює DSE, розгляньмо такий приклад: + +```solidity +function f(uint a){ + + if (a == 65) { + // Наявна помилка + } + +} +``` + +Оскільки `f()` містить два шляхи, DSE побудує два різні предикати шляху: + +- Шлях 1: `a == 65` +- Шлях 2: `Not (a == 65)` + +Кожен предикат шляху — це математична формула, яку можна передати так званому [SMT-рішувачу](https://wikipedia.org/wiki/Satisfiability_modulo_theories), який спробує розв'язати рівняння. Для `Шляху 1` рішувач повідомить, що шлях можна дослідити за допомогою `a = 65`. Для `Шляху 2` рішувач може надати `a` будь-яке значення, відмінне від 65, наприклад `a = 0`. + +### Перевірка властивостей {#verifying-properties} + +Manticore дає змогу повністю контролювати виконання кожного шляху. Завдяки цьому ви можете додавати довільні обмеження майже до будь-чого. Цей контроль дає змогу створювати властивості контракту. + +Розгляньмо такий приклад: + +```solidity +function unsafe_add(uint a, uint b) returns(uint c){ + c = a + b; // немає захисту від переповнення + return c; +} +``` + +Тут є лише один шлях для дослідження у функції: + +- Шлях 1: `c = a + b` + +За допомогою Manticore ви можете перевірити наявність переповнення та додати обмеження до предиката шляху: + +- `c = a + b AND (c < a OR c < b)` + +Якщо можливо знайти такі значення `a` та `b`, для яких наведений вище предикат шляху є здійсненним, це означає, що ви знайшли переповнення. Наприклад, рішувач може згенерувати вхідні дані `a = 10, b = MAXUINT256`. + +Якщо розглянути виправлену версію: + +```solidity +function safe_add(uint a, uint b) returns(uint c){ + c = a + b; + require(c>=a); + require(c>=b); + return c; +} +``` + +Відповідна формула з перевіркою переповнення матиме такий вигляд: + +- `c = a + b AND (c >= a) AND (c=>b) AND (c < a OR c < b)` + +Цю формулу неможливо розв'язати; іншими словами, це **доказ** того, що в `safe_add` `c` завжди збільшуватиметься. + +Таким чином, DSE є потужним інструментом, який може перевіряти довільні обмеження у вашому коді. + +## Запуск у Manticore {#running-under-manticore} + +Ми розглянемо, як досліджувати смарт-контракт за допомогою Manticore API. Цільовим є такий смарт-контракт [`example.sol`](https://github.com/crytic/building-secure-contracts/blob/master/program-analysis/manticore/examples/example.sol): + +```solidity +pragma solidity >=0.4.24 <0.6.0; + +contract Simple { + function f(uint a) payable public{ + if (a == 65) { + revert(); + } + } +} +``` + +### Запуск автономного дослідження {#run-a-standalone-exploration} + +Ви можете запустити Manticore безпосередньо для смарт-контракту за допомогою такої команди (`project` може бути файлом Solidity або каталогом проєкту): + +```bash +$ manticore project +``` + +Ви отримаєте вивід тестових випадків, подібний до цього (порядок може змінюватися): + +``` +... +... m.c.manticore:INFO: Generated testcase No. 0 - STOP +... m.c.manticore:INFO: Generated testcase No. 1 - REVERT +... m.c.manticore:INFO: Generated testcase No. 2 - RETURN +... m.c.manticore:INFO: Generated testcase No. 3 - REVERT +... m.c.manticore:INFO: Generated testcase No. 4 - STOP +... m.c.manticore:INFO: Generated testcase No. 5 - REVERT +... m.c.manticore:INFO: Generated testcase No. 6 - REVERT +... m.c.manticore:INFO: Results in /home/ethsec/workshops/Automated Smart Contracts Audit - TruffleCon 2018/manticore/examples/mcore_t6vi6ij3 +... +``` + +Без додаткової інформації Manticore досліджуватиме контракт за допомогою нових символьних +транзакцій, доки не перестане знаходити нові шляхи в контракті. Manticore не запускає нові транзакції після невдалої (наприклад, після скасування). + +Manticore виведе інформацію в каталог `mcore_*`. Серед іншого, у цьому каталозі ви знайдете: + +- `global.summary`: покриття та попередження компілятора +- `test_XXXXX.summary`: покриття, остання інструкція, баланси облікових записів для кожного тестового випадку +- `test_XXXXX.tx`: детальний список транзакцій для кожного тестового випадку + +Тут Manticore знаходить 7 тестових випадків, які відповідають (порядок імен файлів може змінюватися): + +| | Транзакція 0 | Транзакція 1 | Транзакція 2 | Результат | +| :-------------------------------------------------------: | :-----------------: | :------------------------: | -------------------------- | :--------: | +| **test_00000000.tx** | Створення контракту | f(!=65) | f(!=65) | Зупинка | +| **test_00000001.tx** | Створення контракту | резервна функція | | Повернення | +| **test_00000002.tx** | Створення контракту | | | Повернення | +| **test_00000003.tx** | Створення контракту | f(65) | | Повернення | +| **test_00000004.tx** | Створення контракту | f(!=65) | | Зупинка | +| **test_00000005.tx** | Створення контракту | f(!=65) | f(65) | Повернення | +| **test_00000006.tx** | Створення контракту | f(!=65) | резервна функція | Повернення | + +_Підсумок дослідження: f(!=65) означає виклик f із будь-яким значенням, відмінним від 65._ + +Як ви можете помітити, Manticore генерує унікальний тестовий випадок для кожної успішної або скасованої транзакції. + +Використовуйте прапорець `--quick-mode` для швидкого дослідження коду (він вимикає детектори помилок, розрахунок газу тощо) + +### Маніпулювання смарт-контрактом через API {#manipulate-a-smart-contract-through-the-api} + +У цьому розділі докладно описано, як маніпулювати смарт-контрактом за допомогою Manticore Python API. Ви можете створити новий файл із розширенням Python `*.py` і написати необхідний код, додавши в цей файл команди API (основи яких будуть описані нижче), а потім запустити його за допомогою команди `$ python3 *.py`. Також ви можете виконати наведені нижче команди безпосередньо в консолі Python. Щоб запустити консоль, скористайтеся командою `$ python3`. + +### Створення облікових записів {#creating-accounts} + +Перше, що вам потрібно зробити, це ініціювати новий блокчейн за допомогою таких команд: + +```python +from manticore.ethereum import ManticoreEVM + +m = ManticoreEVM() +``` + +Неконтрактний обліковий запис створюється за допомогою [m.create_account](https://manticore.readthedocs.io/en/latest/evm.html?highlight=create_account#manticore.ethereum.ManticoreEVM.create_account): + +```python +user_account = m.create_account(balance=1000) +``` + +Контракт Solidity можна розгорнути за допомогою [m.solidity_create_contract](https://manticore.readthedocs.io/en/latest/evm.html?highlight=solidity_create#manticore.ethereum.ManticoreEVM.create_contract): + +```solidity +source_code = ''' +pragma solidity >=0.4.24 <0.6.0; +contract Simple { + function f(uint a) payable public{ + if (a == 65) { + revert(); + } + } +} +''' +# Initiate the contract +contract_account = m.solidity_create_contract(source_code, owner=user_account) +``` + +#### Підсумок {#summary} + +- Ви можете створювати облікові записи користувачів і контракти за допомогою [m.create_account](https://manticore.readthedocs.io/en/latest/evm.html?highlight=create_account#manticore.ethereum.ManticoreEVM.create_account) та [m.solidity_create_contract](https://manticore.readthedocs.io/en/latest/evm.html?highlight=solidity_create#manticore.ethereum.ManticoreEVM.create_contract). + +### Виконання транзакцій {#executing-transactions} + +Manticore підтримує два типи транзакцій: + +- Необроблена транзакція: досліджуються всі функції +- Іменована транзакція: досліджується лише одна функція + +#### Необроблена транзакція {#raw-transaction} + +Необроблена транзакція виконується за допомогою [m.transaction](https://manticore.readthedocs.io/en/latest/evm.html?highlight=transaction#manticore.ethereum.ManticoreEVM.transaction): + +```python +m.transaction(caller=user_account, + address=contract_account, + data=data, + value=value) +``` + +Той, хто викликає, адреса, дані або значення транзакції можуть бути конкретними або символьними: + +- [m.make_symbolic_value](https://manticore.readthedocs.io/en/latest/evm.html?highlight=make_symbolic_value#manticore.ethereum.ManticoreEVM.make_symbolic_value) створює символьне значення. +- [m.make_symbolic_buffer(size)](https://manticore.readthedocs.io/en/latest/evm.html?highlight=make_symbolic_buffer#manticore.ethereum.ManticoreEVM.make_symbolic_buffer) створює символьний масив байтів. + +Наприклад: + +```python +symbolic_value = m.make_symbolic_value() +symbolic_data = m.make_symbolic_buffer(320) +m.transaction(caller=user_account, + address=contract_address, + data=symbolic_data, + value=symbolic_value) +``` + +Якщо дані є символьними, Manticore дослідить усі функції контракту під час виконання транзакції. Щоб зрозуміти, як працює вибір функції, корисно ознайомитися з поясненням резервної функції в статті [Hands on the Ethernaut CTF](https://blog.trailofbits.com/2017/11/06/hands-on-the-ethernaut-ctf/). + +#### Іменована транзакція {#named-transaction} + +Функції можна виконувати за їхньою назвою. +Щоб виконати `f(uint var)` із символьним значенням від user_account і з 0 ефіру, використовуйте: + +```python +symbolic_var = m.make_symbolic_value() +contract_account.f(symbolic_var, caller=user_account, value=0) +``` + +Якщо `value` транзакції не вказано, за замовчуванням воно дорівнює 0. + +#### Підсумок {#summary-1} + +- Аргументи транзакції можуть бути конкретними або символьними +- Необроблена транзакція досліджуватиме всі функції +- Функції можна викликати за їхньою назвою + +### Робоча область {#workspace} + +`m.workspace` — це каталог, який використовується як вихідний каталог для всіх створених файлів: + +```python +print("Results are in {}".format(m.workspace)) +``` + +### Завершення дослідження {#terminate-the-exploration} + +Щоб зупинити дослідження, використовуйте [m.finalize()](https://manticore.readthedocs.io/en/latest/evm.html?highlight=finalize#manticore.ethereum.ManticoreEVM.finalize). Після виклику цього методу не слід надсилати жодних транзакцій, і Manticore генерує тестові випадки для кожного дослідженого шляху. + +### Підсумок: Запуск у Manticore {#summary-running-under-manticore} + +Об'єднавши всі попередні кроки, отримуємо: + +```python +from manticore.ethereum import ManticoreEVM + +m = ManticoreEVM() + +with open('example.sol') as f: + source_code = f.read() + +user_account = m.create_account(balance=1000) +contract_account = m.solidity_create_contract(source_code, owner=user_account) + +symbolic_var = m.make_symbolic_value() +contract_account.f(symbolic_var) + +print("Results are in {}".format(m.workspace)) +m.finalize() # зупинити дослідження +``` + +Весь наведений вище код можна знайти в [`example_run.py`](https://github.com/crytic/building-secure-contracts/blob/master/program-analysis/manticore/examples/example_run.py) + +## Отримання шляхів, що спричиняють винятки {#getting-throwing-paths} + +Тепер ми згенеруємо конкретні вхідні дані для шляхів, які викликають виняток у `f()`. Цільовим є такий смарт-контракт [`example.sol`](https://github.com/crytic/building-secure-contracts/blob/master/program-analysis/manticore/examples/example.sol): + +```solidity +pragma solidity >=0.4.24 <0.6.0; +contract Simple { + function f(uint a) payable public{ + if (a == 65) { + revert(); + } + } +} +``` + +### Використання інформації про стан {#using-state-information} + +Кожен виконаний шлях має свій стан блокчейну. Стан може бути або готовим (ready), або завершеним (killed), що означає, що він досяг інструкції THROW або REVERT: + +- [m.ready_states](https://manticore.readthedocs.io/en/latest/states.html#accessing): список готових станів (вони не виконали REVERT/INVALID) +- [m.killed_states](https://manticore.readthedocs.io/en/latest/states.html#accessings): список завершених станів +- [m.all_states](https://manticore.readthedocs.io/en/latest/states.html#accessings): усі стани + +```python +for state in m.all_states: + # щось зробити зі станом +``` + +Ви можете отримати доступ до інформації про стан. Наприклад: + +- `state.platform.get_balance(account.address)`: баланс облікового запису +- `state.platform.transactions`: список транзакцій +- `state.platform.transactions[-1].return_data`: дані, повернуті останньою транзакцією + +Дані, повернуті останньою транзакцією, є масивом, який можна перетворити на значення за допомогою ABI.deserialize, наприклад: + +```python +data = state.platform.transactions[0].return_data +data = ABI.deserialize("uint", data) +``` + +### Як згенерувати тестовий випадок {#how-to-generate-testcase} + +Використовуйте [m.generate_testcase(state, name)](https://manticore.readthedocs.io/en/latest/evm.html?highlight=generate_testcase#manticore.ethereum.ManticoreEVM.generate_testcase) для генерації тестового випадку: + +```python +m.generate_testcase(state, 'BugFound') +``` + +### Підсумок {#summary-2} + +- Ви можете ітерувати стани за допомогою m.all_states +- `state.platform.get_balance(account.address)` повертає баланс облікового запису +- `state.platform.transactions` повертає список транзакцій +- `transaction.return_data` — це повернуті дані +- `m.generate_testcase(state, name)` генерує вхідні дані для стану + +### Підсумок: Отримання шляху, що спричиняє виняток {#summary-getting-throwing-path} + +```python +from manticore.ethereum import ManticoreEVM + +m = ManticoreEVM() + +with open('example.sol') as f: + source_code = f.read() + +user_account = m.create_account(balance=1000) +contract_account = m.solidity_create_contract(source_code, owner=user_account) + +symbolic_var = m.make_symbolic_value() +contract_account.f(symbolic_var) + +## Перевірка, чи завершується виконання з REVERT або INVALID + +for state in m.terminated_states: + last_tx = state.platform.transactions[-1] + if last_tx.result in ['REVERT', 'INVALID']: + print('Throw found {}'.format(m.workspace)) + m.generate_testcase(state, 'ThrowFound') +``` + +Весь наведений вище код можна знайти в [`example_run.py`](https://github.com/crytic/building-secure-contracts/blob/master/program-analysis/manticore/examples/example_run.py) + +_Зауважте, що ми могли б створити набагато простіший скрипт, оскільки всі стани, що повертаються terminated_state, мають у своєму результаті REVERT або INVALID: цей приклад мав лише продемонструвати, як маніпулювати API._ + +## Додавання обмежень {#adding-constraints} + +Ми розглянемо, як обмежувати дослідження. Припустимо, що в +документації `f()` зазначено, що функція ніколи не викликається з `a == 65`, тому будь-яка помилка при `a == 65` не є справжньою помилкою. Цільовим є такий смарт-контракт [`example.sol`](https://github.com/crytic/building-secure-contracts/blob/master/program-analysis/manticore/examples/example.sol): + +```solidity +pragma solidity >=0.4.24 <0.6.0; +contract Simple { + function f(uint a) payable public{ + if (a == 65) { + revert(); + } + } +} +``` + +### Оператори {#operators} + +Модуль [Operators](https://github.com/trailofbits/manticore/blob/master/manticore/core/smtlib/operators.py) полегшує маніпулювання обмеженнями, серед іншого він надає: + +- Operators.AND, +- Operators.OR, +- Operators.UGT (беззнакове «більше ніж»), +- Operators.UGE (беззнакове «більше або дорівнює»), +- Operators.ULT (беззнакове «менше ніж»), +- Operators.ULE (беззнакове «менше або дорівнює»). + +Щоб імпортувати модуль, використовуйте таке: + +```python +from manticore.core.smtlib import Operators +``` + +`Operators.CONCAT` використовується для конкатенації масиву зі значенням. Наприклад, return_data транзакції потрібно змінити на значення, щоб перевірити його відносно іншого значення: + +```python +last_return = Operators.CONCAT(256, *last_return) +``` + +### Обмеження {#state-constraint} + +Ви можете використовувати обмеження глобально або для певного стану. + +#### Глобальне обмеження {#state-constraint} + +Використовуйте `m.constrain(constraint)` для додавання глобального обмеження. +Наприклад, ви можете викликати контракт із символьної адреси та обмежити цю адресу певними значеннями: + +```python +symbolic_address = m.make_symbolic_value() +m.constraint(Operators.OR(symbolic == 0x41, symbolic_address == 0x42)) +m.transaction(caller=user_account, + address=contract_account, + data=m.make_symbolic_buffer(320), + value=0) +``` + +#### Обмеження стану {#state-constraint} + +Використовуйте [state.constrain(constraint)](https://manticore.readthedocs.io/en/latest/states.html?highlight=StateBase#manticore.core.state.StateBase.constrain) для додавання обмеження до певного стану. +Його можна використовувати для обмеження стану після його дослідження, щоб перевірити певну властивість. + +### Перевірка обмеження {#checking-constraint} + +Використовуйте `solver.check(state.constraints)`, щоб дізнатися, чи є обмеження все ще здійсненним. +Наприклад, наступний код обмежить symbolic_value значенням, відмінним від 65, і перевірить, чи стан все ще є здійсненним: + +```python +state.constrain(symbolic_var != 65) +if solver.check(state.constraints): + # стан є здійсненним +``` + +### Підсумок: Додавання обмежень {#summary-adding-constraints} + +Додавши обмеження до попереднього коду, отримуємо: + +```python +from manticore.ethereum import ManticoreEVM +from manticore.core.smtlib.solver import Z3Solver + +solver = Z3Solver.instance() + +m = ManticoreEVM() + +with open("example.sol") as f: + source_code = f.read() + +user_account = m.create_account(balance=1000) +contract_account = m.solidity_create_contract(source_code, owner=user_account) + +symbolic_var = m.make_symbolic_value() +contract_account.f(symbolic_var) + +no_bug_found = True + +## Перевірка, чи завершується виконання з REVERT або INVALID + +for state in m.terminated_states: + last_tx = state.platform.transactions[-1] + if last_tx.result in ['REVERT', 'INVALID']: + # ми не розглядаємо шлях, де a == 65 + condition = symbolic_var != 65 + if m.generate_testcase(state, name="BugFound", only_if=condition): + print(f'Bug found, results are in {m.workspace}') + no_bug_found = False + +if no_bug_found: + print(f'No bug found') +``` + +Весь наведений вище код можна знайти в [`example_run.py`](https://github.com/crytic/building-secure-contracts/blob/master/program-analysis/manticore/examples/example_run.py) diff --git a/public/content/translations/uk/developers/tutorials/how-to-use-slither-to-find-smart-contract-bugs/index.md b/public/content/translations/uk/developers/tutorials/how-to-use-slither-to-find-smart-contract-bugs/index.md new file mode 100644 index 00000000000..999343a4c86 --- /dev/null +++ b/public/content/translations/uk/developers/tutorials/how-to-use-slither-to-find-smart-contract-bugs/index.md @@ -0,0 +1,239 @@ +--- +title: "Як використовувати Slither для пошуку помилок у смартконтрактах" +description: "Як використовувати Slither для автоматичного пошуку помилок у смартконтрактах" +author: Trailofbits +lang: uk +tags: + [ + "мова програмування", + "Смарт-контракти", + "захист", + "тестування" + ] +skill: advanced +published: 2020-06-09 +source: Building secure contracts +sourceUrl: https://github.com/crytic/building-secure-contracts/tree/master/program-analysis/slither +--- + +## Як використовувати Slither {#how-to-use-slither} + +Мета цього посібника — показати, як використовувати Slither для автоматичного пошуку помилок у смарт-контрактах. + +- [Встановлення](#installation) +- [Використання командного рядка](#command-line) +- [Вступ до статичного аналізу](#static-analysis): Короткий вступ до статичного аналізу +- [API](#api-basics): Опис Python API + +## Встановлення {#installation} + +Slither вимагає Python >= 3.6. Його можна інсталювати за допомогою pip або за допомогою Docker. + +Slither через pip: + +```bash +pip3 install --user slither-analyzer +``` + +Slither через docker: + +```bash +docker pull trailofbits/eth-security-toolbox +docker run -it -v "$PWD":/home/trufflecon trailofbits/eth-security-toolbox +``` + +_Остання команда запускає eth-security-toolbox у контейнері Docker, який має доступ до вашого поточного каталогу. Ви можете змінювати файли з вашого хоста та запускати інструменти для файлів із контейнера Docker_ + +Усередині контейнера Docker запустіть: + +```bash +solc-select 0.5.11 +cd /home/trufflecon/ +``` + +### Запуск скрипту {#running-a-script} + +Щоб запустити скрипт Python за допомогою Python 3: + +```bash +python3 script.py +``` + +### Командний рядок {#command-line} + +**Командний рядок проти користувацьких скриптів.** Slither постачається з набором попередньо визначених детекторів, які знаходять багато поширених помилок. Виклик Slither з командного рядка запустить усі детектори, для цього не потрібні глибокі знання статичного аналізу: + +```bash +slither project_paths +``` + +Окрім детекторів, Slither має можливості перевірки коду за допомогою своїх [принтерів](https://github.com/crytic/slither#printers) та [інструментів](https://github.com/crytic/slither#tools). + +Використовуйте [crytic.io](https://github.com/crytic), щоб отримати доступ до приватних детекторів та інтеграції з GitHub. + +## Статичний аналіз {#static-analysis} + +Можливості та дизайн фреймворку статичного аналізу Slither були описані в публікаціях у блозі ([1](https://blog.trailofbits.com/2018/10/19/slither-a-solidity-static-analysis-framework/), [2](https://blog.trailofbits.com/2019/05/27/slither-the-leading-static-analyzer-for-smart-contracts/)) та в [науковій статті](https://github.com/trailofbits/publications/blob/master/papers/wetseb19.pdf). + +Статичний аналіз існує в різних формах. Ви, найімовірніше, розумієте, що компілятори, як-от [clang](https://clang-analyzer.llvm.org/) та [gcc](https://lwn.net/Articles/806099/), залежать від цих методів дослідження, але вони також лежать в основі ([Infer](https://fbinfer.com/), [CodeClimate](https://codeclimate.com/), [FindBugs](http://findbugs.sourceforge.net/) та інструментів, що базуються на формальних методах, як-от [Frama-C](https://frama-c.com/) і [Polyspace](https://www.mathworks.com/products/polyspace.html)). + +Тут ми не будемо вичерпно розглядати методи статичного аналізу та дослідників. Натомість ми зосередимося на тому, що необхідно для розуміння роботи Slither, щоб ви могли ефективніше використовувати його для пошуку помилок та розуміння коду. + +- [Представлення коду](#code-representation) +- [Аналіз коду](#analysis) +- [Проміжне представлення](#intermediate-representation) + +### Представлення коду {#code-representation} + +На відміну від динамічного аналізу, який розглядає один шлях виконання, статичний аналіз розглядає всі шляхи одночасно. Для цього він покладається на інше представлення коду. Двома найпоширенішими є абстрактне синтаксичне дерево (AST) і граф потоку керування (CFG). + +### Абстрактні синтаксичні дерева (AST) {#abstract-syntax-trees-ast} + +AST використовуються щоразу, коли компілятор парсить код. Це, мабуть, найпростіша структура, на якій можна виконувати статичний аналіз. + +Якщо коротко, AST — це структуроване дерево, де зазвичай кожен лист містить змінну або константу, а внутрішні вузли є операндами або операціями потоку керування. Розгляньте такий код: + +```solidity +function safeAdd(uint a, uint b) pure internal returns(uint){ + if(a + b <= a){ + revert(); + } + return a + b; +} +``` + +Відповідний AST показано в: + +![AST](./ast.png) + +Slither використовує AST, експортований `solc`. + +Хоча AST легко побудувати, він є вкладеною структурою. Іноді це не найпростіша для аналізу структура. Наприклад, щоб визначити операції, які використовуються у виразі `a + b <= a`, ви повинні спочатку проаналізувати `<=` а потім `+`. Поширеним підходом є використання так званого шаблону «Відвідувач» (visitor pattern), який рекурсивно переміщується по дереву. Slither містить загальний відвідувач у [`ExpressionVisitor`](https://github.com/crytic/slither/blob/master/slither/visitors/expression/expression.py). + +Наступний код використовує `ExpressionVisitor`, щоб визначити, чи містить вираз додавання: + +```python +from slither.visitors.expression.expression import ExpressionVisitor +from slither.core.expressions.binary_operation import BinaryOperationType + +class HasAddition(ExpressionVisitor): + + def result(self): + return self._result + + def _post_binary_operation(self, expression): + if expression.type == BinaryOperationType.ADDITION: + self._result = True + +visitor = HasAddition(expression) # expression is the expression to be tested +print(f'The expression {expression} has a addition: {visitor.result()}') +``` + +### Граф потоку керування (CFG) {#control-flow-graph-cfg} + +Другим за поширеністю представленням коду є граф потоку керування (CFG). Як випливає з назви, це графове представлення, яке розкриває всі шляхи виконання. Кожен вузол містить одну або кілька інструкцій. Ребра в графі представляють операції потоку керування (if/then/else, цикл тощо). CFG нашого попереднього прикладу: + +![CFG](./cfg.png) + +CFG — це представлення, на основі якого будується більшість аналізів. + +Існує багато інших представлень коду. Кожне представлення має переваги та недоліки залежно від аналізу, який ви хочете виконати. + +### Аналіз {#analysis} + +Найпростіший тип аналізу, який можна виконати за допомогою Slither, — це синтаксичний аналіз. + +### Синтаксичний аналіз {#syntax-analysis} + +Slither може переміщуватися різними компонентами коду та їхніми представленнями, щоб знаходити невідповідності й недоліки, використовуючи підхід, подібний до зіставлення зі зразком. + +Наприклад, наведені нижче детектори шукають проблеми, пов'язані із синтаксисом: + +- [Затінення змінної стану](https://github.com/crytic/slither/wiki/Detector-Documentation#state-variable-shadowing): ітерує по всіх змінних стану та перевіряє, чи не затіняє якась із них змінну з успадкованого контракту ([state.py#L51-L62](https://github.com/crytic/slither/blob/0441338e055ab7151b30ca69258561a5a793f8ba/slither/detectors/shadowing/state.py#L51-L62)) + +- [Неправильний інтерфейс ERC20](https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-erc20-interface): пошук неправильних сигнатур функцій ERC20 ([incorrect_erc20_interface.py#L34-L55](https://github.com/crytic/slither/blob/0441338e055ab7151b30ca69258561a5a793f8ba/slither/detectors/erc/incorrect_erc20_interface.py#L34-L55)) + +### Семантичний аналіз {#semantic-analysis} + +На відміну від синтаксичного аналізу, семантичний аналіз заглиблюється і аналізує "значення" коду. Ця родина включає деякі широкі типи аналізів. Вони дають потужніші та корисніші результати, але їх також складніше писати. + +Семантичний аналіз використовується для найсучасніших виявлень вразливостей. + +#### Аналіз залежностей даних {#fixed-point-computation} + +Кажуть, що змінна `variable_a` залежить від даних змінної `variable_b`, якщо існує шлях, у якому на значення `variable_a` впливає `variable_b`. + +У наведеному нижче коді `variable_a` залежить від `variable_b`: + +```solidity +// ... +variable_a = variable_b + 1; +``` + +Slither має вбудовані можливості [залежності даних](https://github.com/crytic/slither/wiki/data-dependency) завдяки своєму проміжному представленню (обговорюється в наступному розділі). + +Приклад використання залежності даних можна знайти в [детекторі небезпечної суворої рівності](https://github.com/crytic/slither/wiki/Detector-Documentation#dangerous-strict-equalities). Тут Slither шукатиме порівняння суворої рівності з небезпечним значенням ([incorrect_strict_equality.py#L86-L87](https://github.com/crytic/slither/blob/6d86220a53603476f9567c3358524ea4db07fb25/slither/detectors/statements/incorrect_strict_equality.py#L86-L87)) і повідомить користувача, що слід використовувати `>=` або `<=` замість `==`, щоб не дати зловмиснику заблокувати контракт. Крім іншого, детектор вважатиме небезпечним значення, що повертається викликом `balanceOf(address)` ([incorrect_strict_equality.py#L63-L64](https://github.com/crytic/slither/blob/6d86220a53603476f9567c3358524ea4db07fb25/slither/detectors/statements/incorrect_strict_equality.py#L63-L64)), і використовуватиме механізм залежності даних для відстеження його використання. + +#### Обчислення з нерухомою точкою {#fixed-point-computation} + +Якщо ваш аналіз проходить через CFG і слідує за ребрами, ви, ймовірно, побачите вже відвідані вузли. Наприклад, якщо цикл представлений, як показано нижче: + +```solidity +for(uint i; i < range; ++){ + variable_a += 1 +} +``` + +Ваш аналіз повинен знати, коли зупинитися. Тут є дві основні стратегії: (1) ітерувати кожен вузол скінченну кількість разів, (2) обчислити так звану _нерухому точку_ (fixpoint). Нерухома точка по суті означає, що аналіз цього вузла більше не дає жодної значущої інформації. + +Приклад використання нерухомої точки можна знайти в детекторах повторного входу: Slither досліджує вузли та шукає зовнішні виклики, запис у сховище та читання з нього. Щойно він досягає нерухомої точки ([reentrancy.py#L125-L131](https://github.com/crytic/slither/blob/master/slither/detectors/reentrancy/reentrancy.py#L125-L131)), він припиняє дослідження та аналізує результати, щоб побачити, чи є повторний вхід, за допомогою різних шаблонів повторного входу ([reentrancy_benign.py](https://github.com/crytic/slither/blob/b275bcc824b1b932310cf03b6bfb1a1fef0ebae1/slither/detectors/reentrancy/reentrancy_benign.py), [reentrancy_read_before_write.py](https://github.com/crytic/slither/blob/b275bcc824b1b932310cf03b6bfb1a1fef0ebae1/slither/detectors/reentrancy/reentrancy_read_before_write.py), [reentrancy_eth.py](https://github.com/crytic/slither/blob/b275bcc824b1b932310cf03b6bfb1a1fef0ebae1/slither/detectors/reentrancy/reentrancy_eth.py)). + +Написання аналізів з використанням ефективного обчислення нерухомої точки вимагає доброго розуміння того, як аналіз поширює свою інформацію. + +### Проміжне представлення {#intermediate-representation} + +Проміжне представлення (IR) — це мова, яка має бути більш придатною для статичного аналізу, ніж вихідна. Slither перетворює Solidity у власне проміжне представлення: [SlithIR](https://github.com/crytic/slither/wiki/SlithIR). + +Розуміння SlithIR не є необхідним, якщо ви хочете писати лише базові перевірки. Однак, це стане в пригоді, якщо ви плануєте писати розширений семантичний аналіз. Принтери [SlithIR](https://github.com/crytic/slither/wiki/Printer-documentation#slithir) та [SSA](https://github.com/crytic/slither/wiki/Printer-documentation#slithir-ssa) допоможуть вам зрозуміти, як перекладається код. + +## Основи API {#api-basics} + +Slither має API, який дозволяє досліджувати основні атрибути контракту та його функції. + +Щоб завантажити кодову базу: + +```python +from slither import Slither +slither = Slither('/path/to/project') + +``` + +### Дослідження контрактів і функцій {#exploring-contracts-and-functions} + +Об'єкт `Slither` має: + +- `contracts (list(Contract))`: список контрактів +- `contracts_derived (list(Contract))`: список контрактів, які не успадковуються іншим контрактом (підмножина контрактів) +- `get_contract_from_name (str)`: повертає контракт за його назвою + +Об'єкт `Contract` має: + +- `name (str)`: назва контракту +- `functions (list(Function))`: список функцій +- `modifiers (list(Modifier))`: список функцій +- `all_functions_called (list(Function/Modifier))`: список усіх внутрішніх функцій, доступних для контракту +- `inheritance (list(Contract))`: список успадкованих контрактів +- `get_function_from_signature (str)`: повертає функцію за її сигнатурою +- `get_modifier_from_signature (str)`: повертає модифікатор за його сигнатурою +- `get_state_variable_from_name (str)`: повертає змінну стану за її назвою + +Об’єкт `Function` або `Modifier` має: + +- `name (str)`: назва функції +- `contract (contract)`: контракт, у якому оголошено функцію +- `nodes (list(Node))`: список вузлів, що складають CFG функції/модифікатора +- `entry_point (Node)`: точка входу в CFG +- `variables_read (list(Variable))`: список прочитаних змінних +- `variables_written (list(Variable))`: список записаних змінних +- `state_variables_read (list(StateVariable))`: список прочитаних змінних стану (підмножина `variables_read`) +- `state_variables_written (list(StateVariable))`: список записаних змінних стану (підмножина `variables_written`) diff --git a/public/content/translations/uk/developers/tutorials/how-to-use-tellor-as-your-oracle/index.md b/public/content/translations/uk/developers/tutorials/how-to-use-tellor-as-your-oracle/index.md new file mode 100644 index 00000000000..b33aeb2bc20 --- /dev/null +++ b/public/content/translations/uk/developers/tutorials/how-to-use-tellor-as-your-oracle/index.md @@ -0,0 +1,81 @@ +--- +title: "Як налаштувати Tellor як оракул" +description: "Посібник із початку роботи з інтеграції оракула Tellor у ваш протокол" +author: "Tellor" +lang: uk +tags: [ "мова програмування", "Смарт-контракти", "оракули" ] +skill: beginner +published: 2021-06-29 +source: Tellor Docs +sourceUrl: https://docs.tellor.io/tellor/ +--- + +Експрес-тест: ваш протокол майже готовий, але йому потрібен оракул, щоб отримати доступ до даних поза ланцюгом... Що ви робите? + +## (Бажані) попередні вимоги {#soft-prerequisites} + +Ця стаття має на меті зробити доступ до потоку даних оракула якомога простішим і зрозумілішим. Водночас, щоб зосередитися на аспекті оракула, ми припускаємо, що ви володієте певним рівнем навичок програмування. + +Припущення: + +- ви вмієте працювати з терміналом +- у вас встановлений npm +- ви знаєте, як використовувати npm для керування залежностями + +Tellor — це діючий оракул з відкритим кодом, готовий до впровадження. Цей посібник для початківців демонструє, наскільки легко можна почати роботу з Tellor, забезпечивши ваш проєкт повністю децентралізованим і стійким до цензури оракулом. + +## Огляд {#overview} + +Tellor — це система оракула, у якій сторони можуть запитувати значення точки даних поза ланцюгом (наприклад, BTC/USD), а репортери змагаються за додавання цього значення до банку даних у ланцюзі, доступного для всіх смарт-контрактів Ethereum. Дані, що надходять до цього банку даних, захищені мережею репортерів, які зробили стейкінг. Tellor використовує криптоекономічні механізми стимулювання, винагороджуючи репортерів за чесне подання даних і караючи зловмисників через випуск токена Tellor, Tributes (TRB), та механізм оскарження. + +У цьому посібнику ми розглянемо: + +- Налаштування початкового набору інструментів, який знадобиться для початку роботи. +- Розгляд простого прикладу. +- Перелік адрес тестових мереж, де наразі можна тестувати Tellor. + +## UsingTellor {#usingtellor} + +Перше, що вам потрібно зробити, це встановити основні інструменти, необхідні для використання Tellor як вашого оракула. Використовуйте [цей пакет](https://github.com/tellor-io/usingtellor), щоб встановити контракти користувача Tellor: + +`npm install usingtellor` + +Після встановлення це дозволить вашим контрактам успадковувати функції контракту «UsingTellor». + +Чудово! Тепер, коли інструменти готові, давайте виконаємо просту вправу, у якій ми отримаємо ціну біткоїна: + +### Приклад BTC/USD {#btcusd-example} + +Успадкуйте контракт UsingTellor, передаючи адресу Tellor як аргумент конструктора: + +Ось приклад: + +```solidity +import "usingtellor/contracts/UsingTellor.sol"; + +contract PriceContract is UsingTellor { + uint256 public btcPrice; + + //Цей контракт тепер має доступ до всіх функцій в UsingTellor + +constructor(address payable _tellorAddress) UsingTellor(_tellorAddress) public {} + +function setBtcPrice() public { + bytes memory _b = abi.encode("SpotPrice",abi.encode("btc","usd")); + bytes32 _queryId = keccak256(_b); + + uint256 _timestamp; + bytes _value; + + (_value, _timestamp) = getDataBefore(_queryId, block.timestamp - 15 minutes); + + btcPrice = abi.decode(_value,(uint256)); + } +} +``` + +Повний перелік адрес контрактів дивіться [тут](https://docs.tellor.io/tellor/the-basics/contracts-reference). + +Для зручності використання репозиторій UsingTellor постачається з версією контракту [Tellor Playground](https://github.com/tellor-io/TellorPlayground) для простішої інтеграції. Перелік корисних функцій дивіться [тут](https://github.com/tellor-io/sampleUsingTellor#tellor-playground). + +Для більш надійної реалізації оракула Tellor, перегляньте повний список доступних функцій [тут](https://github.com/tellor-io/usingtellor/blob/master/README.md). diff --git a/public/content/translations/uk/developers/tutorials/how-to-view-nft-in-metamask/index.md b/public/content/translations/uk/developers/tutorials/how-to-view-nft-in-metamask/index.md new file mode 100644 index 00000000000..2148f3edc98 --- /dev/null +++ b/public/content/translations/uk/developers/tutorials/how-to-view-nft-in-metamask/index.md @@ -0,0 +1,33 @@ +--- +title: "Як переглянути NFT у своєму гаманці (частина 3/3 із серії посібників про NFT)" +description: "У цьому посібнику описано, як переглянути наявний NFT на MetaMask!" +author: "Sumi Mudgil" +tags: [ "ERC-721", "Alchemy", "Мова програмування Solidity" ] +skill: beginner +lang: uk +published: 2021-04-22 +--- + +Цей посібник — частина 3/3 із серії посібників про NFT, у якому ми переглянемо наш щойно викарбуваний NFT. Однак, ви можете використовувати загальний посібник для будь-якого токена ERC-721 за допомогою MetaMask, включно з головною мережею (Mainnet) або будь-якою тестовою мережею. Якщо ви хочете дізнатися, як викарбувати власний NFT на Ethereum, вам варто переглянути [Частину 1 про те, як написати й розгорнути смарт-контракт NFT](/developers/tutorials/how-to-write-and-deploy-an-nft)! + +Вітаємо! Ви дісталися найкоротшої та найпростішої частини нашої серії посібників про NFT — як переглянути щойно викарбуваний NFT у віртуальному гаманці. Ми будемо використовувати MetaMask для цього прикладу, оскільки це те, що ми використовували у двох попередніх частинах. + +Як обов’язкова умова, у вас уже має бути встановлений мобільний додаток MetaMask, у якому є обліковий запис, на який ви викарбували свій NFT. Ви можете безкоштовно завантажити додаток для [iOS](https://apps.apple.com/us/app/metamask-blockchain-wallet/id1438144202) або [Android](https://play.google.com/store/apps/details?id=io.metamask&hl=en_US&gl=US). + +## Крок 1. Налаштуйте мережу на Sepolia {#set-network-to-sepolia} + +У верхній частині додатка натисніть кнопку «Гаманець», після чого вам буде запропоновано вибрати мережу. Оскільки наш NFT було викарбувано в мережі Sepolia, виберіть Sepolia як свою мережу. + +![Як установити Sepolia як свою мережу в мобільному додатку MetaMask](./goerliMetamask.gif) + +## Крок 2. Додайте свій колекційний предмет у MetaMask {#add-nft-to-metamask} + +Перейшовши в мережу Sepolia, виберіть праворуч вкладку «Колекційні предмети» (Collectibles) і додайте адресу смарт-контракту NFT та ідентифікатор токена ERC-721 вашого NFT — їх можна знайти на Etherscan за хешем транзакції з вашого NFT, розгорнутого в частині II нашого посібника. + +![Як знайти хеш транзакції та ідентифікатор токена ERC-721](./findNFTEtherscan.png) + +Можливо, вам знадобиться кілька разів оновити сторінку, щоб переглянути свій NFT, але він буде там ! + +![Як завантажити свій NFT у MetaMask](./findNFTMetamask.gif) + +Вітаємо! Ви успішно викарбували NFT і тепер можете його переглянути! Ми не можемо дочекатися, щоб побачити, як ви підкорите світ NFT! diff --git a/public/content/translations/uk/developers/tutorials/how-to-write-and-deploy-an-nft/index.md b/public/content/translations/uk/developers/tutorials/how-to-write-and-deploy-an-nft/index.md new file mode 100644 index 00000000000..81414e23f03 --- /dev/null +++ b/public/content/translations/uk/developers/tutorials/how-to-write-and-deploy-an-nft/index.md @@ -0,0 +1,386 @@ +--- +title: "Як написати та розгорнути NFT (частина 1 із 3 серії посібників з NFT)" +description: "Цей урок є першою частиною серії уроків по NFT, у якому ви крок за кроком дізнаєтеся, як написати та розгорнути невзаємозамінний токен смарт-контракту (токен ERC-721) з використанням Ethereum та Міжпланетної Файлової Системи (IPFS)." +author: "Sumi Mudgil" +tags: + [ + "ERC-721", + "Alchemy", + "Мова програмування Solidity", + "Смарт-контракти" + ] +skill: beginner +lang: uk +published: 2021-04-22 +--- + +Оскільки NFT привертають увагу громадськості до блокчейну, зараз у вас є чудова можливість самостійно розібратися в ажіотажі, опублікувавши власний контракт NFT (токен ERC-721) на блокчейні Ethereum! + +Alchemy надзвичайно пишається тим, що забезпечує роботу найбільших імен у просторі NFT, зокрема Makersplace (нещодавно встановили рекордний продаж цифрових творів мистецтва на Christie’s за 69 мільйонів доларів), Dapper Labs (творці NBA Top Shot і Crypto Kitties), OpenSea (найбільший у світі ринок NFT), Zora, Super Rare, NFTfi, Foundation, Enjin, Origin Protocol, Immutable тощо. + +У цьому посібнику ми розглянемо створення та розгортання смарт-контракту ERC-721 у тестовій мережі Sepolia за допомогою [MetaMask](https://metamask.io/), [Solidity](https://docs.soliditylang.org/en/v0.8.0/), [Hardhat](https://hardhat.org/), [Pinata](https://pinata.cloud/) та [Alchemy](https://alchemy.com/signup/eth) (не хвилюйтеся, якщо ви ще не розумієте, що все це означає — ми пояснимо!). + +У 2-му розділі цього підручника ми поговоримо про те, як ми можемо використовувати смарт-контракти для утворення NFT, та в 3-му розділі ми пояснимо, як переглядати ваш NFT в MetaMask. + +І, звісно, якщо у вас виникнуть запитання, не соромтеся звертатися до [Alchemy Discord](https://discord.gg/gWuC7zB) або відвідати [документацію Alchemy з NFT API](https://docs.alchemy.com/alchemy/enhanced-apis/nft-api)! + +## Крок 1. Підключення до мережі Ethereum {#connect-to-ethereum} + +Існує багато способів робити запити до блокчейну Ethereum, але для спрощення ми скористаємося безкоштовним обліковим записом на [Alchemy](https://alchemy.com/signup/eth), платформі для розробників блокчейну та API, яка дозволяє нам взаємодіяти з ланцюжком Ethereum без необхідності запускати власні вузли. + +У цьому уроці ми також скористаємося інструментами розробників Alchemy для моніторингу та аналітики, аби зрозуміти, що відбувається всередині процесу розгортання смарт-контракту. Якщо у вас ще немає облікового запису Alchemy, ви можете безкоштовно зареєструватися [тут](https://alchemy.com/signup/eth). + +## Крок 2. Створіть свій застосунок (та ключ API) {#make-api-key} + +Після того, як ви створили обліковий запис у Alchemy, ви можете зробити ключ API, створивши додаток. Це дозволить нам робити запити до тестової мережі Sepolia. Перегляньте [цей посібник](https://docs.alchemyapi.io/guides/choosing-a-network), якщо вам цікаво дізнатися більше про тестові мережі. + +1. Перейдіть на сторінку "Створити додаток" на панелі управління Alchemy, навівши курсор на "Додатки" в рядку на панелі навігації та натиснувши на "Створити додаток" + +![Створіть свій застосунок](./create-your-app.png) + +2. Назвіть свій застосунок (ми вибрали «Мій перший NFT!»), надайте короткий опис, виберіть «Ethereum» для поля «Ланцюг» (Chain) та «Sepolia» для вашої мережі. Після Злиття (The Merge) інші тестові мережі були визнані застарілими. + +![Налаштуйте та опублікуйте свій застосунок](./alchemy-explorer-sepolia.png) + +3. Натисніть "Створити додаток", ось і все! Ваш додаток повинен з'явитися у таблиці нижче. + +## Крок 3. Створіть обліковий запис Ethereum (адресу) {#create-eth-address} + +Нам потрібен обліковий запис Ethereum для надсилання та отримання транзакцій. Для цього уроку ми будемо використовувати MetaMask, віртуальний гаманець в браузері, який використовується для керування адресою облікового запису Ethereum. Якщо ви хочете дізнатися більше про те, як працюють транзакції в Ethereum, перегляньте [цю сторінку](/developers/docs/transactions/) від Ethereum Foundation. + +Ви можете завантажити та створити обліковий запис MetaMask безкоштовно [тут](https://metamask.io/download). Створюючи обліковий запис, або якщо він у вас уже є, переконайтеся, що ви перейшли на «Тестову мережу Sepolia» (Sepolia Test Network) у верхньому правому куті (щоб ми не мали справу з реальними грошима). + +![Установіть Sepolia як свою мережу](./metamask-goerli.png) + +## Крок 4. Додайте ефір (ether) із крана (Faucet) {#step-4-add-ether-from-a-faucet} + +Для того, щоб розгорнути наш смартконтракт на тестову мережу, нам знадобляться підроблені ETH. Щоб отримати ETH, ви можете перейти до [крана Sepolia](https://sepoliafaucet.com/), який розміщено на Alchemy, увійти в систему та ввести адресу свого облікового запису, а потім натиснути «Надіслати мені ETH» (Send Me ETH). Ви повинні побачити ETH у вашому обліковому записі MetaMask найближчим часом! + +## Крок 5. Перевірте свій баланс {#check-balance} + +Щоб перевірити, чи є у нас баланс, давайте зробимо запит [eth_getBalance](https://docs.alchemyapi.io/alchemy/documentation/alchemy-api-reference/json-rpc#eth_getbalance) за допомогою [інструмента-композитора від Alchemy](https://composer.alchemyapi.io?composer_state=%7B%22network%22%3A0%2C%22methodName%22%3A%22eth_getBalance%22%2C%22paramValues%22%3A%5B%22%22%2C%22latest%22%5D%7D). Це поверне кількість ETH в ваш гаманець. Після введення вашої адреси облікового запису MetaMask і натисніть кнопку "Відправити запит", ви повинні побачити таку відповідь: + + ``` + `{"jsonrpc": "2.0", "id": 0, "result": "0xde0b6b3a7640000"}` + ``` + +> **Примітка**: цей результат вказано у wei, а не в ETH. Wei використовується в якості найменшого номіналу ether. Конвертація від wei до ETH складає 1 eth = 1018 wei. Отже, якщо ми конвертуємо 0xde0b6b3a7640000 в десятковий дріб, ми отримаємо 1\*1018 wei, що дорівнює 1 ETH. + +Фух! Наші "гроші" все ще там. + +## Крок 6. Ініціалізуйте наш проєкт {#initialize-project} + +Спочатку нам потрібно створити папку для нашого проєкту. Перейдіть до командного рядка та надрукуйте: + + ``` + mkdir my-nft + cd my-nft + ``` + +Тепер, коли ми всередині нашої папки для проєкту, ми будемо використовувати npm для ініціалізації проєкту. Якщо у вас ще не встановлено npm, дотримуйтесь [цих інструкцій](https://docs.alchemyapi.io/alchemy/guides/alchemy-for-macs#1-install-nodejs-and-npm) (нам також знадобиться [Node.js](https://nodejs.org/en/download/), тому завантажте і його!). + + ``` + npm init + ``` + +Неважливо, як ви відповідаєте на встановлені запитання: ось так ми зробили це для довідки: + +```json + назва пакунка: (my-nft) + версія: (1.0.0) + опис: Мій перший NFT! + точка входу: (index.js) + тестова команда: + репозиторій git: + ключові слова: + автор: + ліцензія: (ISC) + Буде записано в /Users/thesuperb1/Desktop/my-nft/package.json: + + { + "name": "my-nft", + "version": "1.0.0", + "description": "Мій перший NFT!", + "main": "index.js", + "scripts": { + "test": "echo \"Помилка: тест не вказано\" && exit 1" + }, + "author": "", + "license": "ISC" + } +``` + +Затвердіть package.json, і ми готові рухатися далі! + +## Крок 7. Установіть [Hardhat](https://hardhat.org/getting-started/#overview) {#install-hardhat} + +Hardhat є середовищем розробки для компіляції, розгортання, тестування та налагодження вашого програмного забезпечення Ethereum. Це допомагає розробникам створювати смарт-контракти та dapp локально перед розгортанням у реальний ланцюжок. + +Всередині нашого проекту my-nft запущено: + + ``` + npm install --save-dev hardhat + ``` + +Перегляньте цю сторінку, щоб дізнатися більше про [інструкції з установки](https://hardhat.org/getting-started/#overview). + +## Крок 8. Створіть проєкт Hardhat {#create-hardhat-project} + +Всередині папки проєкту запустіть: + + ``` + npx hardhat + ``` + +Потім ви маєте побачити вітальне повідомлення та вибір подальших бажаних дій. Оберіть "Створити порожній hardhat.config.js": + + ``` + 888 888 888 888 888 + 888 888 888 888 888 + 888 888 888 888 888 + 8888888888 8888b. 888d888 .d88888 88888b. 8888b. 888888 + 888 888 "88b 888P" d88" 888 888 "88b "88b 888 + 888 888 .d888888 888 888 888 888 888 .d888888 888 + 888 888 888 888 888 Y88b 888 888 888 888 888 Y88b. + 888 888 "Y888888 888 "Y88888 888 888 "Y888888 "Y888 + 👷 Ласкаво просимо до Hardhat v2.0.11 👷‍ + ? Що ви хочете зробити? … + Створити зразок проєкту + ❯ Створити порожній hardhat.config.js + Вийти + ``` + +Це згенерує hardhat.config.js файл для нас, де ми виберемо все готове для нашого проекту (на кроці 13). + +## Крок 9. Додайте папки проєкту {#add-project-folders} + +Щоб зберегти наш проєкт організованим, ми створимо дві нові теки. Перейдіть до кореневого каталогу вашого проєкту у командному рядку та надрукуйте: + + ``` + mkdir contracts + mkdir scripts + ``` + +- contracts/ - це місце, де ми будемо зберігати наш NFT код смарт-контракту + +- scripts/ - це місце, де ми триматимемо скрипти для розгортання та взаємодії з нашим смарт-контрактом + +## Крок 10. Напишіть наш контракт {#write-contract} + +Тепер, коли наше середовище налаштоване, переходимо до найцікавішого: _написання коду нашого смарт-контракту!_ + +Відкрийте проєкт my-nft у вашому улюбленому редакторі (нам подобається [VSCode](https://code.visualstudio.com/)). Смарт-контракти написані мовою під назвою Solidity, яку ми будемо використовувати для того, щоб написати наших MyNFT.sol для смарт-контрактів + +1. Перейдіть до папки `contracts` і створіть новий файл з назвою MyNFT.sol + +2. Нижче наведено код нашого смарт-контракту NFT, який ми заснували на реалізації ERC-721 з бібліотеки [OpenZeppelin](https://docs.openzeppelin.com/contracts/3.x/erc721). Скопіюйте та вставте вміст нижче у свій файл MyNFT.sol. + + ```solidity + //Контракт базується на [https://docs.openzeppelin.com/contracts/3.x/erc721](https://docs.openzeppelin.com/contracts/3.x/erc721) + // SPDX-License-Identifier: MIT + pragma solidity ^0.8.0; + + import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + import "@openzeppelin/contracts/utils/Counters.sol"; + import "@openzeppelin/contracts/access/Ownable.sol"; + import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; + + contract MyNFT is ERC721URIStorage, Ownable { + using Counters for Counters.Counter; + Counters.Counter private _tokenIds; + + constructor() ERC721("MyNFT", "NFT") {} + + function mintNFT(address recipient, string memory tokenURI) + public onlyOwner + returns (uint256) + { + _tokenIds.increment(); + + uint256 newItemId = _tokenIds.current(); + _mint(recipient, newItemId); + _setTokenURI(newItemId, tokenURI); + + return newItemId; + } + } + ``` + +3. Оскільки ми успадковуємо класи з бібліотеки контрактів OpenZeppelin, у вашому командному рядку виконайте `npm install @openzeppelin/contracts^4.0.0`, щоб установити бібліотеку в нашу папку. + +Отже, що саме _робить_ цей код? Давайте розберемо все по пунктах. + +У верхній частині нашого смарт-контракту ми імпортуємо три класи смарт-контрактів [OpenZeppelin](https://openzeppelin.com/): + +- @openzeppelin/контракти/токен/ERC721/ERC721.sol містить впровадження стандарту ERC-721, який наш смарт-контракт NFT успадкує. (Аби бути дійсним NFT, Ваш смарт-контракт повинен реалізувати всі методи стандарту ERC-721.) Щоб дізнатися більше про успадковані функції ERC-721, перегляньте визначення інтерфейсу [тут](https://eips.ethereum.org/EIPS/eip-721). + +- @openzeppelin/контракти/utils/Counters.sol надає лічильники, які можуть бути збільшені або зменшені лише на одиницю. Наш смарт-контракт використовує лічильник, щоб відстежувати загальну кількість NFT, яка оцінюється, і встановити унікальний ідентифікатор у нашому новому NFT. (Для кожного NFT, створеного за допомогою смарт-контракту, повинен бути призначений унікальний ID - тут наш унікальний ID просто визначається загальною кількістю існуюючих NFT. Наприклад, перший NFT, який ми створили зі смарт-контрактом, містить ідентифікатор "1", другий NFT має ідентифікатор "2" тощо). + +- @openzeppelin/contracts/access/Ownable.sol налаштовує [контроль доступу](https://docs.openzeppelin.com/contracts/3.x/access-control) до нашого смарт-контракту, тому лише власник смарт-контракту (тобто ви) може карбувати NFT. (Зверніть увагу, Ви можете увімкнути контроль доступу за Вашим бажанням. Якщо Ви бажаєте, щоб хтось зміг створити NFT, використовуючи Ваш смарт-контракт, видаліть слово Ownable у 10-му рядку і onlyOwner у 17-му рядку.) + +Після виконання наших заявок на імпорт ми маємо власний смарт-контракт NFT, який є напрочуд коротким - він містить лише лічильник, конструктор й одну функцію! Це можливо завдяки успадкованим контрактам OpenZeppelin, які реалізують більшість методів, необхідних для створення NFT, як-от `ownerOf`, що повертає власника NFT, і `transferFrom`, що передає право власності на NFT з одного облікового запису на інший. + +У нашому конструкторі ERC-721, ви помітите, що ми переходимо 2 рядки - "MyNFT" та "NFT.” Перша змінна - це назва смарт-контракту, а друга - його символ. Ви можете назвати кожну з цих змінних як забажаєте! + +Нарешті, у нас є функція `mintNFT(address recipient, string memory tokenURI)`, яка дозволяє нам карбувати NFT! Ви помітите, що ця функція має дві змінні: + +- `address recipient` визначає адресу, яка отримає ваш щойно викарбуваний NFT + +- `string memory tokenURI` — це рядок, який має вести до JSON-документа, що описує метадані NFT. Метадані NFT - це дійсно те, що втілює його до життя, дозволяючи йому мати налаштовані властивості, такі як ім’я, опис, зображення та інші атрибути. У 2-й частині цього уроку ми розкажемо, як налаштувати ці метадані. + +`mintNFT` викликає деякі методи з успадкованої бібліотеки ERC-721 і в кінцевому підсумку повертає число, яке представляє ідентифікатор щойно викарбуваного NFT. + +## Крок 11. Підключіть MetaMask і Alchemy до свого проєкту {#connect-metamask-and-alchemy} + +Тепер, коли ми створили гаманець MetaMask, акаунт в Alchemy і написали наш смарт-контракт, настав час їх об'єднати. + +Для кожної транзакції, що відправляється з вашого віртуального гаманця, потрібен підпис, який можна зробити, використавши ваш унікальний приватний ключ. Щоб надати програмі цей дозвіл, ми можемо безпечно зберегти наш приватний ключ (і ключ API Alchemy) у файлі environment. + +Щоб дізнатися більше про надсилання транзакцій, перегляньте [цей посібник](/developers/tutorials/sending-transactions-using-web3-and-alchemy/) про надсилання транзакцій за допомогою web3. + +Спочатку встановіть пакет dotenv у каталог вашого проєкту: + + ``` + npm install dotenv --save + ``` + +Потім створіть файл `.env` у кореневому каталозі нашого проєкту та додайте до нього свій приватний ключ MetaMask та URL-адресу HTTP Alchemy API. + +- Дотримуйтеся [цих інструкцій](https://metamask.zendesk.com/hc/en-us/articles/360015289632-How-to-Export-an-Account-Private-Key), щоб експортувати свій приватний ключ із MetaMask + +- Дивіться нижче, аби отримати URL-адресу HTTP Alchemy API та скопіюйте його в буфер обміну + +![Скопіюйте URL-адресу вашого Alchemy API](./copy-alchemy-api-url.gif) + +Ваш `.env` тепер має виглядати так: + + ``` + API_URL="https://eth-sepolia.g.alchemy.com/v2/your-api-key" + PRIVATE_KEY="your-metamask-private-key" + ``` + +Щоб фактично підключити їх до нашого коду, ми будемо посилатися на ці змінні в нашому файлі hardhat.config.js у кроці 13. + + + +## Крок 12. Установіть Ethers.js {#install-ethers} + +Ethers.js — це бібліотека, яка полегшує взаємодію та надсилання запитів до Ethereum, огортаючи [стандартні методи JSON-RPC](/developers/docs/apis/json-rpc/) у більш зручні для користувача методи. + +Hardhat значно спрощує інтеграцію [плагінів](https://hardhat.org/plugins/) для додаткових інструментів та розширеної функціональності. Ми скористаємося [плагіном Ethers](https://hardhat.org/docs/plugins/official-plugins#hardhat-ethers) для розгортання контракту ([Ethers.js](https://github.com/ethers-io/ethers.js/) має дуже прості методи розгортання контрактів). + +У каталозі вашого проєкту надрукуйте: + + ``` + npm install --save-dev @nomiclabs/hardhat-ethers ethers@^5.0.0 + ``` + +Також ми будемо викликати ethers в нашому hardhat.config.js у наступному кроці. + +## Крок 13. Оновіть hardhat.config.js {#update-hardhat-config} + +Ми додали ще декілька залежностей і плагінів, тепер нам потрібно оновити hardhat.config.js, щоб наш проєкт дізнався про це. + +Оновіть ваш hardhat.config.js, аби він виглядав наступним чином: + +```js + /** + * @type import('hardhat/config').HardhatUserConfig + */ + require('dotenv').config(); + require("@nomiclabs/hardhat-ethers"); + const { API_URL, PRIVATE_KEY } = process.env; + module.exports = { + solidity: "0.8.1", + defaultNetwork: "sepolia", + networks: { + hardhat: {}, + sepolia: { + url: API_URL, + accounts: [`0x${PRIVATE_KEY}`] + } + }, + } +``` + +## Крок 14. Скомпілюйте наш контракт {#compile-contract} + +Щоб переконатися, що все працює, скомпілюймо наш контракт. Завдання компіляція є одним з вбудованих завдань Hardhat. + +З командного рядка запустіть: + + ``` + npx hardhat compile + ``` + +Ви можете отримати попередження про ідентифікатор ліцензії SPDX, який не зазначено у вихідному файлі, але не треба хвилюватися про це - сподіваємося, що все інше гарно виглядає! Якщо ні, ви завжди можете написати повідомлення в [Discord-каналі Alchemy](https://discord.gg/u72VCg3). + +## Крок 15. Напишіть наш скрипт розгортання {#write-deploy} + +Тепер, коли контракт написано, і файл конфігурації готовий до запуску, настав час написати скрипт розгортання контракту. + +Перейдіть до папки `scripts/` і створіть новий файл `deploy.js`, додавши до нього такий вміст: + +```js +async function main() { + const MyNFT = await ethers.getContractFactory("MyNFT") + + // Початок розгортання, повертає проміс, який вирішується в об'єкт контракту + const myNFT = await MyNFT.deploy() + await myNFT.deployed() + console.log("Контракт розгорнуто за адресою:", myNFT.address) +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) +``` + +Hardhat чудово пояснює, що робить кожен із цих рядків коду, у своєму [посібнику з контрактів](https://hardhat.org/tutorial/testing-contracts.html#writing-tests), ми використали їхні пояснення тут. + + ``` + const MyNFT = await ethers.getContractFactory("MyNFT"); + ``` + +Фабрика смарт-контрактів у ethers.js є абстрагуванням частин програми, яка використовується для розкривання нових смарт-контрактів, тому MyNFT є фабрикою для окремих випадків наших контрактів NFT. При використанні плагіна hardhat-ethers Фабрика Контрактів і зразки контракта поєднані з першим підписником за замовчуванням. + + ``` + const myNFT = await MyNFT.deploy(); + ``` + +Виклик розгортання () на Фабриці контрактів розпочне розгортання і поверне Проміс (Обіцянку), яка розв'язує контракт. Це об'єкт, у якого є метод для кожної з наших функцій смартконтракту. + +## Крок 16. Розгорніть наш контракт {#deploy-contract} + +Ми нарешті готові розгорнути наш розумний контракт! Перейдіть назад до кореневого каталогу вашого проєкту та запустіть в командному рядку: + + ``` + npx hardhat --network sepolia run scripts/deploy.js + ``` + +Тоді ви повинні побачити щось на кшталт: + + ``` + Контракт розгорнуто за адресою: 0x4C5266cCc4b3F426965d2f51b6D910325a0E7650 + ``` + +Якщо ми перейдемо до [Etherscan для Sepolia](https://sepolia.etherscan.io/) і знайдемо адресу нашого контракту, то зможемо побачити, що його успішно розгорнуто. Якщо ви не бачите його одразу, будь ласка, зачекайте, оскільки це може зайняти деякий час. Транзакція буде виглядати приблизно так: + +![Перегляньте адресу вашої транзакції на Etherscan](./etherscan-sepoila-contract-creation.png) + +Адреса в полі «From» (Відправник) має збігатися з адресою вашого облікового запису MetaMask, а в полі «To» (Одержувач) буде вказано «Створення контракту» (Contract Creation). Якщо ми перейдемо до транзакції, ми побачимо адресу нашого контракту в полі "To": + +![Перегляньте адресу вашого контракту на Etherscan](./etherscan-sepolia-tx-details.png) + +Вийшло! Ви щойно розгорнули свій смарт-контракт NFT у ланцюжку Ethereum (тестовій мережі)! + +Щоб зрозуміти, що відбувається «під капотом», перейдімо на вкладку «Explorer» на нашій [панелі інструментів Alchemy](https://dashboard.alchemyapi.io/explorer). Якщо у вас є кілька додатків для Alchemy, то обов'язково отфільтруйте їх та оберіть "MyNFT". + +![Перегляд викликів, зроблених «під капотом», за допомогою панелі Explorer від Alchemy](./alchemy-explorer-goerli.png) + +Тут ви побачите багато викликів JSON-RPC, які Hardhat/Ethers зробили усередині для нас, коли ми викликали функцію .deploy(). Тут варто відзначити два важливих виклики: [eth_sendRawTransaction](/developers/docs/apis/json-rpc/#eth_sendrawtransaction), що є запитом на запис нашого смарт-контракту в ланцюжок Sepolia, та [eth_getTransactionByHash](/developers/docs/apis/json-rpc/#eth_gettransactionbyhash), що є запитом на зчитування інформації про нашу транзакцію за її хешем (типовий підхід під час надсилання транзакцій). Щоб дізнатися більше про надсилання транзакцій, перегляньте цей посібник про [надсилання транзакцій за допомогою Web3](/developers/tutorials/sending-transactions-using-web3-and-alchemy/). + +Ось і все, що стосується 1-ї частини цього уроку. У [частині 2 ми фактично будемо взаємодіяти з нашим смарт-контрактом, карбуючи NFT](/developers/tutorials/how-to-mint-an-nft/), а в [частині 3 ми покажемо вам, як переглянути свій NFT у вашому гаманці Ethereum](/developers/tutorials/how-to-view-nft-in-metamask/)! diff --git a/public/content/translations/uk/developers/tutorials/interact-with-other-contracts-from-solidity/index.md b/public/content/translations/uk/developers/tutorials/interact-with-other-contracts-from-solidity/index.md new file mode 100644 index 00000000000..375af102f2b --- /dev/null +++ b/public/content/translations/uk/developers/tutorials/interact-with-other-contracts-from-solidity/index.md @@ -0,0 +1,179 @@ +--- +title: "Взаємодія з іншими контрактами за допомогою Solidity" +description: "Як розгорнути смарт-контракт з існуючого контракту та взаємодіяти з ним" +author: "jdourlens" +tags: + [ + "Смарт-контракти", + "мова програмування", + "remix", + "розгортання", + "композиційність" + ] +skill: advanced +lang: uk +published: 2020-04-05 +source: EthereumDev +sourceUrl: https://ethereumdev.io/interact-with-other-contracts-from-solidity/ +address: "0x19dE91Af973F404EDF5B4c093983a7c6E3EC8ccE" +--- + +У попередніх посібниках ми багато чого дізналися, [як розгорнути свій перший смарт-контракт](/developers/tutorials/deploying-your-first-smart-contract/), а також додати до нього деякі функції, як-от [керування доступом за допомогою модифікаторів](https://ethereumdev.io/organize-your-code-and-control-access-to-your-smart-contract-with-modifiers/) або [обробка помилок у Solidity](https://ethereumdev.io/handle-errors-in-solidity-with-require-and-revert/). У цьому посібнику ми дізнаємося, як розгорнути смарт-контракт з існуючого контракту та взаємодіяти з ним. + +Ми створимо контракт, який дозволить будь-кому мати власний смарт-контракт `Counter`, створивши для нього фабрику під назвою `CounterFactory`. Спочатку ось код нашого початкового смарт-контракту `Counter`: + +```solidity +pragma solidity 0.5.17; + +contract Counter { + + uint256 private _count; + address private _owner; + address private _factory; + + + modifier onlyOwner(address caller) { + require(caller == _owner, "Ви не є власником контракту"); + _; + } + + modifier onlyFactory() { + require(msg.sender == _factory, "Потрібно використовувати фабрику"); + _; + } + + constructor(address owner) public { + _owner = owner; + _factory = msg.sender; + } + + function getCount() public view returns (uint256) { + return _count; + } + + function increment(address caller) public onlyFactory onlyOwner(caller) { + _count++; + } + +} +``` + +Зауважте, що ми трохи змінили код контракту, щоб відстежувати адресу фабрики та адресу власника контракту. Коли ви викликаєте код контракту з іншого контракту, `msg.sender` посилатиметься на адресу нашої фабрики контрактів. Це **дуже важливий момент для розуміння**, оскільки використання одного контракту для взаємодії з іншими є поширеною практикою. Тому у складних випадках слід звертати увагу на те, хто є відправником (`sender`). + +Для цього ми також додали модифікатор `onlyFactory`, який гарантує, що функція, яка змінює стан, може бути викликана лише фабрикою, яка передасть початкового викликача (`caller`) як параметр. + +Усередині нашої нової фабрики `CounterFactory`, яка керуватиме всіма іншими лічильниками (`Counter`), ми додамо відображення (`mapping`), яке пов'язуватиме власника з адресою його контракту-лічильника: + +```solidity +mapping(address => Counter) _counters; +``` + +В Ethereum відображення (`mapping`) є еквівалентом об'єктів у JavaScript; вони дають змогу зіставити ключ типу A зі значенням типу B. У цьому випадку ми зіставляємо адресу власника з екземпляром його лічильника (`Counter`). + +Створення екземпляра нового лічильника (`Counter`) для когось виглядатиме так: + +```solidity + function createCounter() public { + require (_counters[msg.sender] == Counter(0)); + _counters[msg.sender] = new Counter(msg.sender); + } +``` + +Спочатку ми перевіряємо, чи вже має особа лічильник. Якщо в особи немає лічильника, ми створюємо екземпляр нового лічильника, передаючи її адресу в конструктор `Counter`, і присвоюємо новостворений екземпляр відображенню (`mapping`). + +Щоб отримати значення конкретного лічильника (`Counter`), код виглядатиме так: + +```solidity +function getCount(address account) public view returns (uint256) { + require (_counters[account] != Counter(0)); + return (_counters[account].getCount()); +} + +function getMyCount() public view returns (uint256) { + return (getCount(msg.sender)); +} +``` + +Перша функція перевіряє, чи існує контракт `Counter` для заданої адреси, а потім викликає метод `getCount` з екземпляра. Друга функція, `getMyCount`, — це просто скорочення для прямої передачі `msg.sender` у функцію `getCount`. + +Функція `increment` дуже схожа, але передає початкового відправника транзакції до контракту `Counter`: + +```solidity +function increment() public { + require (_counters[msg.sender] != Counter(0)); + Counter(_counters[msg.sender]).increment(msg.sender); + } +``` + +Зауважте, що якщо викликати її забагато разів, наш лічильник може стати жертвою переповнення. Вам слід якомога частіше використовувати [бібліотеку SafeMath](https://ethereumdev.io/using-safe-math-library-to-prevent-from-overflows/), щоб захиститися від цього можливого випадку. + +Щоб розгорнути наш контракт, вам потрібно буде надати код як `CounterFactory`, так і `Counter`. Наприклад, під час розгортання в Remix вам потрібно буде вибрати `CounterFactory`. + +Ось повний код: + +```solidity +pragma solidity 0.5.17; + +contract Counter { + + uint256 private _count; + address private _owner; + address private _factory; + + + modifier onlyOwner(address caller) { + require(caller == _owner, "Ви не є власником контракту"); + _; + } + + modifier onlyFactory() { + require(msg.sender == _factory, "Потрібно використовувати фабрику"); + _; + } + + constructor(address owner) public { + _owner = owner; + _factory = msg.sender; + } + + function getCount() public view returns (uint256) { + return _count; + } + + function increment(address caller) public onlyFactory onlyOwner(caller) { + _count++; + } + +} + +contract CounterFactory { + + mapping(address => Counter) _counters; + + function createCounter() public { + require (_counters[msg.sender] == Counter(0)); + _counters[msg.sender] = new Counter(msg.sender); + } + + function increment() public { + require (_counters[msg.sender] != Counter(0)); + Counter(_counters[msg.sender]).increment(msg.sender); + } + + function getCount(address account) public view returns (uint256) { + require (_counters[account] != Counter(0)); + return (_counters[account].getCount()); + } + + function getMyCount() public view returns (uint256) { + return (getCount(msg.sender)); + } + +} +``` + +Після компіляції в розділі розгортання Remix виберіть фабрику для розгортання: + +![Вибір фабрики для розгортання в Remix](./counterfactory-deploy.png) + +Потім ви можете поекспериментувати з вашою фабрикою контрактів і перевірити, як змінюється значення. Якщо ви хочете викликати смарт-контракт з іншої адреси, вам потрібно буде змінити адресу у випадаючому списку `Account` в Remix. diff --git a/public/content/translations/uk/developers/tutorials/ipfs-decentralized-ui/index.md b/public/content/translations/uk/developers/tutorials/ipfs-decentralized-ui/index.md new file mode 100644 index 00000000000..3909842a163 --- /dev/null +++ b/public/content/translations/uk/developers/tutorials/ipfs-decentralized-ui/index.md @@ -0,0 +1,73 @@ +--- +title: "IPFS для децентралізованих інтерфейсів користувача" +description: "У цьому посібнику пояснюється, як використовувати IPFS для зберігання інтерфейсу користувача для dapp. Хоча дані та бізнес-логіка програми децентралізовані, без стійкого до цензури інтерфейсу користувача користувачі все одно можуть втратити до нього доступ." +author: Ori Pomerantz +tags: [ "ipfs" ] +skill: beginner +lang: uk +published: 2024-06-29 +--- + +Ви написали неймовірну нову dapp. Ви навіть написали [інтерфейс користувача](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/) для неї. Але тепер ви боїтеся, що хтось спробує її цензурувати, вимкнувши ваш інтерфейс користувача, який є лише одним сервером у хмарі. У цьому посібнику ви дізнаєтеся, як уникнути цензури, розмістивши свій інтерфейс користувача в **[міжпланетній файловій системі (IPFS)](https://ipfs.tech/developers/)**, щоб будь-хто зацікавлений міг закріпити його на сервері для майбутнього доступу. + +Ви могли б використати сторонній сервіс, наприклад, [Fleek](https://resources.fleek.xyz/docs/), щоб виконати всю роботу. Цей посібник призначений для тих, хто хоче зробити достатньо, щоб зрозуміти, що вони роблять, навіть якщо це вимагає більше роботи. + +## Початок роботи локально {#getting-started-locally} + +Існує кілька [сторонніх провайдерів IPFS](https://docs.ipfs.tech/how-to/work-with-pinning-services/#use-a-third-party-pinning-service), але найкраще почати з локального запуску IPFS для тестування. + +1. Установіть [інтерфейс користувача IPFS](https://docs.ipfs.tech/install/ipfs-desktop/#install-instructions). + +2. Створіть каталог із вашим вебсайтом. Якщо ви використовуєте [Vite](https://vite.dev/), використовуйте цю команду: + + ```sh + pnpm vite build + ``` + +3. У IPFS Desktop натисніть **Імпортувати > Папка** і виберіть каталог, який ви створили на попередньому кроці. + +4. Виберіть щойно завантажену папку і натисніть **Перейменувати**. Дайте їй змістовнішу назву. + +5. Виберіть її знову і натисніть **Поділитися посиланням**. Скопіюйте URL-адресу в буфер обміну. Посилання буде схоже на `https://ipfs.io/ipfs/QmaCuQ7yN6iyBjLmLGe8YiFuCwnePoKfVu6ue8vLBsLJQJ`. + +6. Натисніть **Статус**. Розгорніть вкладку **Додатково**, щоб побачити адресу шлюзу. Наприклад, у моїй системі адреса: `http://127.0.0.1:8080`. + +7. Об’єднайте шлях із кроку з посиланням з адресою шлюзу, щоб знайти вашу адресу. Наприклад, для наведеного вище прикладу, URL-адреса: `http://127.0.0.1:8080/ipfs/QmaCuQ7yN6iyBjLmLGe8YiFuCwnePoKfVu6ue8vLBsLJQJ`. Відкрийте цю URL-адресу в браузері, щоб побачити свій сайт. + +## Завантаження {#uploading} + +Отже, тепер ви можете використовувати IPFS для локального надання файлів, що не дуже захопливо. Наступний крок — зробити їх доступними для всього світу, коли ви не в мережі. + +Існує низка добре відомих [сервісів закріплення](https://docs.ipfs.tech/concepts/persistence/#pinning-services). Виберіть один із них. Який би сервіс ви не використовували, вам потрібно створити обліковий запис і надати йому **ідентифікатор контенту (CID)** у вашому IPFS desktop. + +Особисто я вважаю [4EVERLAND](https://docs.4everland.org/storage/4ever-pin/guides) найпростішим у використанні. Ось інструкції для нього: + +1. Перейдіть на [інформаційну панель](https://dashboard.4everland.org/overview) і увійдіть за допомогою свого гаманця. + +2. У лівій бічній панелі натисніть **Сховище > 4EVER Pin**. + +3. Натисніть **Завантажити > Вибраний CID**. Дайте назву своєму вмісту і надайте CID із IPFS desktop. Наразі CID — це рядок, що починається з `Qm`, за яким ідуть 44 літери та цифри, які представляють [закодований за стандартом base-58](https://medium.com/bootdotdev/base64-vs-base58-encoding-c25553ff4524) хеш, наприклад, `QmaCuQ7yN6iyBjLmLGe8YiFuCwnePoKfVu6ue8vLBsLJQJ`, але [це, ймовірно, зміниться](https://docs.ipfs.tech/concepts/content-addressing/#version-1-v1). + +4. Початковий статус: **У черзі**. Оновлюйте, доки він не зміниться на **Закріплено**. + +5. Натисніть на свій CID, щоб отримати посилання. Ви можете побачити мою програму [тут](https://bafybeifqka2odrne5b6l5guthqvbxu4pujko2i6rx2zslvr3qxs6u5o7im.ipfs.dweb.link/). + +6. Вам може знадобитися активувати свій обліковий запис, щоб він був закріплений більше ніж на місяць. Активація облікового запису коштує близько 1 дол. США. Якщо ви закрили його, вийдіть із системи та увійдіть знову, щоб вам знову запропонували активувати. + +## Використання з IPFS {#using-from-ipfs} + +На цьому етапі у вас є посилання на централізований шлюз, який обслуговує ваш вміст IPFS. Коротко кажучи, ваш інтерфейс користувача може бути трохи безпечнішим, але він усе ще не є стійким до цензури. Для справжньої стійкості до цензури користувачам потрібно використовувати IPFS [безпосередньо з браузера](https://docs.ipfs.tech/install/ipfs-companion/#prerequisites). + +Після того як ви його встановите (і IPFS desktop запрацює), ви можете перейти на [/ipfs/``](https://any.site/ipfs/bafybeifqka2odrne5b6l5guthqvbxu4pujko2i6rx2zslvr3qxs6u5o7im) на будь-якому сайті і отримаєте цей вміст, наданий у децентралізований спосіб. + +## Недоліки {#drawbacks} + +Ви не можете надійно видалити файли IPFS, тому, доки ви змінюєте свій інтерфейс користувача, імовірно, краще залишити його централізованим або використовувати [міжпланетну систему імен (IPNS)](https://docs.ipfs.tech/concepts/ipns/#mutability-in-ipfs), систему, що забезпечує змінність поверх IPFS. Звичайно, все, що можна змінити, може бути піддано цензурі, у випадку з IPNS — шляхом тиску на особу з приватним ключем, якому він відповідає. + +Крім того, деякі пакети мають проблеми з IPFS, тому, якщо ваш вебсайт дуже складний, це може бути не найкращим рішенням. І, звичайно, все, що залежить від інтеграції з сервером, неможливо децентралізувати, просто розмістивши клієнтську частину на IPFS. + +## Висновок {#conclusion} + +Так само, як Ethereum дозволяє вам децентралізувати аспекти бази даних та бізнес-логіки вашого dapp, IPFS дозволяє децентралізувати інтерфейс користувача. Це дозволяє вам закрити ще один вектор атаки проти вашого dapp. + +[Більше моїх робіт дивіться тут](https://cryptodocguy.pro/). diff --git a/public/content/translations/uk/developers/tutorials/kickstart-your-dapp-frontend-development-with-create-eth-app/index.md b/public/content/translations/uk/developers/tutorials/kickstart-your-dapp-frontend-development-with-create-eth-app/index.md new file mode 100644 index 00000000000..edee7ca80ae --- /dev/null +++ b/public/content/translations/uk/developers/tutorials/kickstart-your-dapp-frontend-development-with-create-eth-app/index.md @@ -0,0 +1,111 @@ +--- +title: "Швидко розпочніть розробку фронтенду для вашого dapp за допомогою create-eth-app" +description: "Огляд використання create-eth-app та його можливостей" +author: "Markus Waas" +tags: + [ + "використання", + "javaScript", + "ethers.js", + "the graph", + "defi" + ] +skill: beginner +lang: uk +published: 2020-04-27 +source: soliditydeveloper.com +sourceUrl: https://soliditydeveloper.com/create-eth-app +--- + +Минулого разу ми розглядали [загальну картину Solidity](https://soliditydeveloper.com/solidity-overview-2020) і вже згадували про [create-eth-app](https://github.com/PaulRBerg/create-eth-app). Тепер ви дізнаєтеся, як його використовувати, які функції інтегровано та додаткові ідеї щодо його розширення. Цей застосунок, створений Полом Разваном Бергом, засновником [Sablier](http://sablier.com/), допоможе вам швидко розпочати розробку фронтенду та пропонує кілька додаткових інтеграцій на вибір. + +## Встановлення {#installation} + +Для встановлення потрібен Yarn версії 0.25 або вище (`npm install yarn --global`). Це так само просто, як виконати: + +```bash +yarn create eth-app my-eth-app +cd my-eth-app +yarn react-app:start +``` + +Він використовує [create-react-app](https://github.com/facebook/create-react-app) «під капотом». Щоб переглянути свій застосунок, відкрийте `http://localhost:3000/`. Коли ви будете готові розгорнути застосунок для використання, створіть мініфікований пакет за допомогою yarn build. Один із простих способів розмістити його — це [Netlify](https://www.netlify.com/). Ви можете створити репозиторій на GitHub, додати його в Netlify, налаштувати команду збірки, і все готово! Ваш застосунок буде розміщено в мережі та доступно для всіх. І все це безкоштовно. + +## Можливості {#features} + +### React і create-react-app {#react--create-react-app} + +Перш за все, серце застосунку: React і всі додаткові можливості, що надаються _create-react-app_. Використання лише цього є чудовим варіантом, якщо ви не хочете інтегрувати Ethereum. Сам [React](https://react.dev/) значно спрощує створення інтерактивних інтерфейсів користувача. Можливо, він не такий зручний для початківців, як [Vue](https://vuejs.org/), але він все ще є найпоширенішим, має більше можливостей і, що найважливіше, тисячі додаткових бібліотек на вибір. _create-react-app_ також значно спрощує початок роботи та включає: + +- Підтримка синтаксису React, JSX, ES6, TypeScript, Flow. +- Додаткові мовні можливості, крім ES6, як-от оператор розширення об’єкта. +- Автоматичне додавання префіксів у CSS, тому вам не потрібні `-webkit-` або інші префікси. +- Швидкий інтерактивний засіб запуску модульних тестів із вбудованою підтримкою звітів про покриття. +- Сервер для розробки в реальному часі, який попереджає про поширені помилки. +- Скрипт збірки для об’єднання JS, CSS та зображень для робочого середовища з хешами та картами джерел. + +Зокрема, _create-eth-app_ використовує нові [ефекти хуків (hooks effects)](https://legacy.reactjs.org/docs/hooks-effect.html). Це метод для написання потужних, але дуже малих так званих функціональних компонентів. Дивіться розділ про Apollo нижче, щоб дізнатися, як вони використовуються в _create-eth-app_. + +### Yarn Workspaces {#yarn-workspaces} + +[Yarn Workspaces](https://classic.yarnpkg.com/en/docs/workspaces/) дозволяють мати кілька пакетів, але керувати ними всіма з кореневої папки та встановлювати залежності для всіх одночасно за допомогою `yarn install`. Це особливо доцільно для невеликих додаткових пакетів, таких як керування адресами/ABI смарт-контрактів (інформація про те, де і які смарт-контракти ви розгорнули та як з ними взаємодіяти) або інтеграція графа, які є частиною `create-eth-app`. + +### ethers.js {#ethersjs} + +Хоча [Web3](https://docs.web3js.org/) все ще використовується найчастіше, [ethers.js](https://docs.ethers.io/) за останній рік набув значно більшої популярності як альтернатива, і саме він інтегрований у _create-eth-app_. Ви можете працювати з ним, змінити його на Web3 або розглянути можливість оновлення до [ethers.js v5](https://docs.ethers.org/v5/), який вже майже вийшов з бета-версії. + +### The Graph {#the-graph} + +[GraphQL](https://graphql.org/) — це альтернативний спосіб обробки даних у порівнянні з [RESTful API](https://restfulapi.net/). Вони мають кілька переваг над RESTful API, особливо для децентралізованих даних блокчейну. Якщо вас цікавлять причини, що стоять за цим, ознайомтеся зі статтею [GraphQL Will Power the Decentralized Web](https://medium.com/graphprotocol/graphql-will-power-the-decentralized-web-d7443a69c69a). + +Зазвичай дані отримують безпосередньо з вашого смарт-контракту. Хочете дізнатися час останньої угоди? Просто викличте `MyContract.methods.latestTradeTime().call()`, який отримує дані з вузла Ethereum у ваш dapp. Але що, якщо вам потрібні сотні різних точок даних? Це призведе до сотень запитів даних до вузла, кожен з яких вимагатиме [RTT](https://wikipedia.org/wiki/Round-trip_delay_time), що зробить ваш dapp повільним і неефективним. Одним з обхідних шляхів може бути функція виклику засобу отримання даних у вашому контракті, яка повертає кілька даних одночасно. Однак це не завжди ідеальний варіант. + +Крім того, вас можуть зацікавити історичні дані. Ви хочете знати не тільки час останньої угоди, а й час усіх угод, які ви коли-небудь укладали. Використовуйте пакет підграфа _create-eth-app_, прочитайте [документацію](https://thegraph.com/docs/en/subgraphs/developing/creating/starting-your-subgraph) та адаптуйте його до власних контрактів. Якщо ви шукаєте популярні смарт-контракти, для них уже може існувати підграф. Перегляньте [провідник підграфів (subgraph explorer)](https://thegraph.com/explorer/). + +Щойно у вас з’явиться підграф, ви зможете написати у своєму dapp один простий запит, який отримує всі важливі дані блокчейну, включно з потрібними вам історичними даними, і для цього знадобиться лише один запит. + +### Apollo {#apollo} + +Завдяки інтеграції [Apollo Boost](https://www.apollographql.com/docs/react/get-started/) ви можете легко інтегрувати граф у свій React dapp. Особливо при використанні [хуків React та Apollo](https://www.apollographql.com/blog/apollo-client-now-with-react-hooks) отримання даних стає таким же простим, як написання одного запиту GraphQL у вашому компоненті: + +```js +const { loading, error, data } = useQuery(myGraphQlQuery) + +React.useEffect(() => { + if (!loading && !error && data) { + console.log({ data }) + } +}, [loading, error, data]) +``` + +## Шаблони {#templates} + +Крім того, ви можете вибрати один із кількох різних шаблонів. Наразі ви можете використовувати інтеграцію з Aave, Compound, UniSwap або sablier. Усі вони додають важливі адреси сервісних смарт-контрактів разом із готовими інтеграціями підграфів. Просто додайте шаблон до команди створення, наприклад: `yarn create eth-app my-eth-app --with-template aave`. + +### Aave {#aave} + +[Aave](https://aave.com/) — це децентралізований ринок грошового кредитування. Вкладники надають ринку ліквідність, щоб отримувати пасивний дохід, а позичальники можуть позичати кошти під заставу. Однією з унікальних особливостей Aave є [миттєві позики (flash loans)](https://aave.com/docs/developers/flash-loans), які дозволяють позичати гроші без будь-якої застави, за умови, що ви повернете позику в межах однієї транзакції. Це може бути корисно, наприклад, для отримання додаткових грошей для арбітражної торгівлі. + +Торговані токени, які приносять вам відсотки, називаються _aTokens_. + +Якщо ви вирішите інтегрувати Aave з _create-eth-app_, ви отримаєте [інтеграцію підграфа](https://docs.aave.com/developers/getting-started/using-graphql). Aave використовує The Graph і вже надає вам кілька готових до використання підграфів у мережах [Ropsten](https://thegraph.com/explorer/subgraph/aave/protocol-ropsten) і [Mainnet](https://thegraph.com/explorer/subgraph/aave/protocol) у [необробленому (raw)](https://thegraph.com/explorer/subgraph/aave/protocol-raw) або [відформатованому (formatted)](https://thegraph.com/explorer/subgraph/aave/protocol) вигляді. + +![Мем про миттєву позику Aave – \"Так, якби я міг утримати свою миттєву позику довше, ніж на 1 транзакцію, це було б чудово\"](./flashloan-meme.png) + +### Compound {#compound} + +[Compound](https://compound.finance/) схожий на Aave. Інтеграція вже включає новий [підграф Compound v2 (Compound v2 Subgraph)](https://medium.com/graphprotocol/https-medium-com-graphprotocol-compound-v2-subgraph-highlight-a5f38f094195). Токени, що приносять відсотки, тут, як не дивно, називаються _cTokens_. + +### Uniswap {#uniswap} + +[Uniswap](https://uniswap.exchange/) — це децентралізована біржа (DEX). Постачальники ліквідності можуть заробляти комісійні, надаючи необхідні токени або етер для обох сторін угоди. Він широко використовується і тому має одну з найвищих ліквідностей для дуже широкого спектра токенів. Ви можете легко інтегрувати його у свій dapp, щоб, наприклад, дозволити користувачам обмінювати свої ETH на DAI. + +На жаль, на момент написання статті інтеграція доступна лише для Uniswap v1, а не для [щойно випущеної v2](https://uniswap.org/blog/uniswap-v2/). + +### Sablier {#sablier} + +[Sablier](https://sablier.com/) дозволяє користувачам здійснювати потокові грошові платежі. Замість одноразової виплати, ви фактично отримуєте гроші постійно без подальшого адміністрування після початкового налаштування. Інтеграція включає його [власний підграф](https://thegraph.com/explorer/subgraph/sablierhq/sablier). + +## Що далі? {#whats-next} + +Якщо у вас є запитання щодо _create-eth-app_, перейдіть на [сервер спільноти Sablier](https://discord.gg/bsS8T47), де ви зможете зв'язатися з авторами _create-eth-app_. Як перші наступні кроки, ви можете інтегрувати фреймворк інтерфейсу користувача, наприклад [Material UI](https://mui.com/material-ui/), написати запити GraphQL для даних, які вам дійсно потрібні, і налаштувати розгортання. diff --git a/public/content/translations/uk/developers/tutorials/learn-foundational-ethereum-topics-with-sql/index.md b/public/content/translations/uk/developers/tutorials/learn-foundational-ethereum-topics-with-sql/index.md new file mode 100644 index 00000000000..6c40e2acc71 --- /dev/null +++ b/public/content/translations/uk/developers/tutorials/learn-foundational-ethereum-topics-with-sql/index.md @@ -0,0 +1,269 @@ +--- +title: "Вивчайте основні теми Ethereum за допомогою SQL" +description: "Це керівництво допоможе читачам зрозуміти основні концепції Ethereum, включаючи транзакції, блоки та газ, шляхом запиту даних у ланцюжку за допомогою мови структурованих запитів (SQL)." +author: "Paul Apivat" +tags: [ "SQL", "Запити", "Транзакції" ] +skill: beginner +lang: uk +published: 2021-05-11 +source: paulapivat.com +sourceUrl: https://paulapivat.com/post/query_ethereum/ +--- + +Багато посібників з Ethereum орієнтовані на розробників, але бракує освітніх ресурсів для аналітиків даних або для людей, які бажають бачити дані в ланцюжку, не запускаючи клієнт або вузол. + +Цей посібник допоможе читачам зрозуміти основні концепції Ethereum, включно із транзакціями, блоками та газом, шляхом надсилання запитів до даних у ланцюжку за допомогою мови структурованих запитів (SQL) через інтерфейс, наданий [Dune Analytics](https://dune.com/). + +Дані в ланцюжку можуть допомогти нам зрозуміти Ethereum, мережу, а також економіку обчислювальної потужності та мають слугувати основою для розуміння проблем, з якими стикається Ethereum сьогодні (тобто зростанням цін на газ), і, що більш важливо, для обговорення рішень із масштабування. + +### Транзакції {#transactions} + +Шлях користувача в Ethereum починається з ініціалізації контрольованого користувачем облікового запису або сутності з балансом в ETH. Існує два типи облікових записів: контрольовані користувачем або смарт-контрактом (див. [ethereum.org](/developers/docs/accounts/)). + +Будь-який обліковий запис можна переглянути в оглядачі блоків, наприклад [Etherscan](https://etherscan.io/) або [Blockscout](https://eth.blockscout.com/). Оглядачі блоків — це портал до даних Ethereum. Вони відображають у режимі реального часу дані про блоки, транзакції, майнерів, облікові записи та іншу активність у ланцюжку (див. [тут](/developers/docs/data-and-analytics/block-explorers/)). + +Однак користувач може забажати надіслати запит безпосередньо до даних, щоб звірити інформацію, надану зовнішніми оглядачами блоків. [Dune Analytics](https://dune.com/) надає таку можливість усім, хто має певні знання SQL. + +Для довідки: обліковий запис смарт-контракту Ethereum Foundation (EF) можна переглянути на [Blockscout](https://eth.blockscout.com/address/0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe). + +Слід зазначити, що всі облікові записи, включно з EF, мають публічну адресу, яку можна використовувати для надсилання та отримання транзакцій. + +Баланс облікового запису на Etherscan складається зі звичайних і внутрішніх транзакцій. Внутрішні транзакції, попри назву, не є _фактичними_ транзакціями, які змінюють стан ланцюжка. Це перекази коштів, ініційовані виконанням контракту ([джерело](https://ethereum.stackexchange.com/questions/3417/how-to-get-contract-internal-transactions)). Оскільки внутрішні транзакції не мають підпису, вони **не** включаються в блокчейн і до них не можна надіслати запит за допомогою Dune Analytics. + +Тому цей посібник буде зосереджений на звичайних транзакціях. Запит до них можна надіслати таким чином: + +```sql +WITH temp_table AS ( +SELECT + hash, + block_number, + block_time, + "from", + "to", + value / 1e18 AS ether, + gas_used, + gas_price / 1e9 AS gas_price_gwei +FROM ethereum."transactions" +WHERE "to" = '\xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe' +ORDER BY block_time DESC +) +SELECT + hash, + block_number, + block_time, + "from", + "to", + ether, + (gas_used * gas_price_gwei) / 1e9 AS txn_fee +FROM temp_table +``` + +Це дасть ту саму інформацію, що й на сторінці транзакцій Etherscan. Для порівняння, ось два джерела: + +#### Etherscan {#etherscan} + +![](./etherscan_view.png) + +[Сторінка контракту EF на Blockscout.](https://eth.blockscout.com/address/0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe) + +#### Dune Analytics {#dune-analytics} + +![](./dune_view.png) + +Ви можете знайти інформаційну панель [тут](https://dune.com/paulapivat/Learn-Ethereum). Натисніть на таблицю, щоб переглянути запит (також див. вище). + +### Аналіз транзакцій {#breaking_down_transactions} + +Надіслана транзакція включає кілька частин інформації, зокрема ([джерело](/developers/docs/transactions/)): + +- **Одержувач**: адреса отримання (у запиті як "to") +- **Підпис**: хоча транзакція підписується приватними ключами відправника, за допомогою SQL ми можемо зробити запит до публічної адреси відправника ("from"). +- **Значення**: це сума переказаних ETH (див. стовпець `ether`). +- **Дані**: це довільні дані, які були хешовані (див. стовпець `data`) +- **gasLimit** – максимальна кількість одиниць газу, яку може спожити транзакція. Одиниці газу представляють обчислювальні кроки +- **maxPriorityFeePerGas** – максимальна сума газу, що буде включена як «чайові» для майнера +- **maxFeePerGas** – максимальна сума газу, яку ви готові заплатити за транзакцію (включно з baseFeePerGas і maxPriorityFeePerGas) + +Ми можемо надіслати запит до цих конкретних частин інформації для транзакцій на публічну адресу Ethereum Foundation: + +```sql +SELECT + "to", + "from", + value / 1e18 AS ether, + data, + gas_limit, + gas_price / 1e9 AS gas_price_gwei, + gas_used, + ROUND(((gas_used / gas_limit) * 100),2) AS gas_used_pct +FROM ethereum."transactions" +WHERE "to" = '\xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe' +ORDER BY block_time DESC +``` + +### Блоки {#blocks} + +Кожна транзакція змінює стан віртуальної машини Ethereum ([EVM](/developers/docs/evm/)) ([джерело](/developers/docs/transactions/)). Транзакції транслюються в мережу для перевірки та включення в блок. Кожна транзакція пов’язана з номером блоку. Щоб переглянути дані, ми можемо надіслати запит до певного номера блоку: 12396854 (останній блок серед транзакцій Ethereum Foundation на момент написання, 11.05.21). + +Крім того, коли ми надсилаємо запит до наступних двох блоків, ми можемо побачити, що кожен блок містить хеш попереднього блоку (тобто батьківський хеш), що ілюструє, як формується блокчейн. + +Кожен блок містить посилання на свій батьківський блок. Це показано нижче між стовпцями `hash` і `parent_hash` ([джерело](/developers/docs/blocks/)): + +![parent_hash](./parent_hash.png) + +Ось [запит](https://dune.com/queries/44856/88292) на Dune Analytics: + +```sql +SELECT + time, + number, + hash, + parent_hash, + nonce +FROM ethereum."blocks" +WHERE "number" = 12396854 OR "number" = 12396855 OR "number" = 12396856 +LIMIT 10 +``` + +Ми можемо дослідити блок, надіславши запит щодо часу, номера блоку, складності, хешу, батьківського хешу та одноразового номера. + +Єдине, що не охоплює цей запит, – це _список транзакцій_, для якого потрібен окремий запит нижче, і _корінь стану_. Повний або архівний вузол зберігатиме всі транзакції та переходи станів, дозволяючи клієнтам надсилати запити щодо стану ланцюжка в будь-який час. Оскільки для цього потрібен великий обсяг пам’яті, ми можемо відокремити дані ланцюжка від даних стану: + +- Дані ланцюжка (список блоків, транзакцій) +- Дані стану (результат переходу стану кожної транзакції) + +Корінь стану належить до останнього й є _неявними_ даними (не зберігаються в ланцюжку), тоді як дані ланцюжка є явними та зберігаються в самому ланцюжку ([джерело](https://ethereum.stackexchange.com/questions/359/where-is-the-state-data-stored)). + +У цьому посібнику ми зосередимося на даних у ланцюжку, до яких _можна_ надіслати запит за допомогою SQL через Dune Analytics. + +Як зазначено вище, кожен блок містить список транзакцій; ми можемо надіслати запит, відфільтрувавши його за певним блоком. Спробуємо останній блок, 12396854: + +```sql +SELECT * FROM ethereum."transactions" +WHERE block_number = 12396854 +ORDER BY block_time DESC` +``` + +Ось результат SQL-запиту на Dune: + +![](./list_of_txn.png) + +Цей єдиний блок, що додається до ланцюжка, змінює стан віртуальної машини Ethereum ([EVM](/developers/docs/evm/)). Десятки, а іноді й сотні транзакцій перевіряються одночасно. У цьому конкретному випадку було включено 222 транзакції. + +Щоб побачити, скільки з них були насправді успішними, ми додамо ще один фільтр для підрахунку успішних транзакцій: + +```sql +WITH temp_table AS ( + SELECT * FROM ethereum."transactions" + WHERE block_number = 12396854 AND success = true + ORDER BY block_time DESC +) +SELECT + COUNT(success) AS num_successful_txn +FROM temp_table +``` + +Для блоку 12396854 із 222 транзакцій 204 було успішно перевірено: + +![](./successful_txn.png) + +Запити на транзакції відбуваються десятки разів на секунду, але блоки додаються приблизно раз на 15 секунд ([джерело](/developers/docs/blocks/)). + +Щоб переконатися, що один блок створюється приблизно кожні 15 секунд, ми можемо взяти кількість секунд на добу (86400), розділити її на 15, щоб отримати орієнтовну середню кількість блоків на день (~5760). + +Діаграма блоків Ethereum, що створюються за день (з 2016 року дотепер): + +![](./daily_blocks.png) + +Середня кількість блоків, що створюються щодня за цей період, становить ~5874: + +![](./avg_daily_blocks.png) + +Запити: + +```sql +# запит для візуалізації кількості блоків, що створюються щодня з 2016 року + +SELECT + DATE_TRUNC('day', time) AS dt, + COUNT(*) AS block_count +FROM ethereum."blocks" +GROUP BY dt +OFFSET 1 + +# середня кількість блоків, що створюються за день + +WITH temp_table AS ( +SELECT + DATE_TRUNC('day', time) AS dt, + COUNT(*) AS block_count +FROM ethereum."blocks" +GROUP BY dt +OFFSET 1 +) +SELECT + AVG(block_count) AS avg_block_count +FROM temp_table +``` + +Середня кількість блоків, що створюються на день із 2016 року, трохи перевищує це число і становить 5874. Крім того, якщо розділити 86400 секунд на 5874 середніх блоків, вийде 14,7 секунди, або приблизно один блок кожні 15 секунд. + +### Газ {#gas} + +Блоки обмежені за розміром. Максимальний розмір блоку є динамічним і змінюється залежно від попиту в мережі від 12 500 000 до 25 000 000 одиниць. Ліміти необхідні, щоб запобігти довільно великим розмірам блоків, які створюють навантаження на повні вузли з погляду дискового простору та вимог до швидкості ([джерело](/developers/docs/blocks/)). + +Один зі способів концептуалізувати ліміт газу для блоку — це уявити його як **пропозицію** доступного простору блоку для пакетування транзакцій. Ліміт газу для блоку можна запитати та візуалізувати з 2016 року до сьогодні: + +![](./avg_gas_limit.png) + +```sql +SELECT + DATE_TRUNC('day', time) AS dt, + AVG(gas_limit) AS avg_block_gas_limit +FROM ethereum."blocks" +GROUP BY dt +OFFSET 1 +``` + +Далі йде фактичний обсяг газу, що використовується щодня для оплати обчислень у ланцюжку Ethereum (тобто надсилання транзакції, виклик смарт-контракту, карбування NFT). Це **попит** на доступний простір блоку Ethereum: + +![](./daily_gas_used.png) + +```sql +SELECT + DATE_TRUNC('day', time) AS dt, + AVG(gas_used) AS avg_block_gas_used +FROM ethereum."blocks" +GROUP BY dt +OFFSET 1 +``` + +Ми також можемо зіставити ці дві діаграми, щоб побачити, як співвідносяться **попит і пропозиція**: + +![gas_demand_supply](./gas_demand_supply.png) + +Тому ми можемо розуміти ціни на газ як функцію попиту на простір блоку Ethereum за наявної пропозиції. + +Нарешті, ми можемо захотіти запитати середньодобові ціни на газ для ланцюжка Ethereum, однак це призведе до особливо тривалого часу виконання запиту, тому ми відфільтруємо наш запит до середньої кількості газу, сплаченої за транзакцію Ethereum Foundation. + +![](./ef_daily_gas.png) + +Ми можемо бачити ціни на газ, сплачені за всі транзакції, здійснені на адресу Ethereum Foundation протягом багатьох років. Ось запит: + +```sql +SELECT + block_time, + gas_price / 1e9 AS gas_price_gwei, + value / 1e18 AS eth_sent +FROM ethereum."transactions" +WHERE "to" = '\xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe' +ORDER BY block_time DESC +``` + +### Підсумок {#summary} + +За допомогою цього посібника ми розібралися з основними концепціями Ethereum і тим, як працює блокчейн Ethereum, надсилаючи запити та знайомлячись із даними в ланцюжку. + +Інформаційна панель, яка містить весь код, використаний у цьому посібнику, доступна [тут](https://dune.com/paulapivat/Learn-Ethereum). + +Щоб дізнатися більше про використання даних для дослідження web3, [знайдіть мене у Twitter](https://twitter.com/paulapivat). diff --git a/public/content/translations/uk/developers/tutorials/logging-events-smart-contracts/index.md b/public/content/translations/uk/developers/tutorials/logging-events-smart-contracts/index.md new file mode 100644 index 00000000000..5026da1c3c4 --- /dev/null +++ b/public/content/translations/uk/developers/tutorials/logging-events-smart-contracts/index.md @@ -0,0 +1,68 @@ +--- +title: "Реєстрація даних зі смарт-контрактів за допомогою подій" +description: "Вступ до подій у смарт-контрактах та як їх можна використовувати для реєстрації даних" +author: "jdourlens" +tags: + [ + "Смарт-контракти", + "remix", + "мова програмування", + "події" + ] +skill: intermediate +lang: uk +published: 2020-04-03 +source: EthereumDev +sourceUrl: https://ethereumdev.io/logging-data-with-events/ +address: "0x19dE91Af973F404EDF5B4c093983a7c6E3EC8ccE" +--- + +У Solidity [події](/developers/docs/smart-contracts/anatomy/#events-and-logs) — це сигнали, які можуть надсилати смарт-контракти. Dapps або будь-що, пов’язане з Ethereum JSON-RPC API, може прослуховувати ці події та діяти відповідно. Подію також можна проіндексувати, щоб пізніше можна було шукати історію подій. + +## Події {#events} + +Найпоширенішою подією в блокчейні Ethereum на момент написання цієї статті є подія Transfer, яка генерується токенами ERC20, коли хтось передає токени. + +```solidity +event Transfer(address indexed from, address indexed to, uint256 value); +``` + +Сигнатура події оголошується всередині коду контракту та може бути згенерована за допомогою ключового слова `emit`. Наприклад, подія переказу реєструє, хто надіслав переказ (_from_), кому (_to_) і скільки токенів було переказано (_value_). + +Якщо ми повернемося до нашого смарт-контракту Counter і вирішимо реєструвати щоразу, коли значення змінюється. Оскільки цей контракт не призначений для розгортання, а слугує основою для створення іншого контракту шляхом його розширення, він називається абстрактним контрактом. У нашому прикладі з Counter це матиме такий вигляд: + +```solidity +pragma solidity 0.5.17; + +contract Counter { + + event ValueChanged(uint oldValue, uint256 newValue); + + // Приватна змінна типу беззнакового цілого числа для збереження кількості підрахунків + uint256 private count = 0; + + // Функція, яка збільшує наш лічильник + function increment() public { + count += 1; + emit ValueChanged(count - 1, count); + } + + // Гетер для отримання значення лічильника + function getCount() public view returns (uint256) { + return count; + } + +} +``` + +Зверніть увагу: + +- **Рядок 5**: ми оголошуємо нашу подію та її вміст: старе й нове значення. + +- **Рядок 13**: коли ми збільшуємо нашу змінну count, ми генеруємо подію. + +Якщо ми зараз розгорнемо контракт і викличемо функцію `increment`, то побачимо, що Remix автоматично відобразить подію, якщо натиснути на нову транзакцію в масиві з назвою `logs`. + +![Знімок екрана Remix](./remix-screenshot.png) + +Логи дуже корисні для налагодження ваших смарт-контрактів, але вони також важливі, якщо ви створюєте застосунки для різних користувачів. Вони полегшують аналітику, відстеження та розуміння того, як використовується ваш смарт-контракт. Логи, що генеруються транзакціями, відображаються в популярних оглядачах блоків. Ви також можете, наприклад, використовувати їх для створення offchain-скриптів, які прослуховують певні події та реагують на них. diff --git a/public/content/translations/uk/developers/tutorials/merkle-proofs-for-offline-data-integrity/index.md b/public/content/translations/uk/developers/tutorials/merkle-proofs-for-offline-data-integrity/index.md new file mode 100644 index 00000000000..9e261e7b210 --- /dev/null +++ b/public/content/translations/uk/developers/tutorials/merkle-proofs-for-offline-data-integrity/index.md @@ -0,0 +1,250 @@ +--- +title: "Докази Меркла для цілісності даних в автономному режимі" +description: "Забезпечення цілісності даних ончейн для даних, які здебільшого зберігаються офчейн" +author: Ori Pomerantz +tags: [ "сховище" ] +skill: advanced +lang: uk +published: 2021-12-30 +--- + +## Вступ {#introduction} + +В ідеалі ми хотіли б зберігати все в сховищі Ethereum, яке зберігається на тисячах комп’ютерів і має +надзвичайно високу доступність (дані не можуть бути піддані цензурі) і цілісність (дані не можуть бути змінені +несанкціонованим способом), але зберігання 32-байтового слова зазвичай коштує 20 000 газу. Поки я це пишу, ця вартість +еквівалентна $6,60. При 21 центі за байт це занадто дорого для багатьох застосувань. + +Щоб розв'язати цю проблему, екосистема Ethereum розробила [багато альтернативних способів зберігання даних у децентралізований спосіб](/developers/docs/storage/). Зазвичай вони передбачають компроміс між доступністю та ціною. Проте, як правило, цілісність гарантована. + +У цій статті ви дізнаєтеся, **як** забезпечити цілісність даних, не зберігаючи їх на блокчейні, за допомогою +[доказів Меркла](https://computersciencewiki.org/index.php/Merkle_proof). + +## Як це працює? {#how-does-it-work} + +Теоретично ми могли б просто зберігати хеш даних ончейн і надсилати всі дані в транзакціях, які їх потребують. Хоча, це все ще занадто дорого. Байт даних для транзакції коштує близько 16 газів, наразі близько півцента, або близько 5 доларів за кілобайт. При 5000 доларів за мегабайт це все ще занадто дорого для багатьох застосувань, навіть без додаткових витрат на хешування даних. + +Рішення полягає в тому, щоб багаторазово хешувати різні підмножини даних, тому для даних, які вам не потрібно надсилати, ви можете просто надіслати хеш. Ви робите це за допомогою дерева Меркла, структуру даних дерева, де кожен вузол є хешем вузлів під ним: + +![Дерево Меркла](tree.png) + +Кореневий хеш — це єдина частина, яку потрібно зберігати ончейн. Щоб підтвердити певне значення, ви надаєте всі хеші, які потрібно об’єднати з ним для отримання кореня. Наприклад, щоб довести `C`, ви надаєте `D`, `H(A-B)` і `H(E-H)`. + +![Доказ значення C](proof-c.png) + +## Реалізація {#implementation} + +[Приклад коду наведено тут](https://github.com/qbzzt/merkle-proofs-for-offline-data-integrity). + +### Офчейн-код {#offchain-code} + +У цій статті ми використовуємо JavaScript для офчейн-обчислень. Більшість децентралізованих застосунків мають свій офчейн-компонент, написаний на JavaScript. + +#### Створення кореня Меркла {#creating-the-merkle-root} + +Спочатку нам потрібно надати ланцюжку корінь Меркла. + +```javascript +const ethers = require("ethers") +``` + +[Ми використовуємо хеш-функцію з пакета ethers](https://docs.ethers.io/v5/api/utils/hashing/#utils-keccak256). + +```javascript +// Необроблені дані, цілісність яких ми маємо перевірити. Перші два байти +// є ідентифікатором користувача, а останні два байти — це кількість токенів, якими +// користувач володіє на цей час. +const dataArray = [ + 0x0bad0010, 0x60a70020, 0xbeef0030, 0xdead0040, 0xca110050, 0x0e660060, + 0xface0070, 0xbad00080, 0x060d0091, +] +``` + +Кодування кожного запису в одне 256-бітове ціле число призводить до менш читабельного коду, ніж, наприклад, використання JSON. Однак це означає значно меншу обробку для отримання даних у контракті, тому витрати на газ значно нижчі. [JSON можна читати ончейн](https://github.com/chrisdotn/jsmnSol), але це погана ідея, якщо цього можна уникнути. + +```javascript +// Масив хеш-значень у вигляді BigInts +const hashArray = dataArray +``` + +У цьому випадку наші дані початково є 256-бітними значеннями, тому обробка не потрібна. Якщо ми використовуємо складнішу структуру даних, наприклад рядки, нам потрібно переконатися, що ми спочатку хешуємо дані, щоб отримати масив хешів. Зауважте, що це також тому, що нам байдуже, чи знають користувачі інформацію інших користувачів. Інакше нам довелося б хешувати, щоб користувач 1 не знав значення для користувача 0, користувач 2 не знав би значення для користувача 3 тощо. + +```javascript +// Перетворення між рядком, який очікує хеш-функція, та +// BigInt, який ми використовуємо в інших місцях. +const hash = (x) => + BigInt(ethers.utils.keccak256("0x" + x.toString(16).padStart(64, 0))) +``` + +Хеш-функція ethers очікує на отримання рядка JavaScript з шістнадцятковим числом, таким як `0x60A7`, і відповідає іншим рядком з такою ж структурою. Однак для решти коду простіше використовувати `BigInt`, тому ми перетворюємо на шістнадцятковий рядок і назад. + +```javascript +// Симетричний хеш пари, тому нам неважливо, якщо порядок буде змінено. +const pairHash = (a, b) => hash(hash(a) ^ hash(b)) +``` + +Ця функція симетрична (хеш від `a` [xor](https://en.wikipedia.org/wiki/Exclusive_or) `b`). Це означає, що коли ми перевіряємо доказ Меркла, нам не потрібно турбуватися про те, чи слід помістити значення з доказу до чи після обчисленого значення. Перевірка доказів Меркла виконується ончейн, тому чим менше нам потрібно там робити, тим краще. + +Увага: +Криптографія складніша, ніж здається. +Початкова версія цієї статті мала хеш-функцію `hash(a^b)`. +Це була **погана** ідея, оскільки це означало, що якби ви знали легітимні значення `a` та `b`, ви могли б використати `b' = a^b^a'`, щоб довести будь-яке бажане значення `a'`. +З цією функцією вам доведеться обчислювати `b'` так, щоб `hash(a') ^ hash(b')` дорівнювало відомому значенню (наступній гілці на шляху до кореня), що набагато складніше. + +```javascript +// Значення, яке позначає, що певна гілка порожня, не +// має значення +const empty = 0n +``` + +Коли кількість значень не є цілочисельністю від двох, нам потрібно обробляти порожні гілки. Ця програма робить це таким чином, щоб поставити нуль як заповнювач місця. + +![Дерево Меркла з відсутніми гілками](merkle-empty-hash.png) + +```javascript +// Обчислюємо наступний рівень дерева масиву хешів, беручи хеш +// кожної пари послідовно +const oneLevelUp = (inputArray) => { + var result = [] + var inp = [...inputArray] // Щоб уникнути перезапису вхідних даних // Додаємо порожнє значення, якщо необхідно (нам потрібно, щоб усі листки були // спарені) + + if (inp.length % 2 === 1) inp.push(empty) + + for (var i = 0; i < inp.length; i += 2) + result.push(pairHash(inp[i], inp[i + 1])) + + return result +} // oneLevelUp +``` + +Ця функція «підіймається» на один рівень у дереві Меркла, хешуючи пари значень на поточному рівні. Зауважте, що це не найефективніша реалізація. Ми могли б уникнути копіювання вхідних даних і просто додати `empty`, коли це доречно в циклі, але цей код оптимізовано для читабельності. + +```javascript +const getMerkleRoot = (inputArray) => { + var result + + result = [...inputArray] // Піднімаємося по дереву, поки не залишиться одне значення, тобто // корінь. // // Якщо рівень має непарну кількість записів, // код у oneLevelUp додає порожнє значення, тому, якщо ми маємо, наприклад, // 10 листків, у нас буде 5 гілок на другому рівні, 3 // гілки на третьому, 2 на четвертому, а корінь — п'ятий + + while (result.length > 1) result = oneLevelUp(result) + + return result[0] +} +``` + +Щоб отримати корінь, піднімайтеся, поки не залишиться лише одне значення. + +#### Створення доказу Меркла {#creating-a-merkle-proof} + +Доказ Меркла — це значення для хешування разом із доведеним значенням, щоб отримати корінь Меркла. Значення для доведення часто доступне з інших даних, тому я вважаю за краще надавати його окремо, а не як частину коду. + +```javascript +// Доказ Меркла складається зі списку значень елементів для +// хешування. Оскільки ми використовуємо симетричну хеш-функцію, нам не +// потрібно знати місцезнаходження елемента, щоб перевірити доказ, лише щоб його створити +const getMerkleProof = (inputArray, n) => { +    var result = [], currentLayer = [...inputArray], currentN = n + +    // Поки не досягнемо вершини +    while (currentLayer.length > 1) { +        // Немає рівнів непарної довжини +        if (currentLayer.length % 2) +            currentLayer.push(empty) + +        result.push(currentN % 2 +               // Якщо currentN непарне, додаємо до доказу значення, що йому передує +            ? currentLayer[currentN-1] +               // Якщо парне, додаємо наступне значення +            : currentLayer[currentN+1]) + +``` + +Ми хешуємо `(v[0],v[1])`, `(v[2],v[3])` і т. д. Отже, для парних значень нам потрібен наступний, для непарних – попередній. + +```javascript +        // Переходимо на наступний рівень вище +        currentN = Math.floor(currentN/2) +        currentLayer = oneLevelUp(currentLayer) +    }   // поки currentLayer.length > 1 + +    return result +}   // getMerkleProof +``` + +### Ончейн-код {#onchain-code} + +Нарешті ми маємо код, який перевіряє доказ. Ончейн-код написано на [Solidity](https://docs.soliditylang.org/en/v0.8.11/). Оптимізація тут набагато важливіша, оскільки газ відносно дорогий. + +```solidity +//SPDX-License-Identifier: Public Domain +pragma solidity ^0.8.0; + +import "hardhat/console.sol"; +``` + +Я написав це за допомогою [середовища розробки Hardhat](https://hardhat.org/), яке дозволяє нам отримувати [консольний вивід із Solidity](https://hardhat.org/docs/cookbook/debug-logs) під час розробки. + +```solidity + +contract MerkleProof { +    uint merkleRoot; + +    function getRoot() public view returns (uint) { +      return merkleRoot; +    } + +    // Надзвичайно небезпечно, у робочому коді доступ до +    // цієї функції ПОВИНЕН БУТИ суворо обмежений, ймовірно, лише +    // власником +    function setRoot(uint _merkleRoot) external { +      merkleRoot = _merkleRoot; +    }   // setRoot +``` + +Встановіть та отримайте функції для кореня Меркла. Дозволяти будь-кому оновлювати корінь Меркла — це _надзвичайно погана ідея_ в робочій системі. Я роблю це тут заради простоти зразка коду. **Не робіть цього в системі, де цілісність даних справді має значення**. + +```solidity +    function hash(uint _a) internal pure returns(uint) { +      return uint(keccak256(abi.encode(_a))); +    } + +    function pairHash(uint _a, uint _b) internal pure returns(uint) { +      return hash(hash(_a) ^ hash(_b)); +    } +``` + +Ця функція генерує парний хеш. Це просто переклад коду JavaScript для `hash` та `pairHash` на Solidity. + +**Примітка:** це ще один випадок оптимізації для читабельності. На основі [визначення функції](https://www.tutorialspoint.com/solidity/solidity_cryptographic_functions.htm), можливо, вдасться зберігати дані як значення [`bytes32`](https://docs.soliditylang.org/en/v0.5.3/types.html#fixed-size-byte-arrays) та уникнути перетворень. + +```solidity +    // Перевірка доказу Меркла +    function verifyProof(uint _value, uint[] calldata _proof) +        public view returns (bool) { +      uint temp = _value; +      uint i; + +      for(i=0; i<_proof.length; i++) { +        temp = pairHash(temp, _proof[i]); +      } + +      return temp == merkleRoot; +    } + +}  // MarkleProof +``` + +У математичному записі перевірка доказу Меркла виглядає так: `H(proof_n, H(proof_n-1, H(proof_n-2, ...` H(proof_1, H(proof_0, value))...)))`. Цей код реалізує його. + +## Докази Меркла та ролапи не поєднуються {#merkle-proofs-and-rollups} + +Докази Меркла погано працюють з [ролапами](/developers/docs/scaling/#rollups). Причина в тому, що rollups записують всі дані транзакції на L1, але обробляють на L2. Вартість відправки доказу Меркла з транзакцією в середньому становить 638 газів на шар (наразі байт даних виклику коштує 16 газів, якщо він не дорівнює нулю, і 4, якщо він дорівнює нулю). Якщо у нас є 1024 слова даних, доказ Меркла вимагає десяти шарів, або загалом 6380 газів. + +Наприклад, якщо подивитися на [Optimism](https://public-grafana.optimism.io/d/9hkhMxn7z/public-dashboard?orgId=1&refresh=5m), запис на L1 коштує близько 100 gwei газу, а на L2 — 0,001 gwei (це звичайна ціна, яка може зростати при перевантаженні мережі). Отже, на вартість одного газу L1 ми можемо витратити сто тисяч газу на перероблювання L2. Якщо припустити, що ми не перезаписуємо сховище, це означає, що ми можемо написати близько п’яти слів для зберігання на L2 за ціною одного газу L1. Для одного доказу Меркла ми можемо записати всі 1024 слова до сховища (якщо припустити, що їх можна обчислити ончейн, а не надавати в транзакції), і при цьому у нас залишиться більша частина газу. + +## Висновок {#conclusion} + +У реальному житті ви ніколи не зможете реалізувати дерева Меркла самостійно. Існують добре відомі та перевірені бібліотеки, які ви можете використовувати, і, загалом кажучи, краще не реалізовувати криптографічні примітиви самостійно. Але я сподіваюся, що тепер ви краще розумієте докази Меркла і зможете вирішити, коли їх варто використовувати. + +Зауважте, що хоча докази Меркла зберігають _цілісність_, вони не зберігають _доступність_. Знання того, що ніхто інший не може забрати ваші активи, є невеликою втіхою, якщо сховище даних вирішить заборонити доступ, ви також не зможете створити дерево Merkle для доступу до них. Тому дерева Меркла найкраще використовувати з якимось децентралізованим сховищем, таким як IPFS. + +[Більше моїх робіт дивіться тут](https://cryptodocguy.pro/). diff --git a/public/content/translations/uk/developers/tutorials/monitoring-geth-with-influxdb-and-grafana/index.md b/public/content/translations/uk/developers/tutorials/monitoring-geth-with-influxdb-and-grafana/index.md new file mode 100644 index 00000000000..ed16fad4fab --- /dev/null +++ b/public/content/translations/uk/developers/tutorials/monitoring-geth-with-influxdb-and-grafana/index.md @@ -0,0 +1,151 @@ +--- +title: "Моніторинг Geth за допомогою InfluxDB і Grafana" +description: "Налаштуйте моніторинг для вашого вузла Geth за допомогою InfluxDB і Grafana, щоб відстежувати продуктивність і виявляти проблеми." +author: "Mario Havel" +tags: [ "клієнти", "вузли" ] +skill: intermediate +lang: uk +published: 2021-01-13 +--- + +Цей посібник допоможе вам налаштувати моніторинг для вашого вузла Geth, щоб краще зрозуміти його продуктивність та визначити потенційні проблеми. + +## Передумови {#prerequisites} + +- Ви вже повинні мати запущений екземпляр Geth. +- Більшість кроків і прикладів призначені для середовища Linux, тому базові знання терміналу будуть корисними. +- Перегляньте цей відеоогляд набору метрик Geth: [Моніторинг інфраструктури Ethereum від Péter Szilágyi](https://www.youtube.com/watch?v=cOBab8IJMYI). + +## Стек моніторингу {#monitoring-stack} + +Клієнт Ethereum збирає багато даних, які можна прочитати у вигляді хронологічної бази даних. Щоб спростити моніторинг, ви можете передати ці дані в програмне забезпечення для візуалізації. Існує декілька доступних варіантів: + +- [Prometheus](https://prometheus.io/) (модель витягування) +- [InfluxDB](https://www.influxdata.com/get-influxdb/) (модель передавання) +- [Telegraf](https://www.influxdata.com/get-influxdb/) +- [Grafana](https://www.grafana.com/) +- [Datadog](https://www.datadoghq.com/) +- [Chronograf](https://www.influxdata.com/time-series-platform/chronograf/) + +Також існує [Geth Prometheus Exporter](https://github.com/hunterlong/gethexporter) — варіант, попередньо налаштований для роботи з InfluxDB і Grafana. + +У цьому посібнику ми налаштуємо ваш клієнт Geth для передавання даних в InfluxDB для створення бази даних і в Grafana для створення графічної візуалізації цих даних. Виконання цих кроків вручну допоможе вам краще зрозуміти процес, змінювати його та розгортати в різних середовищах. + +## Налаштування InfluxDB {#setting-up-influxdb} + +Спочатку давайте завантажимо та встановимо InfluxDB. Різні варіанти завантаження можна знайти на [сторінці випусків Influxdata](https://portal.influxdata.com/downloads/). Виберіть той, що підходить для вашого середовища. +Ви також можете встановити його з [репозиторію](https://repos.influxdata.com/). Наприклад, у дистрибутиві на основі Debian: + +``` +curl -tlsv1.3 --proto =https -sL https://repos.influxdata.com/influxdb.key | sudo apt-key add +source /etc/lsb-release +echo "deb https://repos.influxdata.com/${DISTRIB_ID,,} ${DISTRIB_CODENAME} stable" | sudo tee /etc/apt/sources.list.d/influxdb.list +sudo apt update +sudo apt install influxdb -y +sudo systemctl enable influxdb +sudo systemctl start influxdb +sudo apt install influxdb-client +``` + +Після успішного встановлення InfluxDB переконайтеся, що він працює у фоновому режимі. За замовчуванням він доступний за адресою `localhost:8086`. +Перш ніж використовувати клієнт `influx`, ви повинні створити нового користувача з правами адміністратора. Цей користувач буде слугувати для управління вищого рівня, створення баз даних і користувачів. + +``` +curl -XPOST "http://localhost:8086/query" --data-urlencode "q=CREATE USER username WITH PASSWORD 'password' WITH ALL PRIVILEGES" +``` + +Тепер за допомогою цього користувача ви можете увійти в [оболонку InfluxDB](https://docs.influxdata.com/influxdb/v1.8/tools/shell/), використовуючи клієнт `influx`. + +``` +influx -username 'username' -password 'password' +``` + +Безпосередньо взаємодіючи з InfluxDB в його оболонці, ви можете створити базу даних і користувача для метрик Geth. + +``` +create database geth +create user geth with password choosepassword +``` + +Перевірте створені записи за допомогою: + +``` +show databases +show users +``` + +Вийдіть з оболонки InfluxDB. + +``` +exit +``` + +InfluxDB працює і налаштований для зберігання метрик з Geth. + +## Підготовка Geth {#preparing-geth} + +Після налаштування бази даних, нам потрібно увімкнути збір метрик в Geth. Зверніть увагу на `METRICS AND STATS OPTIONS` в `geth --help`. Там можна знайти декілька варіантів. У нашому випадку нам потрібно, щоб Geth передавав дані в InfluxDB. +Базове налаштування визначає кінцеву точку, за якою доступний InfluxDB, та дані для автентифікації в базі даних. + +``` +geth --metrics --metrics.influxdb --metrics.influxdb.endpoint "http://0.0.0.0:8086" --metrics.influxdb.username "geth" --metrics.influxdb.password "chosenpassword" +``` + +Ці прапори можна додати до команди запуску клієнта або зберегти у файлі конфігурації. + +Ви можете перевірити, що Geth успішно передає дані, наприклад, переглянувши список метрик у базі даних. В оболонці InfluxDB: + +``` +use geth +show measurements +``` + +## Налаштування Grafana {#setting-up-grafana} + +Наступний крок — встановлення Grafana, яка буде інтерпретувати дані графічно. Дотримуйтесь інструкцій зі встановлення для вашого середовища в документації Grafana. Переконайтеся, що ви встановили версію OSS, якщо не хочете іншу. +Приклад кроків встановлення для дистрибутивів Debian з використанням репозиторію: + +``` +curl -tlsv1.3 --proto =https -sL https://packages.grafana.com/gpg.key | sudo apt-key add - +echo "deb https://packages.grafana.com/oss/deb stable main" | sudo tee -a /etc/apt/sources.list.d/grafana.list +sudo apt update +sudo apt install grafana +sudo systemctl enable grafana-server +sudo systemctl start grafana-server +``` + +Коли Grafana буде запущено, програма має бути доступна за адресою `localhost:3000`. +Відкрийте цю адресу у бажаному браузері, а потім увійдіть, використовуючи облікові дані за замовчуванням (користувач: `admin`, пароль: `admin`). Коли надійде запит, змініть пароль за замовчуванням та збережіть. + +![](./grafana1.png) + +Вас буде перенаправлено на головну сторінку Grafana. Спочатку налаштуйте вихідні дані. Натисніть значок конфігурації на панелі ліворуч і виберіть "Джерела даних". + +![](./grafana2.png) + +Джерел даних ще не створено; натисніть "Додати джерело даних", щоб його визначити. + +![](./grafana3.png) + +Для цього налаштування виберіть "InfluxDB" і продовжуйте. + +![](./grafana4.png) + +Налаштування джерела даних є досить простим, якщо ви запускаєте інструменти на тому ж комп'ютері. Вам потрібно вказати адресу InfluxDB та дані для доступу до бази даних. Дивіться зображення нижче. + +![](./grafana5.png) + +Якщо все заповнено правильно й InfluxDB доступний, натисніть "Зберегти та перевірити" і дочекайтеся спливаючого вікна з підтвердженням. + +![](./grafana6.png) + +Тепер Grafana налаштовано для читання даних з InfluxDB. Тепер вам потрібно створити інформаційну панель, яка буде інтерпретувати та відображати ці дані. Властивості інформаційних панелей закодовані у файлах JSON, які може створити будь-хто, і їх можна легко імпортувати. На панелі ліворуч натисніть "Створити та імпортувати". + +![](./grafana7.png) + +Для інформаційної панелі моніторингу Geth скопіюйте ID [цієї панелі](https://grafana.com/grafana/dashboards/13877/) і вставте його на "Сторінці імпорту" в Grafana. Після збереження інформаційна панель повинна мати такий вигляд: + +![](./grafana8.png) + +Ви можете змінювати свої інформаційні панелі. Кожну панель можна редагувати, переміщувати, видаляти або додавати. Ви можете змінювати свої конфігурації. Усе залежить від вас! Щоб дізнатися більше про те, як працюють інформаційні панелі, зверніться до [документації Grafana](https://grafana.com/docs/grafana/latest/dashboards/). +Вас також може зацікавити розділ [Сповіщення](https://grafana.com/docs/grafana/latest/alerting/). Це дає змогу налаштувати сповіщення, які спрацьовуватимуть, коли метрики досягають певних значень. Підтримуються різні канали зв'язку. diff --git a/public/content/translations/uk/developers/tutorials/nft-minter/index.md b/public/content/translations/uk/developers/tutorials/nft-minter/index.md new file mode 100644 index 00000000000..0815b2e33ed --- /dev/null +++ b/public/content/translations/uk/developers/tutorials/nft-minter/index.md @@ -0,0 +1,874 @@ +--- +title: "Посібник із карбування NFT" +description: "У цьому посібнику ви створите інструмент для карбування NFT та навчитеся створювати повноцінний dapp, підключивши свій смарт-контракт до фронтенду на React за допомогою MetaMask та інструментів Web3." +author: "smudgil" +tags: + [ + "мова програмування", + "NFT", + "alchemy", + "Смарт-контракти", + "використання", + "Pinata" + ] +skill: intermediate +lang: uk +published: 2021-10-06 +--- + +Однією з найбільших проблем для розробників, які працюють із Web2, є можливість підключення вашого смарт-контракту до фронтенду проєкту та взаємодія з ним. + +Створюючи інструмент для карбування NFT — простий інтерфейс користувача, де ви можете ввести посилання на свій цифровий актив, назву та опис, — ви навчитеся: + +- Підключатися до MetaMask через ваш фронтенд-проєкт +- Викликати методи смарт-контракту з вашого фронтенду +- Підписувати транзакції за допомогою MetaMask + +У цьому посібнику ми будемо використовувати [React](https://react.dev/) як наш фронтенд-фреймворк. Оскільки цей посібник в основному зосереджений на розробці Web3, ми не будемо витрачати багато часу на розбір основ React. Натомість ми зосередимося на додаванні функціональності до нашого проєкту. + +Як попередня умова, ви повинні мати базове розуміння React — знати, як працюють компоненти, пропси, useState/useEffect та виклик базових функцій. Якщо ви ніколи раніше не чули про ці терміни, можливо, ви захочете ознайомитися з цим [посібником «Вступ до React»](https://react.dev/learn/tutorial-tic-tac-toe). Для тих, хто краще сприймає інформацію візуально, ми наполегливо рекомендуємо цю чудову серію відео [«Повний сучасний посібник з React»](https://www.youtube.com/playlist?list=PL4cUxeGkcC9gZD-Tvwfod2gaISzfRiP9d) від Net Ninja. + +І якщо ви ще цього не зробили, вам обов'язково знадобиться обліковий запис Alchemy, щоб завершити цей посібник, а також створювати що-небудь на блокчейні. Зареєструйте безкоштовний обліковий запис [тут](https://alchemy.com/). + +Без зайвих слів, розпочнімо! + +## Створення NFT 101 {#making-nfts-101} + +Перш ніж ми почнемо розглядати будь-який код, важливо зрозуміти, як працює створення NFT. Це включає два кроки: + +### Опублікуйте смарт-контракт NFT у блокчейні Ethereum {#publish-nft} + +Найбільша відмінність між двома стандартами смарт-контрактів NFT полягає в тому, що ERC-1155 є стандартом з кількома токенами та включає пакетну функціональність, тоді як ERC-721 є стандартом з одним маркером і тому підтримує передачу лише одного токена за раз. + +### Викличте функцію карбування {#minting-function} + +Зазвичай ця функція карбування вимагає передачі двох змінних як параметрів: по-перше, `recipient` (одержувач), який вказує адресу, що отримає ваш щойно викарбуваний NFT, і, по-друге, `tokenURI` NFT — рядок, який вказує на JSON-документ, що описує метадані NFT. + +Метадані NFT — це те, що по-справжньому вдихає в нього життя, дозволяючи йому мати такі властивості, як назва, опис, зображення (або інший цифровий актив) та інші атрибути. Ось [приклад tokenURI](https://gateway.pinata.cloud/ipfs/QmSvBcb4tjdFpajGJhbFAWeK3JAxCdNQLQtr6ZdiSi42V2), який містить метадані NFT. + +У цьому посібнику ми зосередимося на частині 2: виклику функції карбування існуючого смарт-контракту NFT за допомогою нашого інтерфейсу користувача на React. + +[Ось посилання](https://ropsten.etherscan.io/address/0x4C4a07F737Bf57F6632B6CAB089B78f62385aCaE) на смарт-контракт NFT ERC-721, який ми будемо викликати в цьому посібнику. Якщо ви хочете дізнатися, як ми його створили, ми наполегливо рекомендуємо вам ознайомитися з іншим нашим посібником [«Як створити NFT»](https://www.alchemy.com/docs/how-to-create-an-nft). + +Чудово, тепер, коли ми розуміємо, як працює створення NFT, давайте клонуємо наші стартові файли! + +## Клонуйте стартові файли {#clone-the-starter-files} + +Спочатку перейдіть до [репозиторію nft-minter-tutorial на GitHub](https://github.com/alchemyplatform/nft-minter-tutorial), щоб отримати стартові файли для цього проєкту. Клонуйте цей репозиторій у своє локальне середовище. + +Коли ви відкриєте цей клонований репозиторій `nft-minter-tutorial`, ви помітите, що він містить дві папки: `minter-starter-files` та `nft-minter`. + +- `minter-starter-files` містить стартові файли (по суті, інтерфейс користувача на React) для цього проєкту. У цьому посібнику **ми будемо працювати в цьому каталозі**, вивчаючи, як оживити цей інтерфейс користувача, підключивши його до вашого гаманця Ethereum та смарт-контракту NFT. +- `nft-minter` містить повний завершений посібник і слугує для вас **довідковим матеріалом**, **якщо ви зіткнетеся з труднощами.** + +Далі відкрийте свою копію `minter-starter-files` у вашому редакторі коду, а потім перейдіть до папки `src`. + +Весь код, який ми будемо писати, буде знаходитися в папці `src`. Ми будемо редагувати компонент `Minter.js` і писати додаткові файли javascript, щоб надати нашому проєкту функціональність Web3. + +## Крок 2: Ознайомтеся з нашими стартовими файлами {#step-2-check-out-our-starter-files} + +Перш ніж ми почнемо кодувати, важливо перевірити, що вже надано нам у стартових файлах. + +### Запустіть ваш проєкт на React {#get-your-react-project-running} + +Давайте почнемо із запуску проєкту на React у нашому браузері. Перевага React полягає в тому, що коли наш проєкт запущений у браузері, будь-які збережені зміни будуть оновлюватися в браузері в реальному часі. + +Щоб запустити проєкт, перейдіть до кореневого каталогу папки `minter-starter-files` і запустіть `npm install` у вашому терміналі, щоб встановити залежності проєкту: + +```bash +cd minter-starter-files +npm install +``` + +Після завершення інсталяції запустіть `npm start` у вашому терміналі: + +```bash +npm start +``` + +Це має відкрити http://localhost:3000/ у вашому браузері, де ви побачите фронтенд нашого проєкту. Він повинен складатися з 3 полів: місце для введення посилання на актив вашого NFT, введення назви вашого NFT та надання опису. + +Якщо ви спробуєте натиснути кнопки «Підключити гаманець» або «Викарбувати NFT», ви помітите, що вони не працюють — це тому, що нам ще потрібно запрограмувати їхню функціональність! :\) + +### Компонент Minter.js {#minter-js} + +**ПРИМІТКА:** Переконайтеся, що ви перебуваєте в папці `minter-starter-files`, а не в папці `nft-minter`! + +Давайте повернемося до папки `src` у нашому редакторі та відкриємо файл `Minter.js`. Дуже важливо, щоб ми розуміли все в цьому файлі, оскільки це основний компонент React, над яким ми будемо працювати. + +У верхній частині цього файлу ми маємо змінні стану, які ми будемо оновлювати після певних подій. + +```javascript +//Змінні стану +const [walletAddress, setWallet] = useState("") +const [status, setStatus] = useState("") +const [name, setName] = useState("") +const [description, setDescription] = useState("") +const [url, setURL] = useState("") +``` + +Ніколи не чули про змінні стану React або хуки стану? Ознайомтеся з [цією](https://legacy.reactjs.org/docs/hooks-state.html) документацією. + +Ось що представляє кожна зі змінних: + +- `walletAddress` — рядок, у якому зберігається адреса гаманця користувача +- `status` — рядок, що містить повідомлення для відображення в нижній частині інтерфейсу користувача +- `name` — рядок, у якому зберігається назва NFT +- `description` — рядок, у якому зберігається опис NFT +- `url` — рядок, що є посиланням на цифровий актив NFT + +Після змінних стану ви побачите три нереалізовані функції: `useEffect`, `connectWalletPressed` та `onMintPressed`. Ви помітите, що всі ці функції є `async`, це тому, що ми будемо робити в них асинхронні виклики API! Їхні назви відповідають їхнім функціональним можливостям: + +```javascript +useEffect(async () => { + //TODO: реалізувати +}, []) + +const connectWalletPressed = async () => { + //TODO: реалізувати +} + +const onMintPressed = async () => { + //TODO: реалізувати +} +``` + +- [`useEffect`](https://legacy.reactjs.org/docs/hooks-effect.html) — це хук React, який викликається після візуалізації вашого компонента. Оскільки йому передається пропс у вигляді порожнього масиву `[]` (див. рядок 3), він буде викликаний лише під час _першої_ візуалізації компонента. Тут ми викличемо наш прослуховувач гаманця та іншу функцію гаманця, щоб оновити наш інтерфейс користувача та відобразити, чи гаманець уже підключено. +- `connectWalletPressed` — ця функція буде викликана для підключення гаманця MetaMask користувача до нашого dapp. +- `onMintPressed` — ця функція буде викликана для карбування NFT користувача. + +Ближче до кінця цього файлу ми маємо інтерфейс користувача нашого компонента. Якщо ви уважно переглянете цей код, ви помітите, що ми оновлюємо наші змінні стану `url`, `name` та `description`, коли змінюється введення у відповідних текстових полях. + +Ви також побачите, що `connectWalletPressed` та `onMintPressed` викликаються при натисканні кнопок з ідентифікаторами `mintButton` та `walletButton` відповідно. + +```javascript +//інтерфейс нашого компонента +return ( +
+ + +

+

🧙‍♂️ Інструмент карбування NFT від Alchemy

+

+ Просто додайте посилання на ваш актив, назву та опис, а потім натисніть «Викарбувати». +

+
+

🖼 Посилання на актив:

+ setURL(event.target.value)} + /> +

🤔 Назва:

+ setName(event.target.value)} + /> +

✍️ Опис:

+ setDescription(event.target.value)} + /> +
+ +

{status}

+
+) +``` + +Нарешті, давайте розглянемо, де додається цей компонент Minter. + +Якщо ви перейдете до файлу `App.js`, який є основним компонентом у React, що діє як контейнер для всіх інших компонентів, ви побачите, що наш компонент Minter вставлено в рядку 7. + +**У цьому посібнику ми будемо редагувати лише файл `Minter.js` та додавати файли до нашої папки `src`.** + +Тепер, коли ми розуміємо, з чим працюємо, давайте налаштуємо наш гаманець Ethereum! + +## Налаштуйте свій гаманець Ethereum {#set-up-your-ethereum-wallet} + +Щоб користувачі могли взаємодіяти з вашим смарт-контрактом, їм потрібно буде підключити свій гаманець Ethereum до вашого dapp. + +### Завантажте MetaMask {#download-metamask} + +Для цього уроку ми будемо використовувати MetaMask, віртуальний гаманець в браузері, який використовується для керування адресою облікового запису Ethereum. Якщо ви хочете дізнатися більше про те, як працюють транзакції в Ethereum, перегляньте [цю сторінку](/developers/docs/transactions/). + +Ви можете завантажити та створити обліковий запис MetaMask безкоштовно [тут](https://metamask.io/download). Під час створення облікового запису, або якщо у вас вже є обліковий запис, не забудьте переключитися на «Тестову мережу Ropsten» у верхньому правому куті (щоб ми не мали справу з реальними грошима). + +### Додайте ефір із крана (Faucet) {#add-ether-from-faucet} + +Щоб карбувати наші NFT (або підписувати будь-які транзакції в блокчейні Ethereum), нам знадобиться трохи тестового Eth. Щоб отримати Eth, ви можете перейти до [крана Ropsten](https://faucet.ropsten.be/), ввести адресу свого облікового запису Ropsten, а потім натиснути «Надіслати Ropsten Eth». Незабаром ви маєте побачити Eth у своєму обліковому записі MetaMask! + +### Перевірте свій баланс {#check-your-balance} + +Щоб перевірити наявність балансу, давайте зробимо запит [eth_getBalance](https://docs.alchemyapi.io/alchemy/documentation/alchemy-api-reference/json-rpc#eth_getbalance) за допомогою [інструмента-композитора від Alchemy](https://composer.alchemyapi.io/?composer_state=%7B%22network%22%3A0%2C%22methodName%22%3A%22eth_getBalance%22%2C%22paramValues%22%3A%5B%22%22%2C%22latest%22%5D%7D). Це поверне кількість Eth у нашому гаманці. Після введення вашої адреси облікового запису MetaMask і натисніть кнопку "Відправити запит", ви повинні побачити таку відповідь: + +```text +{"jsonrpc": "2.0", "id": 0, "result": "0xde0b6b3a7640000"} +``` + +**ПРИМІТКА:** Цей результат у wei, а не в eth. Wei використовується в якості найменшого номіналу ether. Перетворення з wei в eth: 1 eth = 10¹⁸ wei. Отже, якщо ми перетворимо 0xde0b6b3a7640000 у десяткове число, ми отримаємо 1\*10¹⁸, що дорівнює 1 eth. + +Фух! Наші підроблені гроші усі там! + +## Підключіть MetaMask до свого інтерфейсу користувача {#connect-metamask-to-your-UI} + +Тепер, коли наш гаманець MetaMask налаштовано, давайте підключимо до нього наш dapp! + +Оскільки ми хочемо дотримуватися парадигми [MVC](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller), ми створимо окремий файл, який міститиме наші функції для керування логікою, даними та правилами нашого dapp, а потім передамо ці функції нашому фронтенду (компоненту Minter.js). + +### Функція `connectWallet` {#connect-wallet-function} + +Для цього давайте створимо нову папку під назвою `utils` у вашому каталозі `src` і додамо до неї файл `interact.js`, який міститиме всі наші функції взаємодії з гаманцем та смарт-контрактом. + +У нашому файлі `interact.js` ми напишемо функцію `connectWallet`, яку потім імпортуємо та викличемо в нашому компоненті `Minter.js`. + +У вашому файлі `interact.js` додайте наступне + +```javascript +export const connectWallet = async () => { + if (window.ethereum) { + try { + const addressArray = await window.ethereum.request({ + method: "eth_requestAccounts", + }) + const obj = { + status: "👆🏽 Напишіть повідомлення в текстовому полі вище.", + address: addressArray[0], + } + return obj + } catch (err) { + return { + address: "", + status: "😥 " + err.message, + } + } + } else { + return { + address: "", + status: ( + +

+ {" "} + 🦊 + Ви повинні встановити MetaMask, віртуальний гаманець Ethereum, у вашому браузері. + +

+
+ ), + } + } +} +``` + +Давайте розберемо, що робить цей код: + +По-перше, наша функція перевіряє, чи ввімкнено `window.ethereum` у вашому браузері. + +`window.ethereum` — це глобальний API, що впроваджується MetaMask та іншими постачальниками гаманців, який дозволяє веб-сайтам запитувати облікові записи Ethereum користувачів. Якщо схвалено, він може читати дані з блокчейнів, до яких підключений користувач, і пропонувати користувачеві підписувати повідомлення та транзакції. Для отримання додаткової інформації перегляньте [документацію MetaMask](https://docs.metamask.io/guide/ethereum-provider.html#table-of-contents)! + +Якщо `window.ethereum` _не_ присутній, це означає, що MetaMask не встановлено. Це призводить до повернення об'єкта JSON, де повернута `address` є порожнім рядком, а об'єкт JSX `status` повідомляє, що користувач повинен встановити MetaMask. + +**Більшість функцій, які ми пишемо, повертатимуть об'єкти JSON, які ми можемо використовувати для оновлення наших змінних стану та інтерфейсу користувача.** + +Тепер, якщо `window.ethereum` _присутній_, ось тут і починається найцікавіше. + +Використовуючи цикл try/catch, ми спробуємо підключитися до MetaMask, викликавши [`window.ethereum.request({ method: "eth_requestAccounts" });`](https://docs.metamask.io/guide/rpc-api.html#eth-requestaccounts). Виклик цієї функції відкриє MetaMask у браузері, де користувачеві буде запропоновано підключити свій гаманець до вашого dapp. + +- Якщо користувач вирішить підключитися, `method: "eth_requestAccounts"` поверне масив, що містить усі адреси облікових записів користувача, підключені до dapp. Загалом, наша функція `connectWallet` поверне об'єкт JSON, який містить _першу_ `address` у цьому масиві (див. рядок 9) та повідомлення `status`, яке пропонує користувачеві написати повідомлення для смарт-контракту. +- Якщо користувач відхиляє підключення, то об'єкт JSON міститиме порожній рядок для повернутої `address` та повідомлення `status`, що відображає відхилення підключення користувачем. + +### Додайте функцію connectWallet до вашого компонента Minter.js UI {#add-connect-wallet} + +Тепер, коли ми написали цю функцію `connectWallet`, давайте підключимо її до нашого компонента `Minter.js`. + +Спочатку нам потрібно буде імпортувати нашу функцію у файл `Minter.js`, додавши `import { connectWallet } from "./utils/interact.js";` у верхню частину файлу `Minter.js`. Ваші перші 11 рядків файлу `Minter.js` тепер повинні виглядати так: + +```javascript +import { useEffect, useState } from "react"; +import { connectWallet } from "./utils/interact.js"; + +const Minter = (props) => { + + //Змінні стану + const [walletAddress, setWallet] = useState(""); + const [status, setStatus] = useState(""); + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [url, setURL] = useState(""); +``` + +Потім усередині нашої функції `connectWalletPressed` ми викличемо нашу імпортовану функцію `connectWallet`, ось так: + +```javascript +const connectWalletPressed = async () => { + const walletResponse = await connectWallet() + setStatus(walletResponse.status) + setWallet(walletResponse.address) +} +``` + +Зверніть увагу, як більша частина нашої функціональності абстрагована від нашого компонента `Minter.js` у файл `interact.js`? Це для того, щоб ми дотримувалися парадигми M-V-C! + +У `connectWalletPressed` ми просто робимо await-виклик нашої імпортованої функції `connectWallet` і, використовуючи її відповідь, оновлюємо наші змінні `status` та `walletAddress` за допомогою їхніх хуків стану. + +Тепер збережімо обидва файли, `Minter.js` та `interact.js`, і протестуємо наш інтерфейс користувача. + +Відкрийте свій браузер за адресою localhost:3000 і натисніть кнопку «Підключити гаманець» у верхньому правому куті сторінки. + +Якщо у вас встановлено MetaMask, вам буде запропоновано підключити свій гаманець до вашого dapp. Прийміть запрошення на підключення. + +Ви маєте побачити, що кнопка гаманця тепер відображає, що ваша адреса підключена. + +Далі спробуйте оновити сторінку… це дивно. Наша кнопка гаманця пропонує нам підключити MetaMask, хоча він уже підключений… + +Але не хвилюйтеся! Ми можемо легко це виправити, реалізувавши функцію під назвою `getCurrentWalletConnected`, яка перевірить, чи адреса вже підключена до нашого dapp, і відповідно оновить наш інтерфейс користувача! + +### Функція getCurrentWalletConnected {#get-current-wallet} + +У вашому файлі `interact.js` додайте наступну функцію `getCurrentWalletConnected`: + +```javascript +export const getCurrentWalletConnected = async () => { + if (window.ethereum) { + try { + const addressArray = await window.ethereum.request({ + method: "eth_accounts", + }) + if (addressArray.length > 0) { + return { + address: addressArray[0], + status: "👆🏽 Напишіть повідомлення в текстовому полі вище.", + } + } else { + return { + address: "", + status: "🦊 Підключіться до MetaMask за допомогою кнопки у верхньому правому куті.", + } + } + } catch (err) { + return { + address: "", + status: "😥 " + err.message, + } + } + } else { + return { + address: "", + status: ( + +

+ {" "} + 🦊 + Ви повинні встановити MetaMask, віртуальний гаманець Ethereum, у вашому браузері. + +

+
+ ), + } + } +} +``` + +Цей код _дуже_ схожий на функцію `connectWallet`, яку ми щойно написали. + +Основна відмінність полягає в тому, що замість виклику методу `eth_requestAccounts`, який відкриває MetaMask для підключення гаманця користувача, тут ми викликаємо метод `eth_accounts`, який просто повертає масив, що містить адреси MetaMask, які зараз підключені до нашого dapp. + +Щоб побачити цю функцію в дії, давайте викличемо її у функції `useEffect` нашого компонента `Minter.js`. + +Як і у випадку з `connectWallet`, ми повинні імпортувати цю функцію з нашого файлу `interact.js` у наш файл `Minter.js` ось так: + +```javascript +import { useEffect, useState } from "react" +import { + connectWallet, + getCurrentWalletConnected, //імпортувати тут +} from "./utils/interact.js" +``` + +Тепер ми просто викликаємо її в нашій функції `useEffect`: + +```javascript +useEffect(async () => { + const { address, status } = await getCurrentWalletConnected() + setWallet(address) + setStatus(status) +}, []) +``` + +Зверніть увагу, що ми використовуємо відповідь нашого виклику `getCurrentWalletConnected` для оновлення наших змінних стану `walletAddress` та `status`. + +Після додавання цього коду спробуйте оновити вікно браузера. Кнопка має показувати, що ви підключені, і відображати попередній перегляд адреси вашого підключеного гаманця — навіть після оновлення! + +### Реалізуйте addWalletListener {#implement-add-wallet-listener} + +Останній крок у налаштуванні гаманця нашого dapp — це реалізація прослуховувача гаманця, щоб наш інтерфейс користувача оновлювався при зміні стану нашого гаманця, наприклад, коли користувач відключається або перемикає облікові записи. + +У вашому файлі `Minter.js` додайте функцію `addWalletListener`, яка виглядає наступним чином: + +```javascript +function addWalletListener() { + if (window.ethereum) { + window.ethereum.on("accountsChanged", (accounts) => { + if (accounts.length > 0) { + setWallet(accounts[0]) + setStatus("👆🏽 Напишіть повідомлення в текстовому полі вище.") + } else { + setWallet("") + setStatus("🦊 Підключіться до MetaMask за допомогою кнопки у верхньому правому куті.") + } + }) + } else { + setStatus( +

+ {" "} + 🦊 + Ви повинні встановити MetaMask, віртуальний гаманець Ethereum, у вашому браузері. + +

+ ) + } +} +``` + +Давайте швидко розберемо, що тут відбувається: + +- По-перше, наша функція перевіряє, чи ввімкнено `window.ethereum` (тобто, чи встановлено MetaMask). + - Якщо ні, ми просто встановлюємо нашу змінну стану `status` на рядок JSX, який пропонує користувачеві встановити MetaMask. + - Якщо він увімкнений, ми налаштовуємо прослуховувач `window.ethereum.on("accountsChanged")` у рядку 3, який прослуховує зміни стану в гаманці MetaMask, зокрема, коли користувач підключає додатковий обліковий запис до dapp, перемикає облікові записи або відключає обліковий запис. Якщо підключено хоча б один обліковий запис, змінна стану `walletAddress` оновлюється як перший обліковий запис у масиві `accounts`, що повертається прослуховувачем. В іншому випадку `walletAddress` встановлюється як порожній рядок. + +Нарешті, ми повинні викликати її в нашій функції `useEffect`: + +```javascript +useEffect(async () => { + const { address, status } = await getCurrentWalletConnected() + setWallet(address) + setStatus(status) + + addWalletListener() +}, []) +``` + +І вуаля! Ми завершили програмування всієї функціональності нашого гаманця! Тепер, коли наш гаманець налаштовано, давайте розберемося, як викарбувати наш NFT! + +## Метадані NFT 101 {#nft-metadata-101} + +Отже, пам'ятаєте метадані NFT, про які ми говорили в кроці 0 цього посібника? Вони вдихають життя в NFT, дозволяючи йому мати такі властивості, як цифровий актив, назву, опис та інші атрибути. + +Нам потрібно буде налаштувати ці метадані як об'єкт JSON і зберегти його, щоб ми могли передати його як параметр `tokenURI` при виклику функції `mintNFT` нашого смарт-контракту. + +Текст у полях «Посилання на актив», «Назва», «Опис» складатиме різні властивості метаданих нашого NFT. Ми відформатуємо ці метадані як об'єкт JSON, але є кілька варіантів, де ми можемо зберігати цей об'єкт JSON: + +- Ми могли б зберігати його в блокчейні Ethereum, однак це було б дуже дорого. +- Ми могли б зберігати його на централізованому сервері, такому як AWS або Firebase. Але це суперечило б нашому етосу децентралізації. +- Ми могли б використовувати IPFS, децентралізований протокол і однорангову мережу для зберігання та обміну даними в розподіленій файловій системі. Оскільки цей протокол децентралізований і безкоштовний, це наш найкращий варіант! + +Для зберігання наших метаданих в IPFS ми будемо використовувати [Pinata](https://pinata.cloud/), зручний API та інструментарій для IPFS. У наступному кроці ми пояснимо, як саме це зробити! + +## Використовуйте Pinata, щоб закріпити ваші метадані в IPFS {#use-pinata-to-pin-your-metadata-to-IPFS} + +Якщо у вас немає облікового запису [Pinata](https://pinata.cloud/), зареєструйте безкоштовний обліковий запис [тут](https://app.pinata.cloud/auth/signup) і виконайте кроки для підтвердження вашої електронної пошти та облікового запису. + +### Створіть свій ключ API Pinata {#create-pinata-api-key} + +Перейдіть на сторінку [https://pinata.cloud/keys](https://pinata.cloud/keys), потім виберіть кнопку «New Key» (Новий ключ) вгорі, увімкніть віджет Admin і назвіть свій ключ. + +Потім вам буде показано спливаюче вікно з інформацією про ваш API. Обов'язково збережіть це в надійному місці. + +Тепер, коли наш ключ налаштовано, давайте додамо його до нашого проєкту, щоб ми могли його використовувати. + +### Створіть файл .env {#create-a-env} + +Ми можемо безпечно зберігати наш ключ і секрет Pinata у файлі середовища. Давайте встановимо пакет [dotenv](https://www.npmjs.com/package/dotenv) у вашому каталозі проєкту. + +Відкрийте нову вкладку у вашому терміналі (окрему від тієї, де запущено localhost) і переконайтеся, що ви перебуваєте в папці `minter-starter-files`, а потім виконайте наступну команду у вашому терміналі: + +```text +npm install dotenv --save +``` + +Далі створіть файл `.env` у кореневому каталозі вашого `minter-starter-files`, ввівши наступне в командному рядку: + +```javascript +vim.env +``` + +Це відкриє ваш файл `.env` у vim (текстовому редакторі). Щоб зберегти його, натисніть «esc» + «:» + «q» на клавіатурі в такому порядку. + +Далі у VSCode перейдіть до файлу `.env` і додайте до нього свій ключ API та секрет API Pinata, ось так: + +```text +REACT_APP_PINATA_KEY = +REACT_APP_PINATA_SECRET = +``` + +Збережіть файл, і тоді ви будете готові почати писати функцію для завантаження ваших метаданих JSON в IPFS! + +### Реалізуйте pinJSONToIPFS {#pin-json-to-ipfs} + +На щастя для нас, Pinata має [API спеціально для завантаження даних JSON в IPFS](https://docs.pinata.cloud/api-reference/endpoint/ipfs/pin-json-to-ipfs#pin-json) і зручний приклад на JavaScript з axios, який ми можемо використовувати з деякими незначними змінами. + +У вашій папці `utils` давайте створимо ще один файл під назвою `pinata.js`, а потім імпортуємо наш секрет і ключ Pinata з файлу .env ось так: + +```javascript +require("dotenv").config() +const key = process.env.REACT_APP_PINATA_KEY +const secret = process.env.REACT_APP_PINATA_SECRET +``` + +Далі вставте додатковий код з нижчеподаного у ваш файл `pinata.js`. Не хвилюйтеся, ми розберемо, що все це означає! + +```javascript +require("dotenv").config() +const key = process.env.REACT_APP_PINATA_KEY +const secret = process.env.REACT_APP_PINATA_SECRET + +const axios = require("axios") + +export const pinJSONToIPFS = async (JSONBody) => { + const url = `https://api.pinata.cloud/pinning/pinJSONToIPFS` + //робимо POST-запит axios до Pinata ⬇️ + return axios + .post(url, JSONBody, { + headers: { + pinata_api_key: key, + pinata_secret_api_key: secret, + }, + }) + .then(function (response) { + return { + success: true, + pinataUrl: + "https://gateway.pinata.cloud/ipfs/" + response.data.IpfsHash, + } + }) + .catch(function (error) { + console.log(error) + return { + success: false, + message: error.message, + } + }) +} +``` + +Отже, що саме робить цей код? + +По-перше, він імпортує [axios](https://www.npmjs.com/package/axios), HTTP-клієнт на основі промісів для браузера та node.js, який ми будемо використовувати для виконання запиту до Pinata. + +Потім у нас є наша асинхронна функція `pinJSONToIPFS`, яка приймає `JSONBody` як вхідні дані та ключ і секрет API Pinata у своєму заголовку, все це для того, щоб зробити POST-запит до їхнього API `pinJSONToIPFS`. + +- Якщо цей POST-запит успішний, то наша функція повертає об'єкт JSON з булевим значенням `success`, встановленим як true, і `pinataUrl`, де були закріплені наші метадані. Ми будемо використовувати цей повернутий `pinataUrl` як вхідні дані `tokenURI` для функції карбування нашого смарт-контракту. +- Якщо цей POST-запит не вдається, то наша функція повертає об'єкт JSON з булевим значенням `success` як false і рядком `message`, що передає нашу помилку. + +Як і у випадку з типами повернення функції `connectWallet`, ми повертаємо об'єкти JSON, щоб ми могли використовувати їхні параметри для оновлення наших змінних стану та інтерфейсу користувача. + +## Завантажте свій смарт-контракт {#load-your-smart-contract} + +Тепер, коли ми маємо спосіб завантажувати наші метадані NFT в IPFS за допомогою нашої функції `pinJSONToIPFS`, нам знадобиться спосіб завантажити екземпляр нашого смарт-контракту, щоб ми могли викликати його функцію `mintNFT`. + +Як ми вже згадували раніше, в цьому посібнику ми будемо використовувати [цей існуючий смарт-контракт NFT](https://ropsten.etherscan.io/address/0x4C4a07F737Bf57F6632B6CAB089B78f62385aCaE); однак, якщо ви хочете дізнатися, як ми його створили, або створити свій власний, ми наполегливо рекомендуємо вам ознайомитися з іншим нашим посібником, [«Як створити NFT».](https://www.alchemy.com/docs/how-to-create-an-nft). + +### ABI контракту {#contract-abi} + +Якщо ви уважно вивчили наші файли, ви помітили, що в нашому каталозі `src` є файл `contract-abi.json`. ABI необхідний для того, щоб вказати, яку функцію викличе контракт, а також для того, щоб функція повертала дані в очікуваному вами форматі. + +Нам також знадобиться ключ API Alchemy та API Alchemy Web3 для підключення до блокчейну Ethereum та завантаження нашого смарт-контракту. + +### Створіть свій ключ API Alchemy {#create-alchemy-api} + +Якщо у вас ще немає облікового запису Alchemy, [зареєструйтеся безкоштовно тут.](https://alchemy.com/?a=eth-org-nft-minter) + +Після того, як ви створили обліковий запис у Alchemy, ви можете зробити ключ API, створивши додаток. Це дозволить нам робити запити до тестової мережі Ropsten. + +Перейдіть на сторінку «Create App» (Створити додаток) на інформаційній панелі Alchemy, навівши курсор на «Apps» (Додатки) на панелі навігації та натиснувши «Create App» (Створити додаток). + +Назвіть свій додаток (ми вибрали «My First NFT!»), надайте короткий опис, виберіть «Staging» (Проміжне середовище) для середовища, яке використовується для обліку вашого додатка, і виберіть «Ropsten» для вашої мережі. + +Натисніть "Створити додаток", ось і все! Ваш додаток повинен з'явитися у таблиці нижче. + +Чудово, тепер, коли ми створили нашу URL-адресу HTTP Alchemy API, скопіюйте її в буфер обміну... + +…а потім додамо його до нашого файлу `.env`. Загалом, ваш файл .env має виглядати так: + +```text +REACT_APP_PINATA_KEY = +REACT_APP_PINATA_SECRET = +REACT_APP_ALCHEMY_KEY = https://eth-ropsten.alchemyapi.io/v2/ +``` + +Тепер, коли у нас є ABI нашого контракту та ключ API Alchemy, ми готові завантажити наш смарт-контракт за допомогою [Alchemy Web3](https://github.com/alchemyplatform/alchemy-web3). + +### Налаштуйте кінцеву точку Alchemy Web3 та контракт {#setup-alchemy-endpoint} + +По-перше, якщо у вас його ще немає, вам потрібно буде встановити [Alchemy Web3](https://github.com/alchemyplatform/alchemy-web3), перейшовши до домашнього каталогу: `nft-minter-tutorial` в терміналі: + +```text +cd .. +npm install @alch/alchemy-web3 +``` + +Далі повернемося до нашого файлу `interact.js`. У верхній частині файлу додайте наступний код, щоб імпортувати ваш ключ Alchemy з файлу .env та налаштувати вашу кінцеву точку Alchemy Web3: + +```javascript +require("dotenv").config() +const alchemyKey = process.env.REACT_APP_ALCHEMY_KEY +const { createAlchemyWeb3 } = require("@alch/alchemy-web3") +const web3 = createAlchemyWeb3(alchemyKey) +``` + +[Alchemy Web3](https://github.com/alchemyplatform/alchemy-web3) — це оболонка для [Web3.js](https://docs.web3js.org/), що надає розширені методи API та інші важливі переваги, щоб полегшити ваше життя як розробника web3. Він розроблений так, щоб вимагати мінімальної конфігурації, щоб ви могли одразу почати використовувати його у своєму додатку! + +Далі додамо ABI та адресу нашого контракту до нашого файлу. + +```javascript +require("dotenv").config() +const alchemyKey = process.env.REACT_APP_ALCHEMY_KEY +const { createAlchemyWeb3 } = require("@alch/alchemy-web3") +const web3 = createAlchemyWeb3(alchemyKey) + +const contractABI = require("../contract-abi.json") +const contractAddress = "0x4C4a07F737Bf57F6632B6CAB089B78f62385aCaE" +``` + +Коли ми маємо обидва, ми готові почати кодувати нашу функцію карбування! + +## Реалізуйте функцію mintNFT {#implement-the-mintnft-function} + +У вашому файлі `interact.js` давайте визначимо нашу функцію, `mintNFT`, яка, як випливає з назви, буде карбувати наш NFT. + +Оскільки ми будемо робити численні асинхронні виклики (до Pinata для закріплення наших метаданих в IPFS, до Alchemy Web3 для завантаження нашого смарт-контракту, і до MetaMask для підписання наших транзакцій), наша функція також буде асинхронною. + +Три вхідні дані для нашої функції будуть `url` нашого цифрового активу, `name` та `description`. Додайте наступний підпис функції під функцією `connectWallet`: + +```javascript +export const mintNFT = async (url, name, description) => {} +``` + +### Обробка помилок введення {#input-error-handling} + +Звичайно, має сенс мати якусь обробку помилок введення на початку функції, щоб ми виходили з цієї функції, якщо наші вхідні параметри неправильні. Усередині нашої функції додамо наступний код: + +```javascript +export const mintNFT = async (url, name, description) => { + //обробка помилок + if (url.trim() == "" || name.trim() == "" || description.trim() == "") { + return { + success: false, + status: "❗Будь ласка, переконайтеся, що всі поля заповнені перед карбуванням.", + } + } +} +``` + +По суті, якщо будь-який з вхідних параметрів є порожнім рядком, то ми повертаємо об'єкт JSON, де булеве значення `success` є false, а рядок `status` повідомляє, що всі поля в нашому інтерфейсі користувача повинні бути заповнені. + +### Завантажте метадані в IPFS {#upload-metadata-to-ipfs} + +Коли ми знаємо, що наші метадані відформатовані належним чином, наступним кроком є їхнє загортання в об'єкт JSON і завантаження в IPFS за допомогою написаної нами функції `pinJSONToIPFS`! + +Для цього нам спочатку потрібно імпортувати функцію `pinJSONToIPFS` до нашого файлу `interact.js`. У самому верху файлу `interact.js` додамо: + +```javascript +import { pinJSONToIPFS } from "./pinata.js" +``` + +Нагадаємо, що `pinJSONToIPFS` приймає тіло JSON. Отже, перш ніж викликати її, нам потрібно буде відформатувати наші параметри `url`, `name` та `description` в об'єкт JSON. + +Давайте оновимо наш код, щоб створити об'єкт JSON під назвою `metadata`, а потім зробимо виклик `pinJSONToIPFS` з цим параметром `metadata`: + +```javascript +export const mintNFT = async (url, name, description) => { + //обробка помилок + if (url.trim() == "" || name.trim() == "" || description.trim() == "") { + return { + success: false, + status: "❗Будь ласка, переконайтеся, що всі поля заповнені перед карбуванням.", + } + } + + //створення метаданих + const metadata = new Object() + metadata.name = name + metadata.image = url + metadata.description = description + + //виклик pinata + const pinataResponse = await pinJSONToIPFS(metadata) + if (!pinataResponse.success) { + return { + success: false, + status: "😢 Щось пішло не так під час завантаження вашого tokenURI.", + } + } + const tokenURI = pinataResponse.pinataUrl +} +``` + +Зверніть увагу, ми зберігаємо відповідь нашого виклику `pinJSONToIPFS(metadata)` в об'єкті `pinataResponse`. Потім ми аналізуємо цей об'єкт на наявність помилок. + +Якщо є помилка, ми повертаємо об'єкт JSON, де булеве значення `success` є false, а наш рядок `status` повідомляє, що наш виклик не вдався. В іншому випадку ми витягуємо `pinataURL` з `pinataResponse` і зберігаємо його як нашу змінну `tokenURI`. + +Тепер настав час завантажити наш смарт-контракт за допомогою API Alchemy Web3, який ми ініціалізували у верхній частині нашого файлу. Додайте наступний рядок коду в кінець функції `mintNFT`, щоб встановити контракт у глобальній змінній `window.contract`: + +```javascript +window.contract = await new web3.eth.Contract(contractABI, contractAddress) +``` + +Останнє, що потрібно додати до нашої функції `mintNFT`, це наша транзакція Ethereum: + +```javascript +//налаштуйте свою транзакцію Ethereum +const transactionParameters = { + to: contractAddress, // Обов'язково, крім випадків публікації контракту. + from: window.ethereum.selectedAddress, // має збігатися з активною адресою користувача. + data: window.contract.methods + .mintNFT(window.ethereum.selectedAddress, tokenURI) + .encodeABI(), //зробити виклик до смарт-контракту NFT +} + +//підпишіть транзакцію через MetaMask +try { + const txHash = await window.ethereum.request({ + method: "eth_sendTransaction", + params: [transactionParameters], + }) + return { + success: true, + status: + "✅ Перегляньте свою транзакцію на Etherscan: https://ropsten.etherscan.io/tx/" + + txHash, + } +} catch (error) { + return { + success: false, + status: "😥 Щось пішло не так: " + error.message, + } +} +``` + +Якщо ви вже знайомі з транзакціями Ethereum, ви помітите, що структура досить схожа на те, що ви бачили. + +- Спочатку ми налаштовуємо параметри нашої транзакції. + - `to` вказує адресу одержувача (наш смарт-контракт) + - `from` вказує підписувача транзакції (підключену адресу користувача до MetaMask: `window.ethereum.selectedAddress`) + - `data` містить виклик методу `mintNFT` нашого смарт-контракту, який отримує наш `tokenURI` та адресу гаманця користувача, `window.ethereum.selectedAddress`, як вхідні дані +- Потім ми робимо await-виклик, `window.ethereum.request`, де ми просимо MetaMask підписати транзакцію. Зверніть увагу, що в цьому запиті ми вказуємо наш метод eth (eth_SentTransaction) і передаємо наші `transactionParameters`. На цьому етапі MetaMask відкриється в браузері і запропонує користувачеві підписати або відхилити транзакцію. + - Якщо транзакція буде успішною, функція поверне об'єкт JSON, де логічне значення `success` встановлено як true, а рядок `status` пропонує користувачеві перевірити Etherscan для отримання додаткової інформації про свою транзакцію. + - Якщо транзакція не вдається, функція поверне об'єкт JSON, де логічне значення `success` встановлено як false, а рядок `status` передає повідомлення про помилку. + +Загалом, наша функція `mintNFT` повинна виглядати так: + +```javascript +export const mintNFT = async (url, name, description) => { + //обробка помилок + if (url.trim() == "" || name.trim() == "" || description.trim() == "") { + return { + success: false, + status: "❗Будь ласка, переконайтеся, що всі поля заповнені перед карбуванням.", + } + } + + //створення метаданих + const metadata = new Object() + metadata.name = name + metadata.image = url + metadata.description = description + + //запит на закріплення pinata + const pinataResponse = await pinJSONToIPFS(metadata) + if (!pinataResponse.success) { + return { + success: false, + status: "😢 Щось пішло не так під час завантаження вашого tokenURI.", + } + } + const tokenURI = pinataResponse.pinataUrl + + //завантаження смарт-контракту + window.contract = await new web3.eth.Contract(contractABI, contractAddress) //loadContract(); + + //налаштуйте свою транзакцію Ethereum + const transactionParameters = { + to: contractAddress, // Обов'язково, крім випадків публікації контракту. + from: window.ethereum.selectedAddress, // має збігатися з активною адресою користувача. + data: window.contract.methods + .mintNFT(window.ethereum.selectedAddress, tokenURI) + .encodeABI(), //зробити виклик до смарт-контракту NFT + } + + //підписати транзакцію через MetaMask + try { + const txHash = await window.ethereum.request({ + method: "eth_sendTransaction", + params: [transactionParameters], + }) + return { + success: true, + status: + "✅ Перегляньте свою транзакцію на Etherscan: https://ropsten.etherscan.io/tx/" + + txHash, + } + } catch (error) { + return { + success: false, + status: "😥 Щось пішло не так: " + error.message, + } + } +} +``` + +Це одна гігантська функція! Тепер нам просто потрібно підключити нашу функцію `mintNFT` до нашого компонента `Minter.js`... + +## Підключіть mintNFT до нашого фронтенду Minter.js {#connect-our-frontend} + +Відкрийте файл `Minter.js` і оновіть рядок `import { connectWallet, getCurrentWalletConnected } from "./utils/interact.js";` у верхній частині, щоб він виглядав так: + +```javascript +import { + connectWallet, + getCurrentWalletConnected, + mintNFT, +} from "./utils/interact.js" +``` + +Нарешті, реалізуйте функцію `onMintPressed`, щоб зробити await-виклик до вашої імпортованої функції `mintNFT` та оновити змінну стану `status`, щоб відобразити, чи вдалася наша транзакція, чи ні: + +```javascript +const onMintPressed = async () => { + const { status } = await mintNFT(url, name, description) + setStatus(status) +} +``` + +## Розгорніть свій NFT на живому вебсайті {#deploy-your-NFT} + +Готові запустити свій проєкт, щоб користувачі могли з ним взаємодіяти? Ознайомтеся з [цим посібником](https://docs.alchemy.com/alchemy/tutorials/nft-minter/how-do-i-deploy-nfts-online) для розгортання вашого інструмента для карбування на живому вебсайті. + +Останній крок... + +## Підкоріть світ блокчейну {#take-the-blockchain-world-by-storm} + +Жартую, ви дійшли до кінця посібника! + +Підсумовуючи, створюючи інструмент для карбування NFT, ви успішно навчилися: + +- Підключатися до MetaMask через ваш фронтенд-проєкт +- Викликати методи смарт-контракту з вашого фронтенду +- Підписувати транзакції за допомогою MetaMask + +Імовірно, ви хотіли б мати можливість показувати NFT, викарбувані через ваш dapp, у своєму гаманці — тож обов'язково ознайомтеся з нашим коротким посібником [Як переглянути свій NFT у своєму гаманці](https://www.alchemy.com/docs/how-to-view-your-nft-in-your-mobile-wallet)! + +І, як завжди, якщо у вас є які-небудь питання, ми тут, щоб допомогти в [Alchemy Discord](https://discord.gg/gWuC7zB). Ми з нетерпінням чекаємо, щоб побачити, як ви застосуєте концепції з цього посібника у своїх майбутніх проєктах! diff --git a/public/content/translations/uk/developers/tutorials/optimism-std-bridge-annotated-code/index.md b/public/content/translations/uk/developers/tutorials/optimism-std-bridge-annotated-code/index.md new file mode 100644 index 00000000000..39b3b91a39c --- /dev/null +++ b/public/content/translations/uk/developers/tutorials/optimism-std-bridge-annotated-code/index.md @@ -0,0 +1,1356 @@ +--- +title: "Покрокове керівництво для стандартного мостового контракту Optimism" +description: "Як працює стандартний міст для Optimism? Чому це працює таким чином?" +author: Ori Pomerantz +tags: [ "мова програмування", "міст", "рівень 2" ] +skill: intermediate +published: 2022-03-30 +lang: uk +--- + +[Optimism](https://www.optimism.io/) — це [оптимістичний ролап](/developers/docs/scaling/optimistic-rollups/). +Оптимістичні ролапи можуть обробляти транзакції за набагато нижчою ціною, ніж мережа Ethereum Mainnet (також відома як рівень 1 або L1), оскільки транзакції обробляються лише кількома вузлами, а не кожним вузлом у мережі. +Водночас усі дані записуються на L1, тому все можна перевірити та реконструювати з усіма гарантіями цілісності та доступності Mainnet. + +Щоб використовувати активи L1 на Optimism (або будь-якому іншому L2), їх потрібно [переказати через міст](/bridges/#prerequisites). +Один зі способів досягти цього — заблокувати активи (найпоширенішими є ETH і [токени ERC-20](/developers/docs/standards/tokens/erc-20/)) на L1 та отримати еквівалентні активи для використання на L2. +Зрештою, той, у кого вони опиняться, може захотіти повернути їх на L1 через міст. +При цьому активи спалюються на L2, а потім вивільняються користувачеві на L1. + +Саме так працює [стандартний міст Optimism](https://docs.optimism.io/app-developers/bridging/standard-bridge). +У цій статті ми розглянемо вихідний код цього мосту, щоб побачити, як він працює, і вивчимо його як приклад добре написаного коду на Solidity. + +## Потоки керування {#control-flows} + +Міст має два основні потоки: + +- Внесення (з L1 до L2) +- Виведення (з L2 до L1) + +### Процес внесення {#deposit-flow} + +#### Рівень 1 {#deposit-flow-layer-1} + +1. При внесенні ERC-20, вкладник надає мосту дозвіл (allowance) на витрату суми, що вноситься +2. Вкладник викликає міст L1 (`depositERC20`, `depositERC20To`, `depositETH`, або `depositETHTo`) +3. Міст L1 отримує у володіння актив, що переказується через міст + - ETH: актив передається вкладником у межах виклику + - ERC-20: актив переказується мостом самому собі з використанням дозволу (allowance), наданого вкладником +4. Міст L1 використовує механізм міждоменних повідомлень для виклику `finalizeDeposit` на мосту L2 + +#### Рівень 2 {#deposit-flow-layer-2} + +5. Міст L2 перевіряє, чи є виклик `finalizeDeposit` легітимним: + - Надійшло з контракту міждоменних повідомлень + - Початково надійшло з мосту на L1 +6. Міст L2 перевіряє, чи є контракт токена ERC-20 на L2 правильним: + - Контракт L2 повідомляє, що його аналог L1 збігається з тим, звідки надійшли токени на L1 + - Контракт L2 повідомляє, що підтримує правильний інтерфейс ([з використанням ERC-165](https://eips.ethereum.org/EIPS/eip-165)). +7. Якщо контракт L2 правильний, викликати його, щоб викарбувати відповідну кількість токенів на відповідну адресу. Якщо ні, розпочати процес виведення, щоб дозволити користувачеві отримати токени на L1. + +### Процес виведення {#withdrawal-flow} + +#### Рівень 2 {#withdrawal-flow-layer-2} + +1. Той, хто виводить кошти, викликає міст L2 (`withdraw` або `withdrawTo`) +2. Міст L2 спалює відповідну кількість токенів, що належать `msg.sender` +3. Міст L2 використовує механізм міждоменних повідомлень для виклику `finalizeETHWithdrawal` або `finalizeERC20Withdrawal` на мосту L1 + +#### Рівень 1 {#withdrawal-flow-layer-1} + +4. Міст L1 перевіряє, чи є виклик `finalizeETHWithdrawal` або `finalizeERC20Withdrawal` легітимним: + - Надійшло з механізму міждоменних повідомлень + - Початково надійшло з мосту на L2 +5. Міст L1 переказує відповідний актив (ETH або ERC-20) на відповідну адресу + +## Код рівня 1 {#layer-1-code} + +Це код, який виконується на L1, у мережі Ethereum Mainnet. + +### IL1ERC20Bridge {#IL1ERC20Bridge} + +[Цей інтерфейс визначено тут](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts/contracts/L1/messaging/IL1ERC20Bridge.sol). +Він містить функції та визначення, необхідні для переказу токенів ERC-20 через міст. + +```solidity +// SPDX-License-Identifier: MIT +``` + +[Більша частина коду Optimism випущена під ліцензією MIT](https://help.optimism.io/hc/en-us/articles/4411908707995-What-software-license-does-Optimism-use-). + +```solidity +pragma solidity >0.5.0 <0.9.0; +``` + +На момент написання статті остання версія Solidity — 0.8.12. +Поки не випущено версію 0.9.0, ми не знаємо, чи сумісний цей код із нею. + +```solidity +/** + * @title IL1ERC20Bridge + */ +interface IL1ERC20Bridge { + /********** + * Події * + **********/ + + event ERC20DepositInitiated( +``` + +У термінології мосту Optimism _deposit_ означає переказ з L1 на L2, а _withdrawal_ — переказ з L2 на L1. + +```solidity + address indexed _l1Token, + address indexed _l2Token, +``` + +У більшості випадків адреса ERC-20 на L1 не збігається з адресою еквівалентного ERC-20 на L2. +[Список адрес токенів можна переглянути тут](https://static.optimism.io/optimism.tokenlist.json). +Адреса з `chainId` 1 знаходиться на L1 (Mainnet), а адреса з `chainId` 10 — на L2 (Optimism). +Інші два значення `chainId` призначені для тестової мережі Kovan (42) та оптимістичної тестової мережі Kovan (69). + +```solidity + address indexed _from, + address _to, + uint256 _amount, + bytes _data + ); +``` + +Можна додавати примітки до переказів, і в такому разі вони додаються до подій, що їх фіксують. + +```solidity + event ERC20WithdrawalFinalized( + address indexed _l1Token, + address indexed _l2Token, + address indexed _from, + address _to, + uint256 _amount, + bytes _data + ); +``` + +Той самий контракт мосту обробляє перекази в обох напрямках. +У випадку мосту L1 це означає ініціалізацію внесення та фіналізацію виведення коштів. + +```solidity + + /******************** + * Публічні функції * + ********************/ + + /** + * @dev отримати адресу відповідного контракту мосту L2. + * @return Адреса відповідного контракту мосту L2. + */ + function l2TokenBridge() external returns (address); +``` + +Ця функція насправді не потрібна, оскільки на L2 це попередньо розгорнутий контракт, тому вона завжди знаходиться за адресою `0x4200000000000000000000000000000000000010`. +Вона тут для симетрії з мостом L2, оскільки адресу мосту L1 дізнатися _не_ так просто. + +```solidity + /** + * @dev внести суму ERC20 на баланс викликаючого на L2. + * @param _l1Token Адреса L1 ERC20, який ми вносимо + * @param _l2Token Адреса відповідного L2 ERC20 на L1 + * @param _amount Сума ERC20 для внесення + * @param _l2Gas Ліміт газу, необхідний для завершення внесення на L2. + * @param _data Необов’язкові дані для пересилання на L2. Ці дані надаються + * виключно для зручності зовнішніх контрактів. Крім встановлення максимальної + * довжини, ці контракти не дають жодних гарантій щодо їхнього вмісту. + */ + function depositERC20( + address _l1Token, + address _l2Token, + uint256 _amount, + uint32 _l2Gas, + bytes calldata _data + ) external; +``` + +Параметр `_l2Gas` — це кількість газу L2, яку транзакція може витратити. +[До певного (високого) ліміту це безкоштовно](https://community.optimism.io/docs/developers/bridge/messaging/#for-l1-%E2%87%92-l2-transactions-2), тому, якщо контракт ERC-20 не робить нічого дивного під час карбування, це не повинно бути проблемою. +Ця функція обробляє поширений сценарій, коли користувач переказує активи через міст на ту саму адресу в іншому блокчейні. + +```solidity + /** + * @dev внести суму ERC20 на баланс одержувача на L2. + * @param _l1Token Адреса L1 ERC20, який ми вносимо + * @param _l2Token Адреса відповідного L2 ERC20 на L1 + * @param _to Адреса L2, на яку буде зараховано виведення коштів. + * @param _amount Сума ERC20 для внесення. + * @param _l2Gas Ліміт газу, необхідний для завершення внесення на L2. + * @param _data Необов’язкові дані для пересилання на L2. Ці дані надаються + * виключно для зручності зовнішніх контрактів. Крім встановлення максимальної + * довжини, ці контракти не дають жодних гарантій щодо їхнього вмісту. + */ + function depositERC20To( + address _l1Token, + address _l2Token, + address _to, + uint256 _amount, + uint32 _l2Gas, + bytes calldata _data + ) external; +``` + +Ця функція майже ідентична `depositERC20`, але вона дозволяє надсилати ERC-20 на іншу адресу. + +```solidity + /************************* + * Міжмережеві функції * + *************************/ + + /** + * @dev Завершити виведення з L2 на L1 і зарахувати кошти на баланс одержувача + * токена L1 ERC20. + * Цей виклик не вдасться, якщо ініційоване виведення з L2 не було фіналізовано. + * + * @param _l1Token Адреса токена L1, для якого виконується finalizeWithdrawal. + * @param _l2Token Адреса токена L2, де було ініційовано виведення. + * @param _from Адреса L2, що ініціює переказ. + * @param _to Адреса L1, на яку буде зараховано виведення коштів. + * @param _amount Сума ERC20 для внесення. + * @param _data Дані, надані відправником на L2. Ці дані надаються + * виключно для зручності зовнішніх контрактів. Крім встановлення максимальної + * довжини, ці контракти не дають жодних гарантій щодо їхнього вмісту. + */ + function finalizeERC20Withdrawal( + address _l1Token, + address _l2Token, + address _from, + address _to, + uint256 _amount, + bytes calldata _data + ) external; +} +``` + +Виведення коштів (та інші повідомлення з L2 на L1) в Optimism — це двоетапний процес: + +1. Ініціююча транзакція на L2. +2. Фіналізуюча або підтверджуюча транзакція на L1. + Ця транзакція має відбутися після завершення [періоду оскарження помилок](https://community.optimism.io/docs/how-optimism-works/#fault-proofs) для транзакції L2. + +### IL1StandardBridge {#il1standardbridge} + +[Цей інтерфейс визначено тут](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts/contracts/L1/messaging/IL1StandardBridge.sol). +Цей файл містить визначення подій та функцій для ETH. +Ці визначення дуже схожі на визначені вище в `IL1ERC20Bridge` для ERC-20. + +Інтерфейс мосту розділено на два файли, оскільки деякі токени ERC-20 вимагають спеціальної обробки і не можуть бути оброблені стандартним мостом. +Таким чином, спеціальний міст, який обробляє такий токен, може реалізувати `IL1ERC20Bridge` і не мусить також переказувати ETH через міст. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity >0.5.0 <0.9.0; + +import "./IL1ERC20Bridge.sol"; + +/** + * @title IL1StandardBridge + */ +interface IL1StandardBridge is IL1ERC20Bridge { + /********** + * Події * + **********/ + event ETHDepositInitiated( + address indexed _from, + address indexed _to, + uint256 _amount, + bytes _data + ); +``` + +Ця подія майже ідентична версії ERC-20 (`ERC20DepositInitiated`), за винятком відсутності адрес токенів L1 та L2. +Те саме стосується інших подій і функцій. + +```solidity + event ETHWithdrawalFinalized( + . + . + . + ); + + /******************** + * Публічні функції * + ********************/ + + /** + * @dev Внести суму ETH на баланс викликаючого на L2. + . + . + . + */ + function depositETH(uint32 _l2Gas, bytes calldata _data) external payable; + + /** + * @dev Внести суму ETH на баланс одержувача на L2. + . + . + . + */ + function depositETHTo( + address _to, + uint32 _l2Gas, + bytes calldata _data + ) external payable; + + /************************* + * Міжмережеві функції * + *************************/ + + /** + * @dev Завершити виведення з L2 на L1 і зарахувати кошти на баланс одержувача + * токена L1 ETH. Оскільки тільки xDomainMessenger може викликати цю функцію, вона ніколи не буде викликана + * до фіналізації виведення. + . + . + . + */ + function finalizeETHWithdrawal( + address _from, + address _to, + uint256 _amount, + bytes calldata _data + ) external; +} +``` + +### CrossDomainEnabled {#crossdomainenabled} + +[Цей контракт](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts/contracts/libraries/bridge/CrossDomainEnabled.sol) успадковується обома мостами ([L1](#the-l1-bridge-contract) і [L2](#the-l2-bridge-contract)) для надсилання повідомлень на інший рівень. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity >0.5.0 <0.9.0; + +/* Імпорт інтерфейсів */ +import { ICrossDomainMessenger } from "./ICrossDomainMessenger.sol"; +``` + +[Цей інтерфейс](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts/contracts/libraries/bridge/ICrossDomainMessenger.sol) повідомляє контракту, як надсилати повідомлення на інший рівень за допомогою міждоменного месенджера. +Цей міждоменний месенджер — це ціла окрема система, яка заслуговує на власну статтю, яку я сподіваюся написати в майбутньому. + +```solidity +/** + * @title CrossDomainEnabled + * @dev Допоміжний контракт для контрактів, що виконують міждоменні комунікації + * + * Використаний компілятор: визначається контрактом, що успадковує + */ +contract CrossDomainEnabled { + /************* + * Змінні * + *************/ + + // Контракт месенджера, що використовується для надсилання та отримання повідомлень з іншого домену. + address public messenger; + + /*************** + * Конструктор * + ***************/ + + /** + * @param _messenger Адреса CrossDomainMessenger на поточному рівні. + */ + constructor(address _messenger) { + messenger = _messenger; + } +``` + +Єдиний параметр, який має знати контракт, — це адреса міждоменного месенджера на цьому рівні. +Цей параметр встановлюється один раз у конструкторі й ніколи не змінюється. + +```solidity + + /********************** + * Модифікатори функцій * + **********************/ + + /** + * Застосовує обмеження, щоб змінена функція могла викликатися лише з певного міждоменного облікового запису. + * @param _sourceDomainAccount Єдиний обліковий запис у вихідному домені, який + * автентифікований для виклику цієї функції. + */ + modifier onlyFromCrossDomainAccount(address _sourceDomainAccount) { +``` + +Міждоменний обмін повідомленнями доступний будь-якому контракту в блокчейні, де він виконується (або в Ethereum Mainnet, або в Optimism). +Але нам потрібно, щоб міст на кожній стороні довіряв _лише_ тим повідомленням, які надходять від мосту з іншого боку. + +```solidity + require( + msg.sender == address(getCrossDomainMessenger()), + "OVM_XCHAIN: контракт месенджера не автентифікований" + ); +``` + +Можна довіряти лише повідомленням із відповідного міждоменного месенджера (`messenger`, як ви побачите нижче). + +```solidity + + require( + getCrossDomainMessenger().xDomainMessageSender() == _sourceDomainAccount, + "OVM_XCHAIN: неправильний відправник міждоменного повідомлення" + ); +``` + +Спосіб, у який міждоменний месенджер надає адресу, що надіслала повідомлення з іншого рівня, — це [функція `.xDomainMessageSender()`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts/contracts/L1/messaging/L1CrossDomainMessenger.sol#L122-L128). +Доки вона викликається в транзакції, ініційованій повідомленням, вона може надавати цю інформацію. + +Нам потрібно переконатися, що отримане повідомлення надійшло з іншого мосту. + +```solidity + + _; + } + + /********************** + * Внутрішні функції * + **********************/ + + /** + * Отримує месенджер, зазвичай зі сховища. Ця функція є відкритою на випадок, якщо дочірньому контракту + * знадобиться її перевизначити. + * @return Адреса контракту міждоменного месенджера, який слід використовувати. + */ + function getCrossDomainMessenger() internal virtual returns (ICrossDomainMessenger) { + return ICrossDomainMessenger(messenger); + } +``` + +Ця функція повертає міждоменний месенджер. +Ми використовуємо функцію, а не змінну `messenger`, щоб дозволити контрактам, які успадковують від цього, використовувати алгоритм для визначення, який міждоменний месенджер використовувати. + +```solidity + + /** + * Надсилає повідомлення на обліковий запис в іншому домені + * @param _crossDomainTarget Запланований одержувач у цільовому домені + * @param _message Дані для надсилання цілі (зазвичай calldata для функції з + * `onlyFromCrossDomainAccount()`) + * @param _gasLimit gasLimit для отримання повідомлення в цільовому домені. + */ + function sendCrossDomainMessage( + address _crossDomainTarget, + uint32 _gasLimit, + bytes memory _message +``` + +Нарешті, функція, яка надсилає повідомлення на інший рівень. + +```solidity + ) internal { + // slither-disable-next-line reentrancy-events, reentrancy-benign +``` + +[Slither](https://github.com/crytic/slither) — це статичний аналізатор, який Optimism запускає для кожного контракту, щоб знайти вразливості та інші потенційні проблеми. +У цьому випадку наступний рядок викликає дві вразливості: + +1. [Події повторного входу](https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilities-3) +2. [Безпечний повторний вхід](https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilities-2) + +```solidity + getCrossDomainMessenger().sendMessage(_crossDomainTarget, _message, _gasLimit); + } +} +``` + +У цьому випадку ми не турбуємося про повторний вхід, оскільки знаємо, що `getCrossDomainMessenger()` повертає надійну адресу, навіть якщо Slither не може цього знати. + +### Контракт мосту L1 {#the-l1-bridge-contract} + +[Вихідний код цього контракту тут](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts/contracts/L1/messaging/L1StandardBridge.sol). + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; +``` + +Інтерфейси можуть бути частиною інших контрактів, тому вони мають підтримувати широкий діапазон версій Solidity. +Але сам міст — це наш контракт, і ми можемо бути суворими щодо версії Solidity, яку він використовує. + +```solidity +/* Імпорт інтерфейсів */ +import { IL1StandardBridge } from "./IL1StandardBridge.sol"; +import { IL1ERC20Bridge } from "./IL1ERC20Bridge.sol"; +``` + +[IL1ERC20Bridge](#IL1ERC20Bridge) та [IL1StandardBridge](#IL1StandardBridge) пояснено вище. + +```solidity +import { IL2ERC20Bridge } from "../../L2/messaging/IL2ERC20Bridge.sol"; +``` + +[Цей інтерфейс](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts/contracts/L2/messaging/IL2ERC20Bridge.sol) дозволяє нам створювати повідомлення для керування стандартним мостом на L2. + +```solidity +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +``` + +[Цей інтерфейс](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/IERC20.sol) дозволяє нам керувати контрактами ERC-20. +[Детальніше про це можна прочитати тут](/developers/tutorials/erc20-annotated-code/#the-interface). + +```solidity +/* Імпорт бібліотек */ +import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol"; +``` + +[Як пояснювалося вище](#crossdomainenabled), цей контракт використовується для міжрівневого обміну повідомленнями. + +```solidity +import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployAddresses.sol"; +``` + +[`Lib_PredeployAddresses`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts/contracts/libraries/constants/Lib_PredeployAddresses.sol) містить адреси контрактів L2, які завжди мають однакову адресу. Це включає стандартний міст на L2. + +```solidity +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; +``` + +[Утиліти Address від OpenZeppelin](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol). Він використовується для розрізнення адрес контрактів і тих, що належать зовнішнім обліковим записам (EOA). + +Зауважте, що це не ідеальне рішення, оскільки немає способу розрізнити прямі виклики та виклики, зроблені з конструктора контракту, але принаймні це дозволяє нам виявляти та запобігати деяким поширеним помилкам користувачів. + +```solidity +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +``` + +[Стандарт ERC-20](https://eips.ethereum.org/EIPS/eip-20) підтримує два способи повідомлення контрактом про помилку: + +1. Revert +2. Повернення `false` + +Обробка обох випадків ускладнила б наш код, тому замість цього ми використовуємо [`SafeERC20` від OpenZeppelin](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/utils/SafeERC20.sol), який гарантує, що [всі помилки призводять до revert](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/utils/SafeERC20.sol#L96). + +```solidity +/** + * @title L1StandardBridge + * @dev Міст L1 для ETH та ERC20 — це контракт, який зберігає внесені кошти L1 та стандартні + * токени, що використовуються на L2. Він синхронізує відповідний міст L2, інформуючи його про депозити + * та очікуючи від нього повідомлень про нові фіналізовані виведення коштів. + * + */ +contract L1StandardBridge is IL1StandardBridge, CrossDomainEnabled { + using SafeERC20 for IERC20; +``` + +Цей рядок визначає використання обгортки `SafeERC20` щоразу, коли ми використовуємо інтерфейс `IERC20`. + +```solidity + + /******************************** + * Посилання на зовнішні контракти * + ********************************/ + + address public l2TokenBridge; +``` + +Адреса [L2StandardBridge](#the-l2-bridge-contract). + +```solidity + + // Відображає токен L1 на токен L2 до балансу внесеного токена L1 + mapping(address => mapping(address => uint256)) public deposits; +``` + +Подвійне [відображення (mapping)](https://www.tutorialspoint.com/solidity/solidity_mappings.htm) — це спосіб визначення [двовимірного розрідженого масиву](https://en.wikipedia.org/wiki/Sparse_matrix). +Значення в цій структурі даних ідентифікуються як `deposit[адреса токена L1][адреса токена L2]`. +Значення за замовчуванням — нуль. +До сховища записуються лише ті комірки, яким встановлено інше значення. + +```solidity + + /*************** + * Конструктор * + ***************/ + + // Цей контракт працює за проксі, тому параметри конструктора не будуть використовуватися. + constructor() CrossDomainEnabled(address(0)) {} +``` + +Щоб мати можливість оновити цей контракт без необхідності копіювати всі змінні в сховищі. +Для цього ми використовуємо [`Proxy`](https://docs.openzeppelin.com/contracts/3.x/api/proxy), контракт, який використовує [`delegatecall`](https://solidity-by-example.org/delegatecall/) для переадресації викликів до окремого контракту, чия адреса зберігається в контракті проксі (під час оновлення ви повідомляєте проксі змінити цю адресу). +Коли ви використовуєте `delegatecall`, сховище залишається сховищем _викликаючого_ контракту, тому значення всіх змінних стану контракту не змінюються. + +Одним з наслідків цього шаблону є те, що сховище _викликаного_ контракту `delegatecall` не використовується, і тому значення конструктора, передані йому, не мають значення. +Саме тому ми можемо надати безглузде значення конструктору `CrossDomainEnabled`. +Це також причина, чому наведена нижче ініціалізація відокремлена від конструктора. + +```solidity + /****************** + * Ініціалізація * + ******************/ + + /** + * @param _l1messenger Адреса L1 Messenger, що використовується для міжмережевих комунікацій. + * @param _l2TokenBridge Адреса стандартного мосту L2. + */ + // slither-disable-next-line external-function +``` + +Цей [тест Slither](https://github.com/crytic/slither/wiki/Detector-Documentation#public-function-that-could-be-declared-external) виявляє функції, які не викликаються з коду контракту і тому можуть бути оголошені як `external` замість `public`. +Вартість газу для функцій `external` може бути нижчою, оскільки їм можна передавати параметри в calldata. +Функції, оголошені як `public`, мають бути доступні зсередини контракту. +Контракти не можуть змінювати власні calldata, тому параметри мають бути в пам’яті. +Коли така функція викликається ззовні, необхідно скопіювати calldata в пам’ять, що коштує газу. +У цьому випадку функція викликається лише один раз, тому неефективність для нас не має значення. + +```solidity + function initialize(address _l1messenger, address _l2TokenBridge) public { + require(messenger == address(0), "Контракт уже ініціалізовано."); +``` + +Функція `initialize` має викликатися лише один раз. +Якщо адреса міждоменного месенджера L1 або мосту токенів L2 змінюється, ми створюємо новий проксі та новий міст, який його викликає. +Це навряд чи станеться, за винятком випадків оновлення всієї системи, що буває дуже рідко. + +Зауважте, що ця функція не має механізму, який обмежує, _хто_ може її викликати. +Це означає, що теоретично зловмисник може почекати, доки ми розгорнемо проксі та першу версію мосту, а потім [випередити (front-run)](https://solidity-by-example.org/hacks/front-running/), щоб дістатися до функції `initialize` раніше, ніж це зробить легітимний користувач. Але є два способи запобігти цьому: + +1. Якщо контракти розгортаються не безпосередньо EOA, а [у транзакції, яка змушує інший контракт створювати їх](https://medium.com/upstate-interactive/creating-a-contract-with-a-smart-contract-bdb67c5c8595), весь процес може бути атомарним і завершитися до виконання будь-якої іншої транзакції. +2. Якщо легітимний виклик `initialize` не вдається, завжди можна проігнорувати щойно створений проксі та міст і створити нові. + +```solidity + messenger = _l1messenger; + l2TokenBridge = _l2TokenBridge; + } +``` + +Це два параметри, які міст повинен знати. + +```solidity + + /************** + * Внесення * + **************/ + + /** @dev Модифікатор, що вимагає, щоб відправник був EOA. Цю перевірку може + * обійти зловмисний контракт через initcode, але вона запобігає помилці користувача, якої ми хочемо уникнути. + */ + modifier onlyEOA() { + // Використовується для зупинки депозитів з контрактів (щоб уникнути випадкової втрати токенів) + require(!Address.isContract(msg.sender), "Обліковий запис не є EOA"); + _; + } +``` + +Ось чому нам знадобилися утиліти `Address` від OpenZeppelin. + +```solidity + /** + * @dev Цю функцію можна викликати без даних + * для внесення суми ETH на баланс викликаючого на L2. + * Оскільки функція receive не приймає дані, консервативна + * стандартна сума пересилається на L2. + */ + receive() external payable onlyEOA { + _initiateETHDeposit(msg.sender, msg.sender, 200_000, bytes("")); + } +``` + +Ця функція існує для тестування. +Зверніть увагу, що вона не з’являється у визначеннях інтерфейсу — вона не для звичайного використання. + +```solidity + /** + * @inheritdoc IL1StandardBridge + */ + function depositETH(uint32 _l2Gas, bytes calldata _data) external payable onlyEOA { + _initiateETHDeposit(msg.sender, msg.sender, _l2Gas, _data); + } + + /** + * @inheritdoc IL1StandardBridge + */ + function depositETHTo( + address _to, + uint32 _l2Gas, + bytes calldata _data + ) external payable { + _initiateETHDeposit(msg.sender, _to, _l2Gas, _data); + } +``` + +Ці дві функції є обгортками навколо `_initiateETHDeposit`, функції, яка обробляє фактичне внесення ETH. + +```solidity + /** + * @dev Виконує логіку для депозитів, зберігаючи ETH та інформуючи шлюз L2 ETH про + * депозит. + * @param _from Обліковий запис, з якого буде взято депозит на L1. + * @param _to Обліковий запис, на який буде зараховано депозит на L2. + * @param _l2Gas Ліміт газу, необхідний для завершення депозиту на L2. + * @param _data Необов’язкові дані для пересилання на L2. Ці дані надаються + * виключно для зручності зовнішніх контрактів. Крім встановлення максимальної + * довжини, ці контракти не дають жодних гарантій щодо їхнього вмісту. + */ + function _initiateETHDeposit( + address _from, + address _to, + uint32 _l2Gas, + bytes memory _data + ) internal { + // Сконструювати calldata для виклику finalizeDeposit + bytes memory message = abi.encodeWithSelector( +``` + +Спосіб роботи міждоменних повідомлень полягає в тому, що контракт призначення викликається з повідомленням як його calldata. +Контракти Solidity завжди інтерпретують свої calldata відповідно до +[специфікацій ABI](https://docs.soliditylang.org/en/v0.8.12/abi-spec.html). +Функція Solidity [`abi.encodeWithSelector`](https://docs.soliditylang.org/en/v0.8.12/units-and-global-variables.html#abi-encoding-and-decoding-functions) створює ці calldata. + +```solidity + IL2ERC20Bridge.finalizeDeposit.selector, + address(0), + Lib_PredeployAddresses.OVM_ETH, + _from, + _to, + msg.value, + _data + ); +``` + +Повідомлення тут — це виклик [функції `finalizeDeposit`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts/contracts/L2/messaging/L2StandardBridge.sol#L141-L148) з такими параметрами: + +| Параметр | Значення | Значення | +| ------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| \_l1Token | address(0) | Спеціальне значення для ETH (який не є токеном ERC-20) на L1 | +| \_l2Token | Lib_PredeployAddresses.OVM_ETH | Контракт L2, який керує ETH на Optimism, `0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000` (цей контракт призначений лише для внутрішнього використання Optimism) | +| \_from | \_from | Адреса на L1, яка надсилає ETH | +| \_to | \_to | Адреса на L2, яка отримує ETH | +| сума | msg.value | Кількість надісланих wei (які вже надіслано на міст) | +| \_data | \_data | Додаткові дані, які додаються до депозиту | + +```solidity + // Надіслати calldata на L2 + // slither-disable-next-line reentrancy-events + sendCrossDomainMessage(l2TokenBridge, _l2Gas, message); +``` + +Надішліть повідомлення через міждоменний месенджер. + +```solidity + // slither-disable-next-line reentrancy-events + emit ETHDepositInitiated(_from, _to, msg.value, _data); + } +``` + +Надішліть подію, щоб повідомити будь-який децентралізований застосунок, який відстежує цей переказ. + +```solidity + /** + * @inheritdoc IL1ERC20Bridge + */ + function depositERC20( + . + . + . + ) external virtual onlyEOA { + _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, msg.sender, _amount, _l2Gas, _data); + } + + /** + * @inheritdoc IL1ERC20Bridge + */ + function depositERC20To( + . + . + . + ) external virtual { + _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, _to, _amount, _l2Gas, _data); + } +``` + +Ці дві функції є обгортками навколо `_initiateERC20Deposit`, функції, яка обробляє фактичне внесення ERC-20. + +```solidity + /** + * @dev Виконує логіку для депозитів, інформуючи токен L2 Deposited Token + * про депозит і викликаючи обробник для блокування коштів L1. (наприклад, transferFrom) + * + * @param _l1Token Адреса L1 ERC20, який ми вносимо + * @param _l2Token Адреса відповідного L2 ERC20 на L1 + * @param _from Обліковий запис, з якого буде взято депозит на L1 + * @param _to Обліковий запис, на який буде зараховано депозит на L2 + * @param _amount Сума ERC20 для внесення. + * @param _l2Gas Ліміт газу, необхідний для завершення депозиту на L2. + * @param _data Необов’язкові дані для пересилання на L2. Ці дані надаються + * виключно для зручності зовнішніх контрактів. Крім встановлення максимальної + * довжини, ці контракти не дають жодних гарантій щодо їхнього вмісту. + */ + function _initiateERC20Deposit( + address _l1Token, + address _l2Token, + address _from, + address _to, + uint256 _amount, + uint32 _l2Gas, + bytes calldata _data + ) internal { +``` + +Ця функція подібна до `_initiateETHDeposit` вище, з кількома важливими відмінностями. +Перша відмінність полягає в тому, що ця функція отримує адреси токенів і суму для переказу як параметри. +У випадку з ETH виклик мосту вже включає переказ активу на рахунок мосту (`msg.value`). + +```solidity + // Коли депозит ініціюється на L1, міст L1 переказує кошти на свій рахунок для майбутніх + // виведень. safeTransferFrom також перевіряє, чи є у контракту код, тому це не спрацює, якщо + // _from є EOA або address(0). + // slither-disable-next-line reentrancy-events, reentrancy-benign + IERC20(_l1Token).safeTransferFrom(_from, address(this), _amount); +``` + +Перекази токенів ERC-20 відбуваються за іншим процесом, ніж ETH: + +1. Користувач (`_from`) надає дозвіл мосту на переказ відповідних токенів. +2. Користувач викликає міст з адресою контракту токена, сумою тощо. +3. Міст переказує токени (собі) у рамках процесу внесення. + +Перший крок може відбуватися в окремій транзакції від останніх двох. +Однак випередження (front-running) не є проблемою, оскільки дві функції, які викликають `_initiateERC20Deposit` (`depositERC20` і `depositERC20To`), викликають цю функцію лише з `msg.sender` як параметром `_from`. + +```solidity + // Сконструювати calldata для _l2Token.finalizeDeposit(_to, _amount) + bytes memory message = abi.encodeWithSelector( + IL2ERC20Bridge.finalizeDeposit.selector, + _l1Token, + _l2Token, + _from, + _to, + _amount, + _data + ); + + // Надіслати calldata на L2 + // slither-disable-next-line reentrancy-events, reentrancy-benign + sendCrossDomainMessage(l2TokenBridge, _l2Gas, message); + + // slither-disable-next-line reentrancy-benign + deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] + _amount; +``` + +Додайте внесену кількість токенів до структури даних `deposits`. +На L2 може бути кілька адрес, які відповідають одному токену L1 ERC-20, тому для відстеження депозитів недостатньо використовувати баланс токена L1 ERC-20 на мосту. + +```solidity + + // slither-disable-next-line reentrancy-events + emit ERC20DepositInitiated(_l1Token, _l2Token, _from, _to, _amount, _data); + } + + /************************* + * Міжмережеві функції * + *************************/ + + /** + * @inheritdoc IL1StandardBridge + */ + function finalizeETHWithdrawal( + address _from, + address _to, + uint256 _amount, + bytes calldata _data +``` + +Міст L2 надсилає повідомлення до міждоменного месенджера L2, який змушує міждоменний месенджер L1 викликати цю функцію (звичайно, коли [транзакція, яка завершує повідомлення](https://community.optimism.io/docs/developers/bridge/messaging/#fees-for-l2-%E2%87%92-l1-transactions), надсилається на L1). + +```solidity + ) external onlyFromCrossDomainAccount(l2TokenBridge) { +``` + +Переконайтеся, що це _легітимне_ повідомлення, яке надходить від міждоменного месенджера та походить від мосту токенів L2. +Ця функція використовується для виведення ETH з мосту, тому ми повинні переконатися, що її викликає лише авторизований викликаючий. + +```solidity + // slither-disable-next-line reentrancy-events + (bool success, ) = _to.call{ value: _amount }(new bytes(0)); +``` + +Спосіб переказу ETH полягає у виклику одержувача із сумою wei в `msg.value`. + +```solidity + require(success, "TransferHelper::safeTransferETH: не вдалося переказати ETH"); + + // slither-disable-next-line reentrancy-events + emit ETHWithdrawalFinalized(_from, _to, _amount, _data); +``` + +Випромінюйте подію про виведення коштів. + +```solidity + } + + /** + * @inheritdoc IL1ERC20Bridge + */ + function finalizeERC20Withdrawal( + address _l1Token, + address _l2Token, + address _from, + address _to, + uint256 _amount, + bytes calldata _data + ) external onlyFromCrossDomainAccount(l2TokenBridge) { +``` + +Ця функція подібна до `finalizeETHWithdrawal` вище, з необхідними змінами для токенів ERC-20. + +```solidity + deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] - _amount; +``` + +Оновіть структуру даних `deposits`. + +```solidity + + // Коли виведення фіналізується на L1, міст L1 переказує кошти тому, хто виводить + // slither-disable-next-line reentrancy-events + IERC20(_l1Token).safeTransfer(_to, _amount); + + // slither-disable-next-line reentrancy-events + emit ERC20WithdrawalFinalized(_l1Token, _l2Token, _from, _to, _amount, _data); + } + + + /***************************** + * Тимчасово - міграція ETH * + *****************************/ + + /** + * @dev Додає баланс ETH до облікового запису. Це призначено для того, щоб дозволити міграцію ETH + * зі старого шлюзу на новий. + * ПРИМІТКА: Це залишено лише для одного оновлення, щоб ми могли отримати мігрований ETH зі + * старого контракту + */ + function donateETH() external payable {} +} +``` + +Існувала рання реалізація мосту. +Коли ми переходили від тієї реалізації до цієї, нам довелося перемістити всі активи. +Токени ERC-20 можна просто перемістити. +Однак для переказу ETH до контракту потрібне схвалення цього контракту, що нам і надає `donateETH`. + +## Токени ERC-20 на L2 {#erc-20-tokens-on-l2} + +Щоб токен ERC-20 підходив до стандартного мосту, він має дозволяти стандартному мосту, і _лише_ стандартному мосту, карбувати токен. +Це необхідно, оскільки мости повинні гарантувати, що кількість токенів, що обертаються на Optimism, дорівнює кількості токенів, заблокованих у контракті мосту L1. +Якщо на L2 буде занадто багато токенів, деякі користувачі не зможуть переказати свої активи назад на L1 через міст. +Замість надійного мосту ми, по суті, відтворимо [банківську систему з частковим резервуванням](https://www.investopedia.com/terms/f/fractionalreservebanking.asp). +Якщо на L1 забагато токенів, деякі з них назавжди залишаться заблокованими в бридж-контракті, оскільки їх неможливо звільнити, не спаливши токени L2. + +### IL2StandardERC20 {#il2standarderc20} + +Кожен токен ERC-20 на L2, який використовує стандартний міст, повинен надавати [цей інтерфейс](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts/contracts/standards/IL2StandardERC20.sol), який має функції та події, необхідні стандартному мосту. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +``` + +[Стандартний інтерфейс ERC-20](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/IERC20.sol) не включає функції `mint` та `burn`. +Ці методи не вимагаються [стандартом ERC-20](https://eips.ethereum.org/EIPS/eip-20), який не визначає механізми створення та знищення токенів. + +```solidity +import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +``` + +[Інтерфейс ERC-165](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/introspection/IERC165.sol) використовується для визначення того, які функції надає контракт. +[Стандарт можна прочитати тут](https://eips.ethereum.org/EIPS/eip-165). + +```solidity +interface IL2StandardERC20 is IERC20, IERC165 { + function l1Token() external returns (address); +``` + +Ця функція надає адресу токена L1, який переказується на цей контракт через міст. +Зауважте, що у нас немає подібної функції у зворотному напрямку. +Нам потрібно мати можливість переказувати будь-який токен L1 через міст, незалежно від того, чи планувалася підтримка L2 під час його впровадження. + +```solidity + + function mint(address _to, uint256 _amount) external; + + function burn(address _from, uint256 _amount) external; + + event Mint(address indexed _account, uint256 _amount); + event Burn(address indexed _account, uint256 _amount); +} +``` + +Функції та події для карбування (створення) і спалювання (знищення) токенів. +Міст має бути єдиною сутністю, яка може запускати ці функції, щоб переконатися, що кількість токенів правильна (дорівнює кількості токенів, заблокованих на L1). + +### L2StandardERC20 {#L2StandardERC20} + +[Це наша реалізація інтерфейсу `IL2StandardERC20`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts/contracts/standards/L2StandardERC20.sol). +Якщо вам не потрібна якась спеціальна логіка, ви повинні використовувати цю. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +``` + +[Контракт ERC-20 від OpenZeppelin](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol). +Optimism не вірить у винахід колеса, особливо коли колесо добре перевірено та має бути достатньо надійним, щоб зберігати активи. + +```solidity +import "./IL2StandardERC20.sol"; + +contract L2StandardERC20 is IL2StandardERC20, ERC20 { + address public l1Token; + address public l2Bridge; +``` + +Це два додаткові параметри конфігурації, які потрібні нам, а ERC-20 зазвичай не вимагає. + +```solidity + + /** + * @param _l2Bridge Адреса стандартного мосту L2. + * @param _l1Token Адреса відповідного токена L1. + * @param _name Назва ERC20. + * @param _symbol Символ ERC20. + */ + constructor( + address _l2Bridge, + address _l1Token, + string memory _name, + string memory _symbol + ) ERC20(_name, _symbol) { + l1Token = _l1Token; + l2Bridge = _l2Bridge; + } +``` + +Спочатку викликаємо конструктор для контракту, від якого ми успадковуємо (`ERC20(_name, _symbol)`), а потім встановлюємо наші власні змінні. + +```solidity + + modifier onlyL2Bridge() { + require(msg.sender == l2Bridge, "Тільки міст L2 може карбувати та спалювати"); + _; + } + + + // slither-disable-next-line external-function + function supportsInterface(bytes4 _interfaceId) public pure returns (bool) { + bytes4 firstSupportedInterface = bytes4(keccak256("supportsInterface(bytes4)")); // ERC165 + bytes4 secondSupportedInterface = IL2StandardERC20.l1Token.selector ^ + IL2StandardERC20.mint.selector ^ + IL2StandardERC20.burn.selector; + return _interfaceId == firstSupportedInterface || _interfaceId == secondSupportedInterface; + } +``` + +Саме так працює [ERC-165](https://eips.ethereum.org/EIPS/eip-165). +Кожен інтерфейс — це набір підтримуваних функцій, що ідентифікується як [виключне АБО](https://en.wikipedia.org/wiki/Exclusive_or) [селекторів функцій ABI](https://docs.soliditylang.org/en/v0.8.12/abi-spec.html#function-selector) цих функцій. + +Міст L2 використовує ERC-165 як перевірку на адекватність, щоб переконатися, що контракт ERC-20, на який він надсилає активи, є `IL2StandardERC20`. + +**Примітка:** ніщо не заважає зловмисному контракту надавати неправдиві відповіді на `supportsInterface`, тому це механізм перевірки на адекватність, а _не_ механізм безпеки. + +```solidity + // slither-disable-next-line external-function + function mint(address _to, uint256 _amount) public virtual onlyL2Bridge { + _mint(_to, _amount); + + emit Mint(_to, _amount); + } + + // slither-disable-next-line external-function + function burn(address _from, uint256 _amount) public virtual onlyL2Bridge { + _burn(_from, _amount); + + emit Burn(_from, _amount); + } +} +``` + +Лише на мосту L2 дозволено карбувати та спалювати активи. + +`_mint` та `_burn` насправді визначені в [контракті ERC-20 від OpenZeppelin](/developers/tutorials/erc20-annotated-code/#the-_mint-and-_burn-functions-_mint-and-_burn). +Цей контракт просто не робить їх доступними ззовні, оскільки умови для карбування та спалювання токенів настільки ж різноманітні, як і кількість способів використання ERC-20. + +## Код мосту L2 {#l2-bridge-code} + +Це код, який запускає міст у Optimism. +[Вихідний код цього контракту тут](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts/contracts/L2/messaging/L2StandardBridge.sol). + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + +/* Імпорт інтерфейсів */ +import { IL1StandardBridge } from "../../L1/messaging/IL1StandardBridge.sol"; +import { IL1ERC20Bridge } from "../../L1/messaging/IL1ERC20Bridge.sol"; +import { IL2ERC20Bridge } from "./IL2ERC20Bridge.sol"; +``` + +Інтерфейс [IL2ERC20Bridge](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts/contracts/L2/messaging/IL2ERC20Bridge.sol) дуже схожий на [еквівалент L1](#IL1ERC20Bridge), який ми бачили вище. +Є дві істотні відмінності: + +1. На L1 ви ініціюєте внесення та фіналізуєте виведення коштів. + Тут ви ініціюєте виведення та фіналізуєте внесення коштів. +2. На L1 необхідно розрізняти токени ETH і ERC-20. + На L2 ми можемо використовувати однакові функції для обох, оскільки внутрішні баланси ETH на Optimism обробляються як токен ERC-20 з адресою [0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000](https://explorer.optimism.io/address/0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000). + +```solidity +/* Імпорт бібліотек */ +import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; +import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol"; +import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployAddresses.sol"; + +/* Імпорт контрактів */ +import { IL2StandardERC20 } from "../../standards/IL2StandardERC20.sol"; + +/** + * @title L2StandardBridge + * @dev Стандартний міст L2 — це контракт, який працює разом зі стандартним мостом L1, щоб + * забезпечити перекази ETH та ERC20 між L1 та L2. + * Цей контракт діє як мінтер для нових токенів, коли він отримує інформацію про депозити в стандартний міст + * L1. + * Цей контракт також діє як спалювач токенів, призначених для виведення, інформуючи міст L1 + * про необхідність вивільнення коштів L1. + */ +contract L2StandardBridge is IL2ERC20Bridge, CrossDomainEnabled { + /******************************** + * Посилання на зовнішні контракти * + ********************************/ + + address public l1TokenBridge; +``` + +Відстежуйте адресу мосту L1. +Зверніть увагу, що на відміну від еквівалента L1, тут нам _потрібна_ ця змінна. +Адреса мосту L1 заздалегідь невідома. + +```solidity + + /*************** + * Конструктор * + ***************/ + + /** + * @param _l2CrossDomainMessenger Міждоменний месенджер, що використовується цим контрактом. + * @param _l1TokenBridge Адреса мосту L1, розгорнутого в основній мережі. + */ + constructor(address _l2CrossDomainMessenger, address _l1TokenBridge) + CrossDomainEnabled(_l2CrossDomainMessenger) + { + l1TokenBridge = _l1TokenBridge; + } + + /*************** + * Виведення * + ***************/ + + /** + * @inheritdoc IL2ERC20Bridge + */ + function withdraw( + address _l2Token, + uint256 _amount, + uint32 _l1Gas, + bytes calldata _data + ) external virtual { + _initiateWithdrawal(_l2Token, msg.sender, msg.sender, _amount, _l1Gas, _data); + } + + /** + * @inheritdoc IL2ERC20Bridge + */ + function withdrawTo( + address _l2Token, + address _to, + uint256 _amount, + uint32 _l1Gas, + bytes calldata _data + ) external virtual { + _initiateWithdrawal(_l2Token, msg.sender, _to, _amount, _l1Gas, _data); + } +``` + +Ці дві функції ініціюють виведення коштів. +Зверніть увагу, що немає необхідності вказувати адресу токена L1. +Очікується, що токени L2 повідомлять нам адресу еквівалента L1. + +```solidity + + /** + * @dev Виконує логіку для виведення, спалюючи токен та інформуючи + * шлюз токенів L1 про виведення. + * @param _l2Token Адреса токена L2, де ініційовано виведення. + * @param _from Обліковий запис, з якого буде взято виведення на L2. + * @param _to Обліковий запис, на який буде зараховано виведення на L1. + * @param _amount Кількість токена для виведення. + * @param _l1Gas Не використовується, але включено для можливих міркувань щодо майбутньої сумісності. + * @param _data Необов’язкові дані для пересилання на L1. Ці дані надаються + * виключно для зручності зовнішніх контрактів. Крім встановлення максимальної + * довжини, ці контракти не дають жодних гарантій щодо їхнього вмісту. + */ + function _initiateWithdrawal( + address _l2Token, + address _from, + address _to, + uint256 _amount, + uint32 _l1Gas, + bytes calldata _data + ) internal { + // Коли виведення ініціюється, ми спалюємо кошти того, хто виводить, щоб запобігти подальшому використанню на L2 + // slither-disable-next-line reentrancy-events + IL2StandardERC20(_l2Token).burn(msg.sender, _amount); +``` + +Зауважте, що ми покладаємося не на параметр `_from`, а на `msg.sender`, який набагато важче підробити (наскільки мені відомо, неможливо). + +```solidity + + // Сконструювати calldata для l1TokenBridge.finalizeERC20Withdrawal(_to, _amount) + // slither-disable-next-line reentrancy-events + address l1Token = IL2StandardERC20(_l2Token).l1Token(); + bytes memory message; + + if (_l2Token == Lib_PredeployAddresses.OVM_ETH) { +``` + +На L1 необхідно розрізняти ETH і ERC-20. + +```solidity + message = abi.encodeWithSelector( + IL1StandardBridge.finalizeETHWithdrawal.selector, + _from, + _to, + _amount, + _data + ); + } else { + message = abi.encodeWithSelector( + IL1ERC20Bridge.finalizeERC20Withdrawal.selector, + l1Token, + _l2Token, + _from, + _to, + _amount, + _data + ); + } + + // Надіслати повідомлення на міст L1 + // slither-disable-next-line reentrancy-events + sendCrossDomainMessage(l1TokenBridge, _l1Gas, message); + + // slither-disable-next-line reentrancy-events + emit WithdrawalInitiated(l1Token, _l2Token, msg.sender, _to, _amount, _data); + } + + /************************************ + * Міжмережева функція: внесення * + ************************************/ + + /** + * @inheritdoc IL2ERC20Bridge + */ + function finalizeDeposit( + address _l1Token, + address _l2Token, + address _from, + address _to, + uint256 _amount, + bytes calldata _data +``` + +Ця функція викликається `L1StandardBridge`. + +```solidity + ) external virtual onlyFromCrossDomainAccount(l1TokenBridge) { +``` + +Переконайтеся, що джерело повідомлення є легітимним. +Це важливо, оскільки ця функція викликає `_mint` і може бути використана для надання токенів, які не забезпечені токенами, якими володіє міст на L1. + +```solidity + // Перевірити, чи є цільовий токен сумісним, і + // перевірити, чи відповідає внесений токен на L1 представленню токена на L2 тут + if ( + // slither-disable-next-line reentrancy-events + ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) && + _l1Token == IL2StandardERC20(_l2Token).l1Token() +``` + +Перевірки на адекватність: + +1. Підтримується правильний інтерфейс +2. Адреса L1 контракту L2 ERC-20 відповідає джерелу L1 токенів + +```solidity + ) { + // Коли депозит фіналізується, ми зараховуємо на рахунок на L2 таку ж кількість + // токенів. + // slither-disable-next-line reentrancy-events + IL2StandardERC20(_l2Token).mint(_to, _amount); + // slither-disable-next-line reentrancy-events + emit DepositFinalized(_l1Token, _l2Token, _from, _to, _amount, _data); +``` + +Якщо перевірки на адекватність проходять, фіналізуйте депозит: + +1. Викарбуйте токени +2. Випромінюйте відповідну подію + +```solidity + } else { + // Або токен L2, на який вноситься депозит, не погоджується з правильною адресою + // свого токена L1, або не підтримує правильний інтерфейс. + // Це має відбуватися лише у випадку зловмисного токена L2, або якщо користувач якимось чином + // вказав неправильну адресу токена L2 для депозиту. + // У будь-якому випадку, ми зупиняємо процес тут і створюємо повідомлення про + // виведення, щоб користувачі могли в деяких випадках повернути свої кошти. + // Немає способу повністю запобігти зловмисним контрактам токенів, але це обмежує + // помилки користувачів і пом'якшує деякі форми зловмисної поведінки контрактів. +``` + +Якщо користувач зробив помилку, яку можна виявити, використавши неправильну адресу токена L2, ми хочемо скасувати депозит і повернути токени на L1. +Єдиний спосіб зробити це з L2 — надіслати повідомлення, яке має чекати періоду оскарження помилки, але це набагато краще для користувача, ніж остаточна втрата токенів. + +```solidity + bytes memory message = abi.encodeWithSelector( + IL1ERC20Bridge.finalizeERC20Withdrawal.selector, + _l1Token, + _l2Token, + _to, // тут поміняли _to та _from, щоб повернути депозит відправнику + _from, + _amount, + _data + ); + + // Надіслати повідомлення на міст L1 + // slither-disable-next-line reentrancy-events + sendCrossDomainMessage(l1TokenBridge, 0, message); + // slither-disable-next-line reentrancy-events + emit DepositFailed(_l1Token, _l2Token, _from, _to, _amount, _data); + } + } +} +``` + +## Висновок {#conclusion} + +Стандартний міст є найбільш гнучким механізмом для переказу активів. +Однак, оскільки він настільки загальний, це не завжди найпростіший механізм для використання. +Особливо для виведення коштів, більшість користувачів вважають за краще використовувати [сторонні мости](https://optimism.io/apps#bridge), які не чекають періоду оскарження і не вимагають доказу Merkle для завершення виведення. + +Ці мости зазвичай працюють, маючи активи на L1, які вони надають негайно за невелику плату (часто меншу, ніж вартість газу для стандартного виведення через міст). +Коли міст (або люди, які ним керують) очікує брак активів L1, він переказує достатню кількість активів з L2. Оскільки це дуже великі виведення, вартість виведення амортизується на велику суму та становить набагато менший відсоток. + +Сподіваюся, ця стаття допомогла вам більше зрозуміти, як працює рівень 2 і як написати чіткий і безпечний код Solidity. + +[Більше моїх робіт дивіться тут](https://cryptodocguy.pro/). diff --git a/public/content/translations/uk/developers/tutorials/reverse-engineering-a-contract/index.md b/public/content/translations/uk/developers/tutorials/reverse-engineering-a-contract/index.md new file mode 100644 index 00000000000..2f1ab0d23d0 --- /dev/null +++ b/public/content/translations/uk/developers/tutorials/reverse-engineering-a-contract/index.md @@ -0,0 +1,744 @@ +--- +title: "Зворотне проектування контракту" +description: "Як зрозуміти контракт, якщо у вас немає вихідного коду" +author: Ori Pomerantz +lang: uk +tags: [ "evm", "коди-операцій" ] +skill: advanced +published: 2021-12-30 +--- + +## Вступ {#introduction} + +_У блокчейні немає секретів_, усе, що відбувається, є послідовним, таким, що піддається перевірці, та загальнодоступним. В ідеалі [вихідний код контрактів має бути опублікований і перевірений на Etherscan](https://etherscan.io/address/0xb8901acb165ed027e32754e0ffe830802919727f#code). Однак [так буває не завжди](https://etherscan.io/address/0x2510c039cc3b061d79e564b38836da87e31b342f#code). У цій статті ви дізнаєтеся, як виконувати зворотне проектування контрактів, розглядаючи контракт без вихідного коду, [`0x2510c039cc3b061d79e564b38836da87e31b342f`](https://etherscan.io/address/0x2510c039cc3b061d79e564b38836da87e31b342f). + +Існують зворотні компілятори, але вони не завжди дають [придатні для використання результати](https://etherscan.io/bytecode-decompiler?a=0x2510c039cc3b061d79e564b38836da87e31b342f). У цій статті ви дізнаєтеся, як вручну виконати зворотне проектування та зрозуміти контракт на основі [кодів операцій](https://github.com/wolflo/evm-opcodes), а також як інтерпретувати результати декомпілятора. + +Щоб зрозуміти цю статтю, ви вже повинні знати основи EVM і бути принаймні трохи знайомими з асемблером EVM. [Ви можете прочитати про ці теми тут](https://medium.com/mycrypto/the-ethereum-virtual-machine-how-does-it-work-9abac2b7c9e). + +## Підготовка виконуваного коду {#prepare-the-executable-code} + +Ви можете отримати коди операцій, перейшовши на Etherscan для контракту, клацнувши вкладку **Контракт**, а потім **Перейти до перегляду кодів операцій**. Ви отримаєте подання, що містить один код операції на рядок. + +![Перегляд коду операції з Etherscan](opcode-view.png) + +Однак, щоб зрозуміти переходи, вам потрібно знати, де в коді розташований кожен код операції. Для цього один зі способів — відкрити Google Таблицю та вставити коди операцій у стовпець C. [Ви можете пропустити наступні кроки, зробивши копію цієї вже підготовленої таблиці](https://docs.google.com/spreadsheets/d/1tKmTJiNjUwHbW64wCKOSJxHjmh0bAUapt6btUYE7kDA/edit?usp=sharing). + +Наступним кроком є отримання правильного розташування коду, щоб ми могли зрозуміти переходи. Ми розмістимо розмір коду операції в стовпці B, а розташування (у шістнадцятковому вигляді) — у стовпці A. Введіть цю функцію в клітинку `B1`, а потім скопіюйте та вставте її для решти стовпця B до кінця коду. Після цього ви можете приховати стовпець B. + +``` +=1+IF(REGEXMATCH(C1,"PUSH"),REGEXEXTRACT(C1,"PUSH(\d+)"),0) +``` + +Спочатку ця функція додає один байт для самого коду операції, а потім шукає `PUSH`. Коди операцій Push є особливими, оскільки їм потрібні додаткові байти для значення, що передається. Якщо код операції — `PUSH`, ми вилучаємо кількість байтів і додаємо її. + +У `A1` вкажіть перше зміщення, нуль. Потім у `A2` вставте цю функцію та знову скопіюйте і вставте її для решти стовпця A: + +``` +=dec2hex(hex2dec(A1)+B1) +``` + +Ця функція потрібна нам, щоб отримати шістнадцяткове значення, оскільки значення, які передаються перед переходами (`JUMP` і `JUMPI`), надаються нам у шістнадцятковому форматі. + +## Точка входу (0x00) {#the-entry-point-0x00} + +Контракти завжди виконуються з першого байта. Це початкова частина коду: + +| Зміщення | Код операції | Стек (після коду операції) | +| -------: | ------------ | ---------------------------------------------- | +| 0 | PUSH1 0x80 | 0x80 | +| 2 | PUSH1 0x40 | 0x40, 0x80 | +| 4 | MSTORE | Пусто | +| 5 | PUSH1 0x04 | 0x04 | +| 7 | CALLDATASIZE | CALLDATASIZE 0x04 | +| 8 | LT | CALLDATASIZE\<4 | +| 9 | PUSH2 0x005e | 0x5E CALLDATASIZE\<4 | +| C | JUMPI | Пусто | + +Цей код робить дві речі: + +1. Записати 0x80 як 32-байтове значення в комірки пам’яті 0x40-0x5F (0x80 зберігається в 0x5F, а 0x40-0x5E — усі нулі). +2. Прочитати розмір даних виклику. Зазвичай дані виклику для контракту Ethereum відповідають [ABI (двійковому інтерфейсу програми)](https://docs.soliditylang.org/en/v0.8.10/abi-spec.html), для якого потрібно щонайменше чотири байти для селектора функцій. Якщо розмір даних виклику менший за чотири, перейти до 0x5E. + +![Блок-схема для цієї частини](flowchart-entry.png) + +### Обробник за адресою 0x5E (для даних виклику, що не є ABI) {#the-handler-at-0x5e-for-non-abi-call-data} + +| Зміщення | Код операції | +| -------: | ------------ | +| 5E | JUMPDEST | +| 5F | CALLDATASIZE | +| 60 | PUSH2 0x007c | +| 63 | JUMPI | + +Цей фрагмент починається з `JUMPDEST`. Програми EVM (віртуальної машини Ethereum) генерують виняток, якщо ви переходите до коду операції, який не є `JUMPDEST`. Потім він перевіряє CALLDATASIZE і, якщо значення «істинне» (тобто не нуль), переходить до 0x7C. Ми розглянемо це нижче. + +| Зміщення | Код операції | Стек (після коду операції) | +| -------: | ------------ | ------------------------------------------------------------------------------------------- | +| 64 | CALLVALUE | [wei](/glossary/#wei), надані викликом. Викликається `msg.value` у Solidity | +| 65 | PUSH1 0x06 | 6 CALLVALUE | +| 67 | PUSH1 0x00 | 0 6 CALLVALUE | +| 69 | DUP3 | CALLVALUE 0 6 CALLVALUE | +| 6A | DUP3 | 6 CALLVALUE 0 6 CALLVALUE | +| 6B | SLOAD | Storage[6] CALLVALUE 0 6 CALLVALUE | + +Отже, якщо немає даних виклику, ми читаємо значення Storage[6]. Ми ще не знаємо, що це за значення, але можемо знайти транзакції, які контракт отримував без даних виклику. Транзакції, які просто передають ETH без будь-яких даних виклику (і, отже, без методу), мають на Etherscan метод `Transfer`. Фактично, [найперша транзакція, отримана контрактом](https://etherscan.io/tx/0xeec75287a583c36bcc7ca87685ab41603494516a0f5986d18de96c8e630762e7), є переказом. + +Якщо ми заглянемо в цю транзакцію і натиснемо **Клацніть, щоб побачити більше**, ми побачимо, що дані виклику, що називаються вхідними даними, дійсно порожні (`0x`). Зверніть увагу також, що значення становить 1,559 ETH, це буде важливо пізніше. + +![Дані виклику порожні](calldata-empty.png) + +Далі перейдіть на вкладку **Стан** і розгорніть контракт, для якого ми робимо зворотне проектування (0x2510...). Ви можете побачити, що `Storage[6]` змінилося під час транзакції, і якщо ви зміните Hex на **Число**, ви побачите, що воно стало 1 559 000 000 000 000 000, що є значенням, переданим у wei (я додав коми для ясності), що відповідає наступному значенню контракту. + +![Зміна в Storage[6]](storage6.png) + +Якщо ми подивимося на зміни стану, викликані [іншими транзакціями `Transfer` за той самий період](https://etherscan.io/tx/0xf708d306de39c422472f43cb975d97b66fd5d6a6863db627067167cbf93d84d1#statechange), ми побачимо, що `Storage[6]` певний час відстежував вартість контракту. Поки що назвемо це `Value*`. Зірочка (`*`) нагадує нам, що ми ще не _знаємо_, що робить ця змінна, але вона не може просто відстежувати вартість контракту, оскільки немає потреби використовувати сховище, що дуже дорого, коли ви можете отримати баланс свого облікового запису за допомогою `ADDRESS BALANCE`. Перший код операції передає власну адресу контракту. Другий зчитує адресу у верхній частині стека та замінює її балансом цієї адреси. + +| Зміщення | Код операції | Стек | +| -------: | ------------ | ------------------------------------------- | +| 6C | PUSH2 0x0075 | 0x75 Value\* CALLVALUE 0 6 CALLVALUE | +| 6F | SWAP2 | CALLVALUE Value\* 0x75 0 6 CALLVALUE | +| 70 | SWAP1 | Value\* CALLVALUE 0x75 0 6 CALLVALUE | +| 71 | PUSH2 0x01a7 | 0x01A7 Value\* CALLVALUE 0x75 0 6 CALLVALUE | +| 74 | JUMP | | + +Ми продовжимо відстежувати цей код у місці призначення переходу. + +| Зміщення | Код операції | Стек | +| -------: | ------------ | ----------------------------------------------------------- | +| 1A7 | JUMPDEST | Value\* CALLVALUE 0x75 0 6 CALLVALUE | +| 1A8 | PUSH1 0x00 | 0x00 Value\* CALLVALUE 0x75 0 6 CALLVALUE | +| 1AA | DUP3 | CALLVALUE 0x00 Value\* CALLVALUE 0x75 0 6 CALLVALUE | +| 1AB | NOT | 2^256-CALLVALUE-1 0x00 Value\* CALLVALUE 0x75 0 6 CALLVALUE | + +`NOT` є побітовим, тому він інвертує значення кожного біта в значенні виклику. + +| Зміщення | Код операції | Стек | +| -------: | ------------ | ------------------------------------------------------------------------------------------------------ | +| 1AC | DUP3 | Value\* 2^256-CALLVALUE-1 0x00 Value\* CALLVALUE 0x75 0 6 CALLVALUE | +| 1AD | GT | Value\*>2^256-CALLVALUE-1 0x00 Value\* CALLVALUE 0x75 0 6 CALLVALUE | +| 1AE | ISZERO | Value\*\<=2^256-CALLVALUE-1 0x00 Value\* CALLVALUE 0x75 0 6 CALLVALUE | +| 1AF | PUSH2 0x01df | 0x01DF Value\*\<=2^256-CALLVALUE-1 0x00 Value\* CALLVALUE 0x75 0 6 CALLVALUE | +| 1B2 | JUMPI | | + +Ми переходимо, якщо `Value*` менше або дорівнює 2^256-CALLVALUE-1. Це схоже на логіку для запобігання переповненню. І дійсно, ми бачимо, що після кількох безглуздих операцій (наприклад, запис у пам’ять, яка незабаром буде видалена) за зміщенням 0x01DE контракт повертається, якщо виявлено переповнення, що є нормальною поведінкою. + +Зверніть увагу, що таке переповнення вкрай малоймовірне, оскільки для цього значення виклику плюс `Value*` має бути порівнянним із 2^256 wei, приблизно 10^59 ETH. [Загальна пропозиція ETH на момент написання статті становить менше двохсот мільйонів](https://etherscan.io/stat/supply). + +| Зміщення | Код операції | Стек | +| -------: | ------------ | ----------------------------------------- | +| 1DF | JUMPDEST | 0x00 Value\* CALLVALUE 0x75 0 6 CALLVALUE | +| 1E0 | POP | Value\* CALLVALUE 0x75 0 6 CALLVALUE | +| 1E1 | ADD | Value\*+CALLVALUE 0x75 0 6 CALLVALUE | +| 1E2 | SWAP1 | 0x75 Value\*+CALLVALUE 0 6 CALLVALUE | +| 1E3 | JUMP | | + +Якщо ми потрапили сюди, отримуємо `Value* + CALLVALUE` і переходимо до зміщення 0x75. + +| Зміщення | Код операції | Стек | +| -------: | ------------ | ------------------------------- | +| 75 | JUMPDEST | Value\*+CALLVALUE 0 6 CALLVALUE | +| 76 | SWAP1 | 0 Value\*+CALLVALUE 6 CALLVALUE | +| 77 | SWAP2 | 6 Value\*+CALLVALUE 0 CALLVALUE | +| 78 | SSTORE | 0 CALLVALUE | + +Якщо ми потрапимо сюди (що вимагає порожніх даних виклику), ми додаємо до `Value*` значення виклику. Це узгоджується з тим, що, як ми кажемо, роблять транзакції `Transfer`. + +| Зміщення | Код операції | +| -------: | ------------ | +| 79 | POP | +| 7A | POP | +| 7B | Зупинка | + +Нарешті, очистіть стек (що не є необхідним) і повідомте про успішне завершення транзакції. + +Підсумовуючи, ось блок-схема для початкового коду. + +![Блок-схема точки входу](flowchart-entry.png) + +## Обробник за адресою 0x7C {#the-handler-at-0x7c} + +Я навмисно не вказав у заголовку, що робить цей обробник. Справа не в тому, щоб навчити вас, як працює цей конкретний контракт, а в тому, як виконувати зворотне проектування контрактів. Ви дізнаєтеся, що він робить так само, як і я, — дотримуючись коду. + +Ми потрапляємо сюди з кількох місць: + +- Якщо є дані виклику розміром 1, 2 або 3 байти (зі зміщення 0x63) +- Якщо підпис методу невідомий (зі зміщень 0x42 і 0x5D) + +| Зміщення | Код операції | Стек | +| -------: | ------------ | ------------------------------------------------------------------------ | +| 7C | JUMPDEST | | +| 7D | PUSH1 0x00 | 0x00 | +| 7F | PUSH2 0x009d | 0x9D 0x00 | +| 82 | PUSH1 0x03 | 0x03 0x9D 0x00 | +| 84 | SLOAD | Storage[3] 0x9D 0x00 | + +Це ще одна комірка сховища, яку я не зміг знайти в жодній транзакції, тому важче зрозуміти, що вона означає. Код нижче зробить це зрозумілішим. + +| Зміщення | Код операції | Стек | +| -------: | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| 85 | PUSH20 0xffffffffffffffffffffffffffffffffffffffff | 0xff....ff Storage[3] 0x9D 0x00 | +| 9A | AND | Storage[3]-as-address 0x9D 0x00 | + +Ці коди операцій скорочують значення, яке ми зчитуємо зі Storage[3], до 160 біт, що відповідає довжині адреси Ethereum. + +| Зміщення | Код операції | Стек | +| -------: | ------------ | ----------------------------------------------------------------------------------- | +| 9B | SWAP1 | 0x9D Storage[3]-as-address 0x00 | +| 9C | JUMP | Storage[3]-as-address 0x00 | + +Цей перехід зайвий, оскільки ми переходимо до наступного коду операції. Цей код не настільки ефективний за використанням газу, як міг би бути. + +| Зміщення | Код операції | Стек | +| -------: | ------------ | --------------------------------------------------------------------------------------------------------------------------------------- | +| 9D | JUMPDEST | Storage[3]-as-address 0x00 | +| 9E | SWAP1 | 0x00 Storage[3]-as-address | +| 9F | POP | Storage[3]-as-address | +| A0 | PUSH1 0x40 | 0x40 Storage[3]-as-address | +| A2 | MLOAD | Mem[0x40] Storage[3]-as-address | + +На самому початку коду ми встановили Mem[0x40] у 0x80. Якщо ми пізніше подивимося на 0x40, ми побачимо, що ми його не змінюємо — тож можемо припустити, що це 0x80. + +| Зміщення | Код операції | Стек | +| -------: | ------------ | ----------------------------------------------------------------------------------------------------- | +| A3 | CALLDATASIZE | CALLDATASIZE 0x80 Storage[3]-as-address | +| A4 | PUSH1 0x00 | 0x00 CALLDATASIZE 0x80 Storage[3]-as-address | +| A6 | DUP3 | 0x80 0x00 CALLDATASIZE 0x80 Storage[3]-as-address | +| A7 | CALLDATACOPY | 0x80 Storage[3]-as-address | + +Скопіювати всі дані виклику в пам'ять, починаючи з 0x80. + +| Зміщення | Код операції | Стек | +| -------: | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A8 | PUSH1 0x00 | 0x00 0x80 Storage[3]-as-address | +| AA | DUP1 | 0x00 0x00 0x80 Storage[3]-as-address | +| AB | CALLDATASIZE | CALLDATASIZE 0x00 0x00 0x80 Storage[3]-as-address | +| AC | DUP4 | 0x80 CALLDATASIZE 0x00 0x00 0x80 Storage[3]-as-address | +| AD | DUP6 | Storage[3]-as-address 0x80 CALLDATASIZE 0x00 0x00 0x80 Storage[3]-as-address | +| AE | GAS | GAS Storage[3]-as-address 0x80 CALLDATASIZE 0x00 0x00 0x80 Storage[3]-as-address | +| AF | DELEGATE_CALL | | + +Тепер все стало набагато зрозуміліше. Цей контракт може діяти як [проксі](https://blog.openzeppelin.com/proxy-patterns/), викликаючи адресу в Storage[3] для виконання реальної роботи. `DELEGATE_CALL` викликає окремий контракт, але залишається в тому ж сховищі. Це означає, що делегований контракт, для якого ми є проксі, має доступ до того самого простору сховища. Параметри виклику такі: + +- _Газ_: Весь залишковий газ +- _Адреса виклику_: Storage[3]-as-address +- _Дані виклику_: байти CALLDATASIZE, що починаються з 0x80, де ми розмістили початкові дані виклику +- _Дані, що повертаються_: Немає (0x00 - 0x00). Ми отримаємо дані, що повертаються, іншими способами (див. нижче). + +| Зміщення | Код операції | Стек | +| -------: | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| B0 | RETURNDATASIZE | RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address | +| B1 | DUP1 | RETURNDATASIZE RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address | +| B2 | PUSH1 0x00 | 0x00 RETURNDATASIZE RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address | +| B4 | DUP5 | 0x80 0x00 RETURNDATASIZE RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address | +| B5 | RETURNDATACOPY | RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address | + +Тут ми копіюємо всі дані, що повертаються, до буфера пам'яті, починаючи з 0x80. + +| Зміщення | Код операції | Стек | +| -------: | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| B6 | DUP2 | (((call success/failure))) RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address | +| B7 | DUP1 | (((call success/failure))) (((call success/failure))) RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address | +| B8 | ISZERO | (((did the call fail))) (((call success/failure))) RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address | +| B9 | PUSH2 0x00c0 | 0xC0 (((did the call fail))) (((call success/failure))) RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address | +| BC | JUMPI | (((call success/failure))) RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address | +| BD | DUP2 | RETURNDATASIZE (((call success/failure))) RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address | +| BE | DUP5 | 0x80 RETURNDATASIZE (((call success/failure))) RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address | +| BF | Повернення | | + +Отже, після виклику ми копіюємо дані, що повертаються, у буфер 0x80 - 0x80+RETURNDATASIZE, і якщо виклик успішний, ми потім виконуємо `RETURN` з цим самим буфером. + +### Помилка DELEGATECALL {#delegatecall-failed} + +Якщо ми потрапляємо сюди, до 0xC0, це означає, що викликаний нами контракт скасувався. Оскільки ми просто проксі для цього контракту, ми хочемо повернути ті самі дані та також скасувати транзакцію. + +| Зміщення | Код операції | Стек | +| -------: | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| C0 | JUMPDEST | (((call success/failure))) RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address | +| C1 | DUP2 | RETURNDATASIZE (((call success/failure))) RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address | +| C2 | DUP5 | 0x80 RETURNDATASIZE (((call success/failure))) RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address | +| C3 | Повернення | | + +Отже, ми виконуємо `REVERT` із тим самим буфером, який ми використовували для `RETURN` раніше: 0x80 - 0x80+RETURNDATASIZE + +![Блок-схема виклику проксі](flowchart-proxy.png) + +## Виклики ABI {#abi-calls} + +Якщо розмір даних виклику становить чотири байти або більше, це може бути дійсний виклик ABI. + +| Зміщення | Код операції | Стек | +| -------: | ------------ | -------------------------------------------------------------------------------------------------------------------------- | +| D | PUSH1 0x00 | 0x00 | +| F | CALLDATALOAD | (((Перше слово (256 біт) даних виклику))) | +| 10 | PUSH1 0xe0 | 0xE0 (((Перше слово (256 біт) даних виклику))) | +| 12 | SHR | (((перші 32 біти (4 байти) даних виклику))) | + +Etherscan повідомляє нам, що `1C` — це невідомий код операції, тому що [його було додано після того, як Etherscan розробив цю функцію](https://eips.ethereum.org/EIPS/eip-145), і вони його не оновили. [Оновлена таблиця кодів операцій](https://github.com/wolflo/evm-opcodes) показує нам, що це зсув вправо + +| Зміщення | Код операції | Стек | +| -------: | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 13 | DUP1 | (((перші 32 біти (4 байти) даних виклику))) (((перші 32 біти (4 байти) даних виклику))) | +| 14 | PUSH4 0x3cd8045e | 0x3CD8045E (((перші 32 біти (4 байти) даних виклику))) (((перші 32 біти (4 байти) даних виклику))) | +| 19 | GT | 0x3CD8045E>first-32-bits-of-the-call-data (((перші 32 біти (4 байти) даних виклику))) | +| 1A | PUSH2 0x0043 | 0x43 0x3CD8045E>first-32-bits-of-the-call-data (((перші 32 біти (4 байти) даних виклику))) | +| 1D | JUMPI | (((перші 32 біти (4 байти) даних виклику))) | + +Розділення тестів на відповідність підпису методу на дві частини таким чином заощаджує в середньому половину тестів. Код, який слідує одразу за цим, і код у 0x43 мають однаковий шаблон: `DUP1` перших 32 біт даних виклику, `PUSH4 (((підпис методу))`, виконайте `EQ` для перевірки на рівність, а потім `JUMPI`, якщо підпис методу збігається. Ось підписи методів, їхні адреси та, якщо відомо, [відповідне визначення методу](https://www.4byte.directory/): + +| Метод | Підпис методу | Зміщення для переходу | +| --------------------------------------------------------------------------------------------------------- | ------------- | --------------------- | +| [splitter()](https://www.4byte.directory/signatures/?bytes4_signature=0x3cd8045e) | 0x3cd8045e | 0x0103 | +| ??? | 0x81e580d3 | 0x0138 | +| [currentWindow()](https://www.4byte.directory/signatures/?bytes4_signature=0xba0bafb4) | 0xba0bafb4 | 0x0158 | +| ??? | 0x1f135823 | 0x00C4 | +| [merkleRoot()](https://www.4byte.directory/signatures/?bytes4_signature=0x2eb4a7ab) | 0x2eb4a7ab | 0x00ED | + +Якщо збіг не знайдено, код переходить до [обробника проксі за адресою 0x7C](#the-handler-at-0x7c) в надії, що контракт, для якого ми є проксі, має збіг. + +![Блок-схема викликів ABI](flowchart-abi.png) + +## splitter() {#splitter} + +| Зміщення | Код операції | Стек | +| -------: | ------------ | ----------------------------- | +| 103 | JUMPDEST | | +| 104 | CALLVALUE | CALLVALUE | +| 105 | DUP1 | CALLVALUE CALLVALUE | +| 106 | ISZERO | CALLVALUE==0 CALLVALUE | +| 107 | PUSH2 0x010f | 0x010F CALLVALUE==0 CALLVALUE | +| 10A | JUMPI | CALLVALUE | +| 10B | PUSH1 0x00 | 0x00 CALLVALUE | +| 10D | DUP1 | 0x00 0x00 CALLVALUE | +| 10E | Повернення | | + +Перше, що робить ця функція, — це перевіряє, що під час виклику не було надіслано ETH. Ця функція не є [`payable`](https://solidity-by-example.org/payable/). Якщо хтось надіслав нам ETH, це, мабуть, помилка, і ми хочемо виконати `REVERT`, щоб уникнути ситуації, коли цей ETH опиниться там, де його неможливо буде повернути. + +| Зміщення | Код операції | Стек | +| -------: | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 10F | JUMPDEST | | +| 110 | POP | | +| 111 | PUSH1 0x03 | 0x03 | +| 113 | SLOAD | (((Storage[3] або контракт, для якого ми є проксі))) | +| 114 | PUSH1 0x40 | 0x40 (((Storage[3] або контракт, для якого ми є проксі))) | +| 116 | MLOAD | 0x80 (((Storage[3] або контракт, для якого ми є проксі))) | +| 117 | PUSH20 0xffffffffffffffffffffffffffffffffffffffff | 0xFF...FF 0x80 (((Storage[3] або контракт, для якого ми є проксі))) | +| 12C | SWAP1 | 0x80 0xFF...FF (((Storage[3] або контракт, для якого ми є проксі))) | +| 12D | SWAP2 | (((Storage[3] або контракт, для якого ми є проксі))) 0xFF...FF 0x80 | +| 12E | AND | ProxyAddr 0x80 | +| 12F | DUP2 | 0x80 ProxyAddr 0x80 | +| 130 | MSTORE | 0x80 | + +І тепер 0x80 містить адресу проксі + +| Зміщення | Код операції | Стек | +| -------: | ------------ | --------- | +| 131 | PUSH1 0x20 | 0x20 0x80 | +| 133 | ADD | 0xA0 | +| 134 | PUSH2 0x00e4 | 0xE4 0xA0 | +| 137 | JUMP | 0xA0 | + +### Код E4 {#the-e4-code} + +Ми бачимо ці рядки вперше, але вони спільні для інших методів (див. нижче). Тож ми назвемо значення в стеку X і просто пам’ятатимемо, що в `splitter()` значення цього X дорівнює 0xA0. + +| Зміщення | Код операції | Стек | +| -------: | ------------ | ----------- | +| E4 | JUMPDEST | X | +| E5 | PUSH1 0x40 | 0x40 X | +| E7 | MLOAD | 0x80 X | +| E8 | DUP1 | 0x80 0x80 X | +| E9 | SWAP2 | X 0x80 0x80 | +| EA | SUB | X-0x80 0x80 | +| EB | SWAP1 | 0x80 X-0x80 | +| EC | Повернення | | + +Отже, цей код отримує вказівник на пам’ять у стеку (X) і змушує контракт виконати `RETURN` з буфером, який є 0x80 - X. + +У випадку `splitter()` він повертає адресу, для якої ми є проксі. `RETURN` повертає буфер у 0x80-0x9F, куди ми записали ці дані (зміщення 0x130 вище). + +## currentWindow() {#currentwindow} + +Код у зміщеннях 0x158-0x163 ідентичний тому, що ми бачили в 0x103-0x10E у `splitter()` (крім призначення `JUMPI`), тож ми знаємо, що `currentWindow()` також не є `payable`. + +| Зміщення | Код операції | Стек | +| -------: | ------------ | ------------------------------------------------------------------------ | +| 164 | JUMPDEST | | +| 165 | POP | | +| 166 | PUSH2 0x00da | 0xDA | +| 169 | PUSH1 0x01 | 0x01 0xDA | +| 16B | SLOAD | Storage[1] 0xDA | +| 16C | DUP2 | 0xDA Storage[1] 0xDA | +| 16D | JUMP | Storage[1] 0xDA | + +### Код DA {#the-da-code} + +Цей код також є спільним для інших методів. Тож ми назвемо значення в стеку Y і просто пам’ятатимемо, що в `currentWindow()` значення цього Y є Storage[1]. + +| Зміщення | Код операції | Стек | +| -------: | ------------ | ---------------- | +| DA | JUMPDEST | Y 0xDA | +| DB | PUSH1 0x40 | 0x40 Y 0xDA | +| DD | MLOAD | 0x80 Y 0xDA | +| DE | SWAP1 | Y 0x80 0xDA | +| DF | DUP2 | 0x80 Y 0x80 0xDA | +| E0 | MSTORE | 0x80 0xDA | + +Записати Y до 0x80-0x9F. + +| Зміщення | Код операції | Стек | +| -------: | ------------ | -------------- | +| E1 | PUSH1 0x20 | 0x20 0x80 0xDA | +| E3 | ADD | 0xA0 0xDA | + +А решту вже пояснено [вище](#the-e4-code). Отже, переходи до 0xDA записують верхнє значення стека (Y) до 0x80-0x9F і повертають це значення. У випадку `currentWindow()` він повертає Storage[1]. + +## merkleRoot() {#merkleroot} + +Код у зміщеннях 0xED-0xF8 ідентичний тому, що ми бачили в 0x103-0x10E у `splitter()` (крім призначення `JUMPI`), тож ми знаємо, що `merkleRoot()` також не є `payable`. + +| Зміщення | Код операції | Стек | +| -------: | ------------ | ------------------------------------------------------------------------ | +| F9 | JUMPDEST | | +| FA | POP | | +| FB | PUSH2 0x00da | 0xDA | +| FE | PUSH1 0x00 | 0x00 0xDA | +| 100 | SLOAD | Storage[0] 0xDA | +| 101 | DUP2 | 0xDA Storage[0] 0xDA | +| 102 | JUMP | Storage[0] 0xDA | + +Що відбувається після переходу, [ми вже з’ясували](#the-da-code). Отже, `merkleRoot()` повертає Storage[0]. + +## 0x81e580d3 {#0x81e580d3} + +Код у зміщеннях 0x138-0x143 ідентичний тому, що ми бачили в 0x103-0x10E у `splitter()` (крім призначення `JUMPI`), тож ми знаємо, що ця функція також не є `payable`. + +| Зміщення | Код операції | Стек | +| -------: | ------------ | ------------------------------------------------------------------------------- | +| 144 | JUMPDEST | | +| 145 | POP | | +| 146 | PUSH2 0x00da | 0xDA | +| 149 | PUSH2 0x0153 | 0x0153 0xDA | +| 14C | CALLDATASIZE | CALLDATASIZE 0x0153 0xDA | +| 14D | PUSH1 0x04 | 0x04 CALLDATASIZE 0x0153 0xDA | +| 14F | PUSH2 0x018f | 0x018F 0x04 CALLDATASIZE 0x0153 0xDA | +| 152 | JUMP | 0x04 CALLDATASIZE 0x0153 0xDA | +| 18F | JUMPDEST | 0x04 CALLDATASIZE 0x0153 0xDA | +| 190 | PUSH1 0x00 | 0x00 0x04 CALLDATASIZE 0x0153 0xDA | +| 192 | PUSH1 0x20 | 0x20 0x00 0x04 CALLDATASIZE 0x0153 0xDA | +| 194 | DUP3 | 0x04 0x20 0x00 0x04 CALLDATASIZE 0x0153 0xDA | +| 195 | DUP5 | CALLDATASIZE 0x04 0x20 0x00 0x04 CALLDATASIZE 0x0153 0xDA | +| 196 | SUB | CALLDATASIZE-4 0x20 0x00 0x04 CALLDATASIZE 0x0153 0xDA | +| 197 | SLT | CALLDATASIZE-4\<32 0x00 0x04 CALLDATASIZE 0x0153 0xDA | +| 198 | ISZERO | CALLDATASIZE-4>=32 0x00 0x04 CALLDATASIZE 0x0153 0xDA | +| 199 | PUSH2 0x01a0 | 0x01A0 CALLDATASIZE-4>=32 0x00 0x04 CALLDATASIZE 0x0153 0xDA | +| 19C | JUMPI | 0x00 0x04 CALLDATASIZE 0x0153 0xDA | + +Схоже, що ця функція приймає щонайменше 32 байти (одне слово) даних виклику. + +| Зміщення | Код операції | Стек | +| -------: | ------------ | -------------------------------------------- | +| 19D | DUP1 | 0x00 0x00 0x04 CALLDATASIZE 0x0153 0xDA | +| 19E | DUP2 | 0x00 0x00 0x00 0x04 CALLDATASIZE 0x0153 0xDA | +| 19F | Повернення | | + +Якщо дані виклику не отримані, транзакція скасовується без повернення даних. + +Подивимося, що станеться, якщо функція _отримає_ потрібні їй дані виклику. + +| Зміщення | Код операції | Стек | +| -------: | ------------ | ----------------------------------------------------------- | +| 1A0 | JUMPDEST | 0x00 0x04 CALLDATASIZE 0x0153 0xDA | +| 1A1 | POP | 0x04 CALLDATASIZE 0x0153 0xDA | +| 1A2 | CALLDATALOAD | calldataload(4) CALLDATASIZE 0x0153 0xDA | + +`calldataload(4)` — це перше слово даних виклику _після_ підпису методу + +| Зміщення | Код операції | Стек | +| -------: | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 1A3 | SWAP2 | 0x0153 CALLDATASIZE calldataload(4) 0xDA | +| 1A4 | SWAP1 | CALLDATASIZE 0x0153 calldataload(4) 0xDA | +| 1A5 | POP | 0x0153 calldataload(4) 0xDA | +| 1A6 | JUMP | calldataload(4) 0xDA | +| 153 | JUMPDEST | calldataload(4) 0xDA | +| 154 | PUSH2 0x016e | 0x016E calldataload(4) 0xDA | +| 157 | JUMP | calldataload(4) 0xDA | +| 16E | JUMPDEST | calldataload(4) 0xDA | +| 16F | PUSH1 0x04 | 0x04 calldataload(4) 0xDA | +| 171 | DUP2 | calldataload(4) 0x04 calldataload(4) 0xDA | +| 172 | DUP2 | 0x04 calldataload(4) 0x04 calldataload(4) 0xDA | +| 173 | SLOAD | Storage[4] calldataload(4) 0x04 calldataload(4) 0xDA | +| 174 | DUP2 | calldataload(4) Storage[4] calldataload(4) 0x04 calldataload(4) 0xDA | +| 175 | LT | calldataload(4)\)`, а інший — `isClaimed()`, тож це виглядає як контракт для аірдропу. Замість того, щоб розбирати решту код за кодом, ми можемо [спробувати декомпілятор](https://etherscan.io/bytecode-decompiler?a=0x2f81e57ff4f4d83b40a9f719fd892d8e806e0761), який дає придатні для використання результати для трьох функцій з цього контракту. Зворотне проектування інших залишається як вправа для читача. + +### scaleAmountByPercentage {#scaleamountbypercentage} + +Ось що дає нам декомпілятор для цієї функції: + +```python +def unknown8ffb5c97(uint256 _param1, uint256 _param2) payable: + require calldata.size - 4 >=′ 64 + if _param1 and _param2 > -1 / _param1: + revert with 0, 17 + return (_param1 * _param2 / 100 * 10^6) +``` + +Перша вимога `require` перевіряє, що дані виклику, крім чотирьох байтів підпису функції, мають щонайменше 64 байти, що достатньо для двох параметрів. Якщо ні, то, очевидно, щось не так. + +Оператор `if`, схоже, перевіряє, що `_param1` не є нулем, і що `_param1 * _param2` не є від’ємним. Це, ймовірно, для запобігання випадкам переповнення. + +Нарешті, функція повертає масштабоване значення. + +### claim {#claim} + +Код, який створює декомпілятор, є складним, і не весь він має для нас значення. Я пропущу деякі його частини, щоб зосередитися на рядках, які, на мою думку, надають корисну інформацію. + +```python +def unknown2e7ba6ef(uint256 _param1, uint256 _param2, uint256 _param3, array _param4) payable: + ... + require _param2 == addr(_param2) + ... + if currentWindow <= _param1: + revert with 0, 'не можна вимагати для майбутнього вікна' +``` + +Тут ми бачимо дві важливі речі: + +- `_param2`, хоча він і оголошений як `uint256`, насправді є адресою +- `_param1` — це вікно, для якого робиться вимога, і воно має бути `currentWindow` або ранішим. + +```python + ... + if stor5[_claimWindow][addr(_claimFor)]: + revert with 0, 'Обліковий запис уже вимагав для даного вікна' +``` + +Тепер ми знаємо, що Storage[5] — це масив вікон та адрес, і чи отримав обліковий запис винагороду за це вікно. + +```python + ... + idx = 0 + s = 0 + while idx < _param4.length: + ... + if s + sha3(mem[(32 * _param4.length) + 328 len mem[(32 * _param4.length) + 296]]) > mem[(32 * idx) + 296]: + mem[mem[64] + 32] = mem[(32 * idx) + 296] + ... + s = sha3(mem[_62 + 32 len mem[_62]]) + continue + ... + s = sha3(mem[_66 + 32 len mem[_66]]) + continue + if unknown2eb4a7ab != s: + revert with 0, 'Недійсний доказ' +``` + +Ми знаємо, що `unknown2eb4a7ab` насправді є функцією `merkleRoot()`, тому цей код, схоже, перевіряє [доказ Меркла](https://medium.com/crypto-0-nite/merkle-proofs-explained-6dd429623dc5). Це означає, що `_param4` є доказом Меркла. + +```python + call addr(_param2) with: + value unknown81e580d3[_param1] * _param3 / 100 * 10^6 wei + gas 30000 wei +``` + +Ось як контракт передає свій власний ETH на іншу адресу (контракт або зовнішній обліковий запис). Він викликає його зі значенням, яке є сумою, що передається. Отже, схоже, що це аірдроп ETH. + +```python + if not return_data.size: + if not ext_call.success: + require ext_code.size(stor2) + call stor2.deposit() with: + value unknown81e580d3[_param1] * _param3 / 100 * 10^6 wei +``` + +Два нижні рядки говорять нам, що Storage[2] — це також контракт, який ми викликаємо. Якщо ми [подивимося на транзакцію конструктора](https://etherscan.io/tx/0xa1ea0549fb349eb7d3aff90e1d6ce7469fdfdcd59a2fd9b8d1f5e420c0d05b58#statechange), то побачимо, що цей контракт — [0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2](https://etherscan.io/address/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2), контракт Wrapped Ether, [вихідний код якого було завантажено на Etherscan](https://etherscan.io/address/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2#code). + +Отже, схоже, що контракти намагаються надіслати ETH до `_param2`. Якщо це можливо — чудово. Якщо ні, він намагається надіслати [WETH](https://weth.tkn.eth.limo/). Якщо `_param2` є зовнішнім обліковим записом (EOA), він завжди може отримувати ETH, але контракти можуть відмовитися від отримання ETH. Однак WETH є ERC-20, і контракти не можуть відмовитися від його прийняття. + +```python + ... + log 0xdbd5389f: addr(_param2), unknown81e580d3[_param1] * _param3 / 100 * 10^6, bool(ext_call.success) +``` + +Наприкінці функції ми бачимо, що генерується запис у журналі. [Подивіться на згенеровані записи журналу](https://etherscan.io/address/0x2510c039cc3b061d79e564b38836da87e31b342f#events) і відфільтруйте за темою, що починається з `0xdbd5...`. Якщо ми [натиснемо на одну з транзакцій, що згенерували такий запис](https://etherscan.io/tx/0xe7d3b7e00f645af17dfbbd010478ef4af235896c65b6548def1fe95b3b7d2274), ми побачимо, що це справді схоже на вимогу — обліковий запис надіслав повідомлення контракту, для якого ми виконуємо зворотне проектування, і натомість отримав ETH. + +![Транзакція вимоги](claim-tx.png) + +### 1e7df9d3 {#1e7df9d3} + +Ця функція дуже схожа на [`claim`](#claim) вище. Він також перевіряє доказ Меркла, намагається передати ETH на першу адресу та створює той самий тип запису в журналі. + +```python +def unknown1e7df9d3(uint256 _param1, uint256 _param2, array _param3) payable: + ... + idx = 0 + s = 0 + while idx < _param3.length: + if idx >= mem[96]: + revert with 0, 50 + _55 = mem[(32 * idx) + 128] + if s + sha3(mem[(32 * _param3.length) + 160 len mem[(32 * _param3.length) + 128]]) > mem[(32 * idx) + 128]: + ... + s = sha3(mem[_58 + 32 len mem[_58]]) + continue + mem[mem[64] + 32] = s + sha3(mem[(32 * _param3.length) + 160 len mem[(32 * _param3.length) + 128]]) + ... + if unknown2eb4a7ab != s: + revert with 0, 'Недійсний доказ' + ... + call addr(_param1) with: + value s wei + gas 30000 wei + if not return_data.size: + if not ext_call.success: + require ext_code.size(stor2) + call stor2.deposit() with: + value s wei + gas gas_remaining wei + ... + log 0xdbd5389f: addr(_param1), s, bool(ext_call.success) +``` + +Основна відмінність полягає в тому, що перший параметр, вікно для виведення, відсутній. Замість цього є цикл по всіх вікнах, які можна було б заявити. + +```python + idx = 0 + s = 0 + while idx < currentWindow: + ... + if stor5[mem[0]]: + if idx == -1: + revert with 0, 17 + idx = idx + 1 + s = s + continue + ... + stor5[idx][addr(_param1)] = 1 + if idx >= unknown81e580d3.length: + revert with 0, 50 + mem[0] = 4 + if unknown81e580d3[idx] and _param2 > -1 / unknown81e580d3[idx]: + revert with 0, 17 + if s > !(unknown81e580d3[idx] * _param2 / 100 * 10^6): + revert with 0, 17 + if idx == -1: + revert with 0, 17 + idx = idx + 1 + s = s + (unknown81e580d3[idx] * _param2 / 100 * 10^6) + continue +``` + +Отже, це схоже на варіант `claim`, який заявляє всі вікна. + +## Висновок {#conclusion} + +Тепер ви повинні знати, як розуміти контракти, вихідний код яких недоступний, використовуючи або коди операцій, або (коли це працює) декомпілятор. Як видно з обсягу цієї статті, зворотне проектування контракту не є тривіальним, але в системі, де безпека є важливою, це важлива навичка — мати можливість перевіряти, чи контракти працюють, як обіцяно. + +[Більше моїх робіт дивіться тут](https://cryptodocguy.pro/). diff --git a/public/content/translations/uk/developers/tutorials/run-node-raspberry-pi/index.md b/public/content/translations/uk/developers/tutorials/run-node-raspberry-pi/index.md new file mode 100644 index 00000000000..664f3a46caf --- /dev/null +++ b/public/content/translations/uk/developers/tutorials/run-node-raspberry-pi/index.md @@ -0,0 +1,184 @@ +--- +title: "Запустіть вузол Ethereum на Raspberry Pi 4" +description: "Прошийте ваш Raspberry Pi 4, підключіть кабель Ethernet, підключіть SSD-диск і увімкніть пристрій, щоб перетворити Raspberry Pi 4 на повноцінний вузол Ethereum + валідатор" +author: "EthereumOnArm" +tags: [ "клієнти", "шар виконання", "шар консенсусу", "вузли" ] +lang: uk +skill: intermediate +published: 2022-06-10 +source: Ethereum on ARM +sourceUrl: https://ethereum-on-arm-documentation.readthedocs.io/en/latest/ +--- + +**Ethereum on Arm — це спеціальний образ Linux, який може перетворити Raspberry Pi на вузол Ethereum.** + +Щоб використовувати Ethereum on Arm для перетворення Raspberry Pi на вузол Ethereum, рекомендується таке обладнання: + +- Плата Raspberry 4 (модель B 8 ГБ), Odroid M1 або Rock 5B (8 ГБ/16 ГБ ОЗП) +- Карта MicroSD (мінімум 16 ГБ, клас 10) +- SSD-диск USB 3.0 обсягом щонайменше 2 ТБ або SSD-диск у корпусі з перехідником USB-SATA. +- Блок живлення +- Кабель Ethernet +- Переадресація портів (докладнішу інформацію див. у розділі про клієнти) +- Корпус із радіатором і вентилятором +- USB-клавіатура, монітор і кабель HDMI (micro-HDMI) (необов’язково) + +## Навіщо запускати Ethereum на ARM? {#why-run-ethereum-on-arm} + +Плати ARM — це дуже доступні, гнучкі, невеликі комп’ютери. Це чудовий варіант для запуску вузлів Ethereum, тому що їх можна дешево придбати, їх можна налаштувати так, щоб усі їхні ресурси були спрямовані лише на вузол, що робить їх ефективними, вони споживають мало енергії і мають невеликий розмір, тому їх можна непомітно розмістити в будь-якому будинку. Також дуже легко розгорнути вузли, оскільки карту MicroSD Raspberry Pi можна просто прошити готовим образом, не вимагаючи завантаження чи створення програмного забезпечення. + +## Як це працює? {#how-does-it-work} + +Карта пам’яті Raspberry Pi прошивається готовим образом. Цей образ містить усе необхідне для запуску вузла Ethereum. Маючи прошиту карту, користувачеві достатньо лише увімкнути Raspberry Pi. Усі процеси, необхідні для роботи вузла, запускаються автоматично. Це працює, оскільки карта пам’яті містить операційну систему (ОС) на базі Linux, поверх якої автоматично запускаються процеси системного рівня, що перетворюють пристрій на вузол Ethereum. + +Ethereum не можна запустити за допомогою популярної ОС "Raspbian" для Raspberry Pi на базі Linux, тому що Raspbian усе ще використовує 32-розрядну архітектуру, через що користувачі Ethereum стикаються з проблемами пам’яті, а консенсус-клієнти не підтримують 32-розрядні двійкові файли. Щоб вирішити цю проблему, команда Ethereum on Arm перейшла на нативну 64-розрядну ОС під назвою "Armbian". + +**Образи виконують усі необхідні кроки**: від налаштування середовища та форматування SSD-диска до встановлення та запуску програмного забезпечення Ethereum, а також запуску синхронізації блокчейну. + +## Примітка щодо клієнтів виконання та консенсус-клієнтів {#note-on-execution-and-consensus-clients} + +Образ Ethereum on Arm містить готові клієнти виконання та консенсус-клієнти у вигляді служб. Для роботи вузла Ethereum необхідно, щоб обидва клієнти були синхронізовані та запущені. Вам потрібно лише завантажити та прошити образ, а потім запустити служби. В образ попередньо завантажено такі клієнти виконання: + +- Geth +- Nethermind +- Besu + +та такі консенсус-клієнти: + +- Lighthouse +- Nimbus +- Prysm +- Teku + +Вам слід вибрати для запуску по одному клієнту кожного типу — усі клієнти виконання сумісні з усіма консенсус-клієнтами. Якщо ви не виберете клієнта явно, вузол повернеться до налаштувань за замовчуванням — Geth та Lighthouse — і запустить їх автоматично, коли плата увімкнеться. Ви повинні відкрити порт 30303 на вашому маршрутизаторі, щоб Geth міг знаходити піри та підключатися до них. + +## Завантаження образу {#downloading-the-image} + +Образ Ethereum для Raspberry Pi 4 — це образ типу «підключи і працюй», який автоматично встановлює та налаштовує як клієнти виконання, так і консенсус-клієнти, налаштовуючи їх для взаємодії між собою та підключення до мережі Ethereum. Усе, що потрібно зробити користувачеві, — це запустити процеси за допомогою простої команди. + +Завантажте образ Raspberry Pi з [Ethereum on Arm](https://ethereumonarm-my.sharepoint.com/:u:/p/dlosada/Ec_VmUvr80VFjf3RYSU-NzkBmj2JOteDECj8Bibde929Gw?download=1) і перевірте хеш SHA256: + +```sh +# З каталогу, що містить завантажений образ +shasum -a 256 ethonarm_22.04.00.img.zip +# Хеш має вивести: fb497e8f8a7388b62d6e1efbc406b9558bee7ef46ec7e53083630029c117444f +``` + +Зауважте, що образи для плат Rock 5B і Odroid M1 доступні на [сторінці завантажень](https://ethereum-on-arm-documentation.readthedocs.io/en/latest/quick-guide/download-and-install.html) Ethereum-on-Arm. + +## Прошивка MicroSD {#flashing-the-microsd} + +Карту MicroSD, яка використовуватиметься для Raspberry Pi, слід спочатку вставити в настільний комп’ютер або ноутбук, щоб її можна було прошити. Потім, щоб прошити завантажений образ на SD-карту, виконайте такі команди в терміналі: + +```shell +# перевірте назву карти MicroSD +sudo fdisk -l + +>> sdxxx +``` + +Дуже важливо вказати правильну назву, оскільки наступна команда містить `dd`, яка повністю стирає наявний вміст карти перед тим, як записати на неї образ. Щоб продовжити, перейдіть до каталогу, що містить zip-архів з образом: + +```shell +# розпакуйте та прошийте образ +unzip ethonarm_22.04.00.img.zip +sudo dd bs=1M if=ethonarm_22.04.00.img of=/dev/ conv=fdatasync status=progress +``` + +Тепер карта прошита, тому її можна вставити в Raspberry Pi. + +## Запуск вузла {#start-the-node} + +Вставивши SD-карту в Raspberry Pi, підключіть кабель Ethernet і SSD, а потім увімкніть живлення. ОС завантажиться й автоматично почне виконувати попередньо налаштовані завдання, які перетворюють Raspberry Pi на вузол Ethereum, зокрема встановлення та збірку клієнтського програмного забезпечення. Це, ймовірно, займе 10-15 хвилин. + +Після того, як усе буде встановлено та налаштовано, увійдіть у пристрій через ssh-з’єднання або безпосередньо через термінал, якщо до плати підключено монітор і клавіатуру. Для входу використовуйте обліковий запис `ethereum`, оскільки він має дозволи, необхідні для запуску вузла. + +```shell +Користувач: ethereum +Пароль: ethereum +``` + +Клієнт виконання за замовчуванням, Geth, запуститься автоматично. Ви можете перевірити це, переглянувши журнали за допомогою такої команди терміналу: + +```sh +sudo journalctl -u geth -f +``` + +Консенсус-клієнт потрібно запускати явно. Для цього спочатку відкрийте порт 9000 на своєму маршрутизаторі, щоб Lighthouse міг знаходити піри та підключатися до них. Потім увімкніть і запустіть службу Lighthouse: + +```sh +sudo systemctl enable lighthouse-beacon +sudo systemctl start lighthouse-beacon +``` + +Перевірте клієнт за допомогою журналів: + +```sh +sudo journalctl -u lighthouse-beacon +``` + +Зауважте, що консенсус-клієнт синхронізується за кілька хвилин, оскільки він використовує синхронізацію за контрольними точками. Синхронізація клієнта виконання триватиме довше — потенційно кілька годин, і вона не почнеться, доки консенсус-клієнт не завершить синхронізацію (це тому, що клієнту виконання потрібна ціль для синхронізації, яку надає синхронізований консенсус-клієнт). + +Коли служби Geth і Lighthouse запущені та синхронізовані, ваш Raspberry Pi стає вузлом Ethereum! Найчастіше з мережею Ethereum взаємодіють за допомогою Javascript-консолі Geth, яку можна підключити до клієнта Geth через порт 8545. Також можна надсилати команди, відформатовані як об’єкти JSON, за допомогою інструменту для запитів, наприклад Curl. Докладніше див. у [документації Geth](https://geth.ethereum.org/). + +Geth попередньо налаштований для передачі показників на панель Grafana, яку можна переглянути в браузері. Досвідченіші користувачі можуть захотіти використовувати цю функцію для моніторингу працездатності свого вузла, перейшовши за адресою `ipaddress:3000` і ввівши `користувач: admin` та `пароль: ethereum`. + +## Валідатори {#validators} + +До консенсус-клієнта також можна за бажанням додати валідатор. Програмне забезпечення валідатора дозволяє вашому вузлу брати активну участь у досягненні консенсусу і забезпечує мережі криптоекономічну безпеку. За цю роботу ви отримуєте винагороду в ETH. Щоб запустити валідатор, ви повинні спочатку мати 32 ETH, які необхідно внести на депозитний контракт. Депозит можна зробити, дотримуючись покрокової інструкції на [Launchpad](https://launchpad.ethereum.org/). Зробіть це на настільному комп’ютері/ноутбуці, але не генеруйте ключі — це можна зробити безпосередньо на Raspberry Pi. + +Відкрийте термінал на Raspberry Pi і виконайте таку команду, щоб згенерувати ключі для депозиту: + +``` +sudo apt-get update +sudo apt-get install staking-deposit-cli +cd && deposit new-mnemonic --num_validators 1 +``` + +(Або завантажте [staking-deposit-cli](https://github.com/ethereum/staking-deposit-cli) для запуску на ізольованому від мережі комп’ютері та виконайте команду `deposit new-mnemnonic`) + +Зберігайте мнемонічну фразу в безпеці! Наведена вище команда згенерувала два файли у сховищі ключів вузла: ключі валідатора та файл даних депозиту. Дані депозиту потрібно завантажити на Launchpad, тому їх потрібно скопіювати з Raspberry Pi на настільний комп’ютер/ноутбук. Це можна зробити за допомогою ssh-з’єднання або будь-яким іншим способом копіювання/вставки. + +Коли файл даних депозиту стане доступним на комп’ютері, де запущено Launchpad, його можна перетягнути на `+` на екрані Launchpad. Дотримуйтеся вказівок на екрані, щоб надіслати транзакцію на депозитний контракт. + +Повернувшись до Raspberry Pi, можна запустити валідатор. Для цього потрібно імпортувати ключі валідатора, встановити адресу для збору винагород, а потім запустити попередньо налаштований процес валідатора. Наведений нижче приклад стосується Lighthouse — інструкції для інших консенсус-клієнтів доступні в [документації Ethereum on Arm](https://ethereum-on-arm-documentation.readthedocs.io/en/latest/): + +```shell +# імпортуйте ключі валідатора +lighthouse account validator import --directory=/home/ethereum/validator_keys + +# встановіть адресу для винагород +sudo sed -i 's/' /etc/ethereum/lighthouse-validator.conf + +# запустіть валідатор +sudo systemctl start lighthouse-validator +``` + +Вітаємо, тепер у вас є повноцінний вузол і валідатор Ethereum, що працюють на Raspberry Pi! + +## Докладніше {#more-details} + +На цій сторінці наведено огляд того, як налаштувати вузол Geth-Lighthouse і валідатор за допомогою Raspberry Pi. Докладніші інструкції доступні на [вебсайті Ethereum-on-Arm](https://ethereum-on-arm-documentation.readthedocs.io/en/latest/index.html). + +## Будемо вдячні за відгуки {#feedback-appreciated} + +Ми знаємо, що Raspberry Pi має величезну базу користувачів, яка може дуже позитивно вплинути на працездатність мережі Ethereum. +Будь ласка, заглибтеся в деталі цього посібника, спробуйте запустити на тестових мережах, перегляньте GitHub Ethereum on Arm, залишайте відгуки, повідомляйте про проблеми та створюйте pull-запити, а також допомагайте вдосконалювати технологію та документацію! + +## Джерела {#references} + +1. https://ubuntu.com/download/raspberry-pi +2. https://wikipedia.org/wiki/Port_forwarding +3. https://prometheus.io +4. https://grafana.com +5. https://forum.armbian.com/topic/5565-zram-vs-swap/ +6. https://geth.ethereum.org +7. https://nethermind.io +8. https://www.hyperledger.org/projects/besu +9. https://github.com/prysmaticlabs/prysm +10. https://lighthouse.sigmaprime.io +11. https://ethersphere.github.io/swarm-home +12. https://raiden.network +13. https://ipfs.io +14. https://status.im +15. https://vipnode.org diff --git a/public/content/translations/uk/developers/tutorials/scam-token-tricks/index.md b/public/content/translations/uk/developers/tutorials/scam-token-tricks/index.md new file mode 100644 index 00000000000..409720b0931 --- /dev/null +++ b/public/content/translations/uk/developers/tutorials/scam-token-tricks/index.md @@ -0,0 +1,470 @@ +--- +title: "Деякі прийоми, що використовуються в шахрайських токенах, і як їх виявити" +description: "У цьому посібнику ми розберемо шахрайський токен, щоб побачити деякі з прийомів, до яких вдаються шахраї, як вони їх реалізують, і як ми можемо їх виявити." +author: Ori Pomerantz +tags: + [ + "шахрайство", + "мова програмування", + "erc-20", + "javaScript", + "typescript" + ] +skill: intermediate +published: 2023-09-15 +lang: uk +--- + +У цьому посібнику ми розберемо [шахрайський токен](https://etherscan.io/token/0xb047c8032b99841713b8e3872f06cf32beb27b82#code), щоб побачити деякі з прийомів, до яких вдаються шахраї та як вони їх реалізують. Наприкінці посібника ви матимете більш повне уявлення про контракти токенів ERC-20, їхні можливості та чому необхідний скептицизм. Потім ми розглянемо події, що генеруються цим шахрайським токеном, і побачимо, як можна автоматично визначити, що він не є легітимним. + +## Шахрайські токени: що це таке, навіщо їх створюють і як їх уникнути {#scam-tokens} + +Одне з найпоширеніших застосувань Ethereum – це створення групою осіб торгового токена, у певному сенсі – своєї власної валюти. Однак скрізь, де є законні способи використання, що приносять вигоду, завжди з’являються злочинці, які намагаються вкрасти її. + +Ви можете прочитати більше на цю тему [в іншому місці на ethereum.org](/guides/how-to-id-scam-tokens/) з точки зору користувача. Цей посібник присвячено розбору шахрайського токена, щоб побачити, як він створений і як його можна виявити. + +### Як дізнатися, що wARB — це шахрайський токен? {#warb-scam} + +Токен, який ми розбираємо, — це [wARB](https://etherscan.io/token/0xb047c8032b99841713b8e3872f06cf32beb27b82#code), що видає себе за еквівалент легітимного [токена ARB](https://etherscan.io/token/0xb50721bcf8d664c30412cfbc6cf7a15145234ad1). + +Найпростіший спосіб дізнатися, який токен є легітимним, — це подивитися на організацію-засновника, [Arbitrum](https://arbitrum.foundation/). Легітимні адреси вказані [в їхній документації](https://docs.arbitrum.foundation/deployment-addresses#token). + +### Чому вихідний код доступний? {#why-source} + +Зазвичай ми очікуємо, що люди, які намагаються обдурити інших, будуть потайними, і справді, багато шахрайських токенів не мають доступного коду (наприклад, [цей](https://optimistic.etherscan.io/token/0x15992f382d8c46d667b10dc8456dc36651af1452#code) і [цей](https://optimistic.etherscan.io/token/0x026b623eb4aada7de37ef25256854f9235207178#code)). + +Однак легітимні токени зазвичай публікують свій вихідний код, тому, щоб здаватися легітимними, автори шахрайських токенів іноді роблять те саме. [wARB](https://etherscan.io/token/0xb047c8032b99841713b8e3872f06cf32beb27b82#code) — один із тих токенів, вихідний код яких доступний, що полегшує його розуміння. + +Хоча розробники контракту можуть обирати, публікувати вихідний код чи ні, вони _не можуть_ опублікувати неправильний вихідний код. Оглядач блоків незалежно компілює наданий вихідний код, і якщо не отримує точно такий самий байт-код, він відхиляє цей вихідний код. [Ви можете прочитати більше про це на сайті Etherscan](https://etherscan.io/verifyContract). + +## Порівняння з легітимними токенами ERC-20 {#compare-legit-erc20} + +Ми порівняємо цей токен з легітимними токенами ERC-20. Якщо ви не знайомі з тим, як зазвичай пишуться легітимні токени ERC-20, [перегляньте цей посібник](/developers/tutorials/erc20-annotated-code/). + +### Константи для привілейованих адрес {#constants-for-privileged-addresses} + +Контрактам іноді потрібні привілейовані адреси. Контракти, розроблені для довгострокового використання, дозволяють деяким привілейованим адресам змінювати ці адреси, наприклад, щоб увімкнути використання нового контракту з мультипідписом. Існує кілька способів це зробити. + +[Контракт токена `HOP`](https://etherscan.io/address/0xc5102fe9359fd9a28f877a67e36b0f050d81a3cc#code) використовує патерн [`Ownable`](https://docs.openzeppelin.com/contracts/2.x/access-control#ownership-and-ownable). Привілейована адреса зберігається в сховищі, в полі під назвою `_owner` (див. третій файл, `Ownable.sol`). + +```solidity +abstract contract Ownable is Context { + address private _owner; + . + . + . +} +``` + +[Контракт токена `ARB`](https://etherscan.io/address/0xad0c361ef902a7d9851ca7dcc85535da2d3c6fc7#code) не має привілейованої адреси безпосередньо. Однак він йому й не потрібен. Він знаходиться за [`проксі`](https://docs.openzeppelin.com/contracts/5.x/api/proxy) за [адресою `0xb50721bcf8d664c30412cfbc6cf7a15145234ad1`](https://etherscan.io/address/0xb50721bcf8d664c30412cfbc6cf7a15145234ad1#code). Цей контракт має привілейовану адресу (див. четвертий файл, `ERC1967Upgrade.sol`), яка може використовуватися для оновлень. + +```solidity + /** + * @dev Зберігає нову адресу в слоті адміністратора EIP1967. + */ + function _setAdmin(address newAdmin) private { + require(newAdmin != address(0), "ERC1967: новий адміністратор — це нульова адреса"); + StorageSlot.getAddressSlot(_ADMIN_SLOT).value = newAdmin; + } +``` + +Натомість, контракт `wARB` має жорстко закодованого `contract_owner`. + +```solidity +contract WrappedArbitrum is Context, IERC20 { + . + . + . + address deployer = 0xB50721BCf8d664c30412Cfbc6cf7a15145234ad1; + address public contract_owner = 0xb40dE7b1beE84Ff2dc22B70a049A07A13a411A33; + . + . + . +} +``` + +[Цей власник контракту](https://etherscan.io/address/0xb40dE7b1beE84Ff2dc22B70a049A07A13a411A33) — це не контракт, яким могли б керувати різні облікові записи в різний час, а [зовнішній обліковий запис](/developers/docs/accounts/#externally-owned-accounts-and-key-pairs). Це означає, що він, імовірно, розроблений для короткострокового використання окремою особою, а не як довгострокове рішення для контролю ERC-20, що залишатиметься цінним. + +І справді, якщо ми подивимося на Etherscan, то побачимо, що шахрай використовував цей контракт лише 12 годин (від [першої транзакції](https://etherscan.io/tx/0xf49136198c3f925fcb401870a669d43cecb537bde36eb8b41df77f06d5f6fbc2) до [останньої транзакції](https://etherscan.io/tx/0xdfd6e717157354e64bbd5d6adf16761e5a5b3f914b1948d3545d39633244d47b)) 19 травня 2023 року. + +### Підроблена функція `_transfer` {#the-fake-transfer-function} + +Стандартною практикою є виконання фактичних переказів за допомогою [внутрішньої функції `_transfer`](/developers/tutorials/erc20-annotated-code/#the-_transfer-function-_transfer). + +У `wARB` ця функція виглядає майже легітимною: + +```solidity + function _transfer(address sender, address recipient, uint256 amount) internal virtual{ + require(sender != address(0), "ERC20: переказ із нульової адреси"); + require(recipient != address(0), "ERC20: переказ на нульову адресу"); + + _beforeTokenTransfer(sender, recipient, amount); + + _balances[sender] = _balances[sender].sub(amount, "ERC20: сума переказу перевищує баланс"); + _balances[recipient] = _balances[recipient].add(amount); + if (sender == contract_owner){ + sender = deployer; + } + emit Transfer(sender, recipient, amount); + } +``` + +Підозріла частина: + +```solidity + if (sender == contract_owner){ + sender = deployer; + } + emit Transfer(sender, recipient, amount); +``` + +Якщо власник контракту надсилає токени, чому подія `Transfer` показує, що вони надходять від `deployer`? + +Однак є більш важлива проблема. Хто викликає цю функцію `_transfer`? Її не можна викликати ззовні, оскільки вона позначена як `internal`. І код, який у нас є, не містить жодних викликів `_transfer`. Очевидно, що вона тут для відводу очей. + +```solidity + function transfer(address recipient, uint256 amount) public virtual override returns (bool) { + _f_(_msgSender(), recipient, amount); + return true; + } + + function transferFrom(address sender, address recipient, uint256 amount) public virtual override returns (bool) { + _f_(sender, recipient, amount); + _approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount, "ERC20: сума переказу перевищує дозволену")); + return true; + } +``` + +Коли ми дивимося на функції, які викликаються для переказу токенів, `transfer` і `transferFrom`, ми бачимо, що вони викликають зовсім іншу функцію, `_f_`. + +### Справжня функція `_f_` {#the-real-f-function} + +```solidity + function _f_(address sender, address recipient, uint256 amount) internal _mod_(sender,recipient,amount) virtual { + require(sender != address(0), "ERC20: переказ із нульової адреси"); + require(recipient != address(0), "ERC20: переказ на нульову адресу"); + + _beforeTokenTransfer(sender, recipient, amount); + + _balances[sender] = _balances[sender].sub(amount, "ERC20: сума переказу перевищує баланс"); + _balances[recipient] = _balances[recipient].add(amount); + if (sender == contract_owner){ + + sender = deployer; + } + emit Transfer(sender, recipient, amount); + } +``` + +У цій функції є два потенційно підозрілі моменти. + +- Використання [модифікатора функції](https://www.tutorialspoint.com/solidity/solidity_function_modifiers.htm) `_mod_`. Однак, коли ми заглядаємо у вихідний код, ми бачимо, що `_mod_` насправді нешкідливий. + + ```solidity + modifier _mod_(address sender, address recipient, uint256 amount){ + _; + } + ``` + +- Та сама проблема, яку ми бачили в `_transfer`: коли `contract_owner` надсилає токени, вони виглядають так, ніби надходять від `deployer`. + +### Функція підроблених подій `dropNewTokens` {#the-fake-events-function-dropNewTokens} + +Тепер ми підійшли до чогось, що виглядає як справжнє шахрайство. Я трохи відредагував функцію для кращої читабельності, але функціонально вона еквівалентна. + +```solidity +function dropNewTokens(address uPool, + address[] memory eReceiver, + uint256[] memory eAmounts) public auth() +``` + +Ця функція має модифікатор `auth()`, що означає, що її може викликати лише власник контракту. + +```solidity +modifier auth() { + require(msg.sender == contract_owner, "Взаємодія заборонена"); + _; +} +``` + +Це обмеження є цілком логічним, оскільки ми б не хотіли, щоб випадкові облікові записи розповсюджували токени. Однак решта функції є підозрілою. + +```solidity +{ + for (uint256 i = 0; i < eReceiver.length; i++) { + emit Transfer(uPool, eReceiver[i], eAmounts[i]); + } +} +``` + +Функція для переказу з облікового запису пулу масиву отримувачів масиву сум є цілком логічною. Існує багато випадків використання, у яких ви захочете розподілити токени з одного джерела на кілька одержувачів, наприклад, для виплати зарплати, ейрдропів тощо. Це дешевше (за газ) зробити в одній транзакції, замість того, щоб створювати кілька транзакцій або навіть викликати ERC-20 кілька разів з іншого контракту в рамках однієї транзакції. + +Однак, `dropNewTokens` цього не робить. Вона генерує [`події `Transfer`](https://eips.ethereum.org/EIPS/eip-20#transfer-1), але насправді не переказує жодних токенів. Немає жодної легітимної причини вводити в оману позаланцюгові застосунки, повідомляючи їм про переказ, якого насправді не було. + +### Функція `Approve`, що спалює токени {#the-burning-approve-function} + +Контракти ERC-20 повинні мати [функцію `approve`](/developers/tutorials/erc20-annotated-code/#approve) для дозволів, і справді, наш шахрайський токен має таку функцію, і вона навіть правильна. Однак, оскільки Solidity походить від C, він чутливий до регістру. «Approve» та «approve» — це різні рядки. + +Крім того, функціональність не пов'язана з `approve`. + +```solidity + function Approve( + address[] memory holders) +``` + +Ця функція викликається з масивом адрес власників токенів. + +```solidity + public approver() { +``` + +Модифікатор `approver()` гарантує, що лише `contract_owner` може викликати цю функцію (див. нижче). + +```solidity + for (uint256 i = 0; i < holders.length; i++) { + uint256 amount = _balances[holders[i]]; + _beforeTokenTransfer(holders[i], 0x0000000000000000000000000000000000000001, amount); + _balances[holders[i]] = _balances[holders[i]].sub(amount, + "ERC20: спалювана сума перевищує баланс"); + _balances[0x0000000000000000000000000000000000000001] = + _balances[0x0000000000000000000000000000000000000001].add(amount); + } + } + +``` + +Для кожної адреси власника функція переміщує весь баланс власника на адресу `0x00...01`, фактично спалюючи його (справжній `burn` у стандарті також змінює загальну пропозицію та переказує токени на `0x00...00`). Це означає, що `contract_owner` може видаляти активи будь-якого користувача. Це не схоже на функцію, яку ви хотіли б бачити в токені управління. + +### Проблеми з якістю коду {#code-quality-issues} + +Ці проблеми з якістю коду не _доводять_, що цей код є шахрайським, але вони роблять його підозрілим. Організовані компанії, такі як Arbitrum, зазвичай не випускають такий поганий код. + +#### Функція `mount` {#the-mount-function} + +Хоча це не вказано в [стандарті](https://eips.ethereum.org/EIPS/eip-20), загалом кажучи, функція, що створює нові токени, називається [`mint`](https://ethereum.org/el/developers/tutorials/erc20-annotated-code/#the-_mint-and-_burn-functions-_mint-and-_burn). + +Якщо ми подивимося в конструктор `wARB`, ми побачимо, що функцію карбування з якоїсь причини перейменовано на `mount` і викликається п’ять разів з п’ятою частиною початкової пропозиції, замість одного разу для всієї суми для ефективності. + +```solidity + constructor () public { + + _name = "Wrapped Arbitrum"; + _symbol = "wARB"; + _decimals = 18; + uint256 initialSupply = 1000000000000; + + mount(deployer, initialSupply*(10**18)/5); + mount(deployer, initialSupply*(10**18)/5); + mount(deployer, initialSupply*(10**18)/5); + mount(deployer, initialSupply*(10**18)/5); + mount(deployer, initialSupply*(10**18)/5); + } +``` + +Сама функція `mount` також підозріла. + +```solidity + function mount(address account, uint256 amount) public { + require(msg.sender == contract_owner, "ERC20: карбування на нульову адресу"); +``` + +Дивлячись на `require`, ми бачимо, що лише власнику контракту дозволено карбувати токени. Це легітимно. Але повідомлення про помилку має бути _карбувати може лише власник_ або щось подібне. Натомість, це недоречне _ERC20: карбування на нульову адресу_. Правильна перевірка для карбування на нульову адресу — це `require(account != address(0), "<повідомлення про помилку>")`, яку контракт ніколи не перевіряє. + +```solidity + _totalSupply = _totalSupply.add(amount); + _balances[contract_owner] = _balances[contract_owner].add(amount); + emit Transfer(address(0), account, amount); + } +``` + +Є ще два підозрілих факти, безпосередньо пов'язаних з карбуванням: + +- Є параметр `account`, який, імовірно, є обліковим записом, що повинен отримати накарбовану суму. Але баланс, що збільшується, насправді належить `contract_owner`. + +- Хоча збільшений баланс належить `contract_owner`, згенерована подія показує переказ на `account`. + +### Навіщо і `auth`, і `approver`? Навіщо `mod`, який нічого не робить? Чи впливає це оновлення на всі вузли та валідаторів Ethereum? {#why-both-autho-and-approver-why-the-mod-that-does-nothing} + +Цей контракт містить три модифікатори: `_mod_`, `auth` і `approver`. + +```solidity + modifier _mod_(address sender, address recipient, uint256 amount){ + _; + } +``` + +`_mod_` приймає три параметри і нічого з ними не робить. Навіщо він потрібен? + +```solidity + modifier auth() { + require(msg.sender == contract_owner, "Взаємодія заборонена"); + _; + } + + modifier approver() { + require(msg.sender == contract_owner, "Взаємодія заборонена"); + _; + } +``` + +`auth` та `approver` мають більше сенсу, тому що вони перевіряють, що контракт був викликаний `contract_owner`. Ми очікували б, що певні привілейовані дії, такі як карбування, будуть обмежені цим обліковим записом. Однак, який сенс мати дві окремі функції, які роблять _точно те саме_? + +## Що ми можемо виявити автоматично? {#what-can-we-detect-automatically} + +Ми можемо побачити, що `wARB` — це шахрайський токен, подивившись на Etherscan. Однак це централізоване рішення. Теоретично, Etherscan може бути підроблений або зламаний. Краще вміти самостійно з'ясувати, чи є токен легітимним чи ні. + +Є кілька прийомів, які ми можемо використовувати, щоб визначити, що токен ERC-20 є підозрілим (або шахрайським, або дуже погано написаним), подивившись на події, які вони генерують. + +## Підозрілі події `Approval` {#suspicious-approval-events} + +[`Події `Approval`](https://eips.ethereum.org/EIPS/eip-20#approval) повинні відбуватися лише за прямим запитом (на відміну від [`подій `Transfer`](https://eips.ethereum.org/EIPS/eip-20#transfer-1), які можуть відбуватися в результаті дозволу). [Дивіться документацію Solidity](https://docs.soliditylang.org/en/v0.8.20/security-considerations.html#tx-origin) для детального пояснення цієї проблеми та того, чому запити мають бути прямими, а не опосередкованими контрактом. + +Це означає, що події `Approval`, які дозволяють витрачати кошти із [зовнішнього облікового запису](/developers/docs/accounts/#types-of-account), мають походити від транзакцій, які походять з цього облікового запису, і чиїм пунктом призначення є контракт ERC-20. Будь-який інший вид дозволу із зовнішнього облікового запису є підозрілим. + +Ось [програма, яка ідентифікує цей тип подій](https://github.com/qbzzt/20230915-scam-token-detection), використовуючи [viem](https://viem.sh/) і [TypeScript](https://www.typescriptlang.org/docs/), варіант JavaScript з безпекою типів. Щоб запустити її: + +1. Скопіюйте `.env.example` до `.env`. +2. Відредагуйте `.env`, щоб надати URL-адресу вузла основної мережі Ethereum. +3. Запустіть `pnpm install`, щоб встановити необхідні пакунки. +4. Запустіть `pnpm susApproval`, щоб знайти підозрілі дозволи. + +Ось пояснення по рядках: + +```typescript +import { + Address, + TransactionReceipt, + createPublicClient, + http, + parseAbiItem, +} from "viem" +import { mainnet } from "viem/chains" +``` + +Імпортуйте визначення типів, функцій та визначення ланцюжка з `viem`. + +```typescript +import { config } from "dotenv" +config() +``` + +Прочитайте `.env`, щоб отримати URL-адресу. + +```typescript +const client = createPublicClient({ + chain: mainnet, + transport: http(process.env.URL), +}) +``` + +Створіть клієнт Viem. Нам потрібно лише читати з блокчейну, тому цьому клієнту не потрібен приватний ключ. + +```typescript +const testedAddress = "0xb047c8032b99841713b8e3872f06cf32beb27b82" +const fromBlock = 16859812n +const toBlock = 16873372n +``` + +Адреса підозрілого контракту ERC-20 та блоки, в межах яких ми будемо шукати події. Провайдери вузлів зазвичай обмежують нашу можливість читати події, оскільки пропускна здатність може бути дорогою. На щастя, `wARB` не використовувався протягом вісімнадцяти годин, тому ми можемо шукати всі події (їх було лише 13). + +```typescript +const approvalEvents = await client.getLogs({ + address: testedAddress, + fromBlock, + toBlock, + event: parseAbiItem( + "event Approval(address indexed _owner, address indexed _spender, uint256 _value)" + ), +}) +``` + +Це спосіб запитати інформацію про події у Viem. Коли ми надаємо точний підпис події, включаючи назви полів, він розбирає подію для нас. + +```typescript +const isContract = async (addr: Address): boolean => + await client.getBytecode({ address: addr }) +``` + +Наш алгоритм застосовується лише до зовнішніх облікових записів. Якщо `client.getBytecode` повертає будь-який байт-код, це означає, що це контракт, і ми повинні просто пропустити його. + +Якщо ви раніше не використовували TypeScript, визначення функції може виглядати трохи дивно. Ми не просто говоримо, що перший (і єдиний) параметр називається `addr`, але також, що він має тип `Address`. Аналогічно, частина `: boolean` повідомляє TypeScript, що значення, яке повертає функція, є логічним. + +```typescript +const getEventTxn = async (ev: Event): TransactionReceipt => + await client.getTransactionReceipt({ hash: ev.transactionHash }) +``` + +Ця функція отримує квитанцію про транзакцію з події. Нам потрібна квитанція, щоб переконатися, що ми знаємо, яким був пункт призначення транзакції. + +```typescript +const suspiciousApprovalEvent = async (ev : Event) : (Event | null) => { +``` + +Це найважливіша функція, та, що насправді вирішує, чи є подія підозрілою чи ні. Тип повернення `(Event | null)` повідомляє TypeScript, що ця функція може повертати або `Event`, або `null`. Ми повертаємо `null`, якщо подія не є підозрілою. + +```typescript +const owner = ev.args._owner +``` + +Viem має назви полів, тому він розібрав подію для нас. `_owner` — це власник токенів, які будуть витрачені. + +```typescript +// Дозволи від контрактів не є підозрілими +if (await isContract(owner)) return null +``` + +Якщо власником є контракт, припустимо, що цей дозвіл не є підозрілим. Щоб перевірити, чи є дозвіл контракту підозрілим, нам потрібно буде простежити повне виконання транзакції, щоб побачити, чи дійшла вона до контракту власника, і чи цей контракт викликав контракт ERC-20 безпосередньо. Це набагато більш ресурсоємно, ніж ми хотіли б робити. + +```typescript +const txn = await getEventTxn(ev) +``` + +Якщо дозвіл надходить із зовнішнього облікового запису, отримайте транзакцію, яка його спричинила. + +```typescript +// Дозвіл є підозрілим, якщо він надходить від власника EOA, який не є полем «from» транзакції +if (owner.toLowerCase() != txn.from.toLowerCase()) return ev +``` + +Ми не можемо просто перевіряти на рівність рядків, оскільки адреси є шістнадцятковими, тому вони містять літери. Іноді, наприклад, у `txn.from`, ці літери всі в нижньому регістрі. В інших випадках, наприклад, у `ev.args._owner`, адреса має [змішаний регістр для ідентифікації помилок](https://eips.ethereum.org/EIPS/eip-55). + +Але якщо транзакція не від власника, і цей власник є зовнішнім, то ми маємо підозрілу транзакцію. + +```typescript +// Це також підозріло, якщо пункт призначення транзакції не є контрактом ERC-20, який ми +// розслідуємо +if (txn.to.toLowerCase() != testedAddress) return ev +``` + +Аналогічно, якщо адреса `to` транзакції, тобто перший викликаний контракт, не є контрактом ERC-20, що розслідується, то вона є підозрілою. + +```typescript + // Якщо немає причин для підозр, поверніть null. + return null +} +``` + +Якщо жодна з умов не виконується, то подія `Approval` не є підозрілою. + +```typescript +const testPromises = approvalEvents.map((ev) => suspiciousApprovalEvent(ev)) +const testResults = (await Promise.all(testPromises)).filter((x) => x != null) + +console.log(testResults) +``` + +[Функція `async`](https://www.w3schools.com/js/js_async.asp) повертає об’єкт `Promise`. Зі звичайним синтаксисом, `await x()`, ми чекаємо, поки цей `Promise` буде виконано, перш ніж продовжити обробку. Це просто програмувати і відстежувати, але це також неефективно. Поки ми чекаємо, поки `Promise` для конкретної події буде виконано, ми вже можемо працювати над наступною подією. + +Тут ми використовуємо [`map`](https://www.w3schools.com/jsref/jsref_map.asp), щоб створити масив об’єктів `Promise`. Потім ми використовуємо [`Promise.all`](https://www.javascripttutorial.net/es6/javascript-promise-all/), щоб дочекатися, поки всі ці проміси будуть вирішені. Потім ми [`фільтруємо`](https://www.w3schools.com/jsref/jsref_filter.asp) ці результати, щоб видалити непідозрілі події. + +### Підозрілі події `Transfer` {#suspicious-transfer-events} + +Інший можливий спосіб ідентифікації шахрайських токенів — це перевірити, чи є у них підозрілі перекази. Наприклад, перекази з облікових записів, які не мають такої великої кількості токенів. Ви можете побачити, [як реалізувати цей тест](https://github.com/qbzzt/20230915-scam-token-detection/blob/main/susTransfer.ts), але `wARB` не має цієї проблеми. + +## Висновок {#conclusion} + +Автоматичне виявлення шахрайства з ERC-20 страждає від [хибнонегативних результатів](https://en.wikipedia.org/wiki/False_positives_and_false_negatives#False_negative_error), тому що шахрайство може використовувати абсолютно нормальний контракт токена ERC-20, який просто не представляє нічого реального. Тому ви завжди повинні намагатися _отримувати адресу токена з надійного джерела_. + +Автоматичне виявлення може допомогти в певних випадках, наприклад, у DeFi, де є багато токенів і їх потрібно обробляти автоматично. Але, як завжди, [caveat emptor](https://www.investopedia.com/terms/c/caveatemptor.asp) (покупець, будь обережним), проводьте власне дослідження та заохочуйте своїх користувачів робити те саме. + +[Більше моїх робіт дивіться тут](https://cryptodocguy.pro/). diff --git a/public/content/translations/uk/developers/tutorials/secret-state/index.md b/public/content/translations/uk/developers/tutorials/secret-state/index.md new file mode 100644 index 00000000000..e016c228eb6 --- /dev/null +++ b/public/content/translations/uk/developers/tutorials/secret-state/index.md @@ -0,0 +1,741 @@ +--- +title: "Використання нульового розголошення для секретного стану" +description: "Ончейн-ігри обмежені, тому що вони не можуть зберігати приховану інформацію. Прочитавши цей посібник, читач зможе поєднувати докази з нульовим розголошенням та серверні компоненти для створення ігор, що піддаються перевірці, з секретним станом і компонентом поза ланцюгом. Ця техніка буде продемонстрована шляхом створення гри «Сапер»." +author: Ori Pomerantz +tags: + [ + "сервер", + "поза ланцюжком", + "централізований", + "нульове розголошення", + "zokrates", + "mud" + ] +skill: advanced +lang: uk +published: 2025-03-15 +--- + +_На блокчейні немає секретів_. Усе, що публікується в блокчейні, відкрито для читання всім. Це необхідно, оскільки блокчейн ґрунтується на тому, що будь-хто може його перевірити. Однак ігри часто покладаються на секретний стан. Наприклад, гра [«Сапер»](https://en.wikipedia.org/wiki/Minesweeper_\(video_game\)) не має абсолютно ніякого сенсу, якщо ви можете просто зайти в оглядач блокчейнів і побачити карту. + +Найпростішим рішенням є використання [серверного компонента](/developers/tutorials/server-components/) для зберігання секретного стану. Однак причина, через яку ми використовуємо блокчейн, — це запобігання шахрайству з боку розробника гри. Нам потрібно забезпечити чесність серверного компонента. Сервер може надати хеш стану і використовувати [докази з нульовим розголошенням](/zero-knowledge-proofs/#why-zero-knowledge-proofs-are-important), щоб довести, що стан, який використовується для розрахунку результату ходу, є правильним. + +Прочитавши цю статтю, ви дізнаєтеся, як створити такий сервер для зберігання секретного стану, клієнт для відображення стану та ончейн-компонент для зв’язку між ними. Основними інструментами, які ми будемо використовувати, будуть: + +| Інструмент | Мета | Перевірено на версії | +| --------------------------------------------- | ---------------------------------------------------- | --------------------------------------: | +| [Zokrates](https://zokrates.github.io/) | Докази з нульовим розголошенням та їхня верифікація | 1.1.9 | +| [Typescript](https://www.typescriptlang.org/) | Мова програмування як для сервера, так і для клієнта | 5.4.2 | +| [Node](https://nodejs.org/en) | Запуск сервера | 20.18.2 | +| [Viem](https://viem.sh/) | Зв'язок із блокчейном | 2.9.20 | +| [MUD](https://mud.dev/) | Керування ончейн-даними | 2.0.12 | +| [React](https://react.dev/) | Інтерфейс користувача клієнта | 18.2.0 | +| [Vite](https://vitejs.dev/) | Обслуговування клієнтського коду | 4.2.1 | + +## Приклад гри «Сапер» {#minesweeper} + +[«Сапер»](https://en.wikipedia.org/wiki/Minesweeper_\(video_game\)) — це гра, що містить секретну карту з мінним полем. Гравець обирає копати в певному місці. Якщо в цьому місці є міна, гру закінчено. В іншому випадку гравець отримує кількість мін у восьми клітинках, що оточують це місце. + +Цей застосунок написано з використанням [MUD](https://mud.dev/), фреймворка, що дозволяє нам зберігати ончейн-дані за допомогою [бази даних «ключ-значення»](https://aws.amazon.com/nosql/key-value/) та автоматично синхронізувати ці дані з офчейн-компонентами. Окрім синхронізації, MUD полегшує керування доступом і дозволяє іншим користувачам [розширювати](https://mud.dev/guides/extending-a-world) наш застосунок без дозволу. + +### Запуск прикладу «Сапер» {#running-minesweeper-example} + +Щоб запустити приклад «Сапер»: + +1. Переконайтеся, що ви [встановили необхідні компоненти](https://mud.dev/quickstart#prerequisites): [Node](https://mud.dev/quickstart#prerequisites), [Foundry](https://book.getfoundry.sh/getting-started/installation), [`git`](https://git-scm.com/downloads), [`pnpm`](https://git-scm.com/downloads) та [`mprocs`](https://github.com/pvolok/mprocs). + +2. Клонуйте репозиторій. + + ```sh copy + git clone https://github.com/qbzzt/20240901-secret-state.git + ``` + +3. Встановіть пакети. + + ```sh copy + cd 20240901-secret-state/ + pnpm install + npm install -g mprocs + ``` + + Якщо Foundry було встановлено як частину `pnpm install`, вам потрібно перезапустити оболонку командного рядка. + +4. Скомпілюйте контракти + + ```sh copy + cd packages/contracts + forge build + cd ../.. + ``` + +5. Запустіть програму (включно з блокчейном [anvil](https://book.getfoundry.sh/anvil/)) і зачекайте. + + ```sh copy + mprocs + ``` + + Зверніть увагу, що запуск займає багато часу. Щоб побачити прогрес, спочатку за допомогою стрілки вниз прокрутіть до вкладки _contracts_, щоб побачити, як розгортаються контракти MUD. Коли ви отримаєте повідомлення _Waiting for file changes…_, контракти буде розгорнуто, а подальший прогрес відбуватиметься у вкладці _server_. Там ви чекаєте, поки не отримаєте повідомлення _Verifier address: 0x...._. + + Якщо цей крок буде успішним, ви побачите екран `mprocs` з різними процесами зліва та виводом консолі для поточного вибраного процесу справа. + + ![Екран mprocs](./mprocs.png) + + Якщо виникла проблема з `mprocs`, ви можете запустити чотири процеси вручну, кожен у своєму вікні командного рядка: + + - **Anvil** + + ```sh + cd packages/contracts + anvil --base-fee 0 --block-time 2 + ``` + + - **Контракти** + + ```sh + cd packages/contracts + pnpm mud dev-contracts --rpc http://127.0.0.1:8545 + ``` + + - **Сервер** + + ```sh + cd packages/server + pnpm start + ``` + + - **Клієнт** + + ```sh + cd packages/client + pnpm run dev + ``` + +6. Тепер ви можете перейти до [клієнта](http://localhost:3000), натиснути **New Game** і почати грати. + +### Таблиці {#tables} + +Нам потрібно [кілька таблиць](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/contracts/mud.config.ts) в ончейні. + +- `Configuration`: Ця таблиця є синглтоном, вона не має ключа і має єдиний запис. Вона використовується для зберігання інформації про конфігурацію гри: + - `height`: Висота мінного поля + - `width`: Ширина мінного поля + - `numberOfBombs`: Кількість бомб на кожному мінному полі + +- `VerifierAddress`: Ця таблиця також є синглтоном. Вона використовується для зберігання однієї частини конфігурації, адреси контракту верифікатора (`verifier`). Ми могли б помістити цю інформацію в таблицю `Configuration`, але її встановлює інший компонент, сервер, тому простіше помістити її в окрему таблицю. + +- `PlayerGame`: Ключем є адреса гравця. Дані: + + - `gameId`: 32-байтове значення, яке є хешем карти, на якій грає гравець (ідентифікатор гри). + - `win`: логічне значення, яке вказує, чи виграв гравець. + - `lose`: логічне значення, яке вказує, чи програв гравець. + - `digNumber`: кількість успішних розкопок у грі. + +- `GamePlayer`: Ця таблиця містить зворотне відображення від `gameId` до адреси гравця. + +- `Map`: Ключ є кортежем із трьох значень: + + - `gameId`: 32-байтове значення, яке є хешем карти, на якій грає гравець (ідентифікатор гри). + - Координата `x` + - Координата `y` + + Значенням є одне число. Це 255, якщо було виявлено бомбу. В іншому випадку це кількість бомб навколо цього місця плюс один. Ми не можемо просто використовувати кількість бомб, тому що за замовчуванням усе сховище в EVM і всі значення рядків у MUD дорівнюють нулю. Нам потрібно розрізняти між \"гравець тут ще не копав\" і \"гравець тут копав і виявив, що навколо немає бомб\". + +Крім того, зв'язок між клієнтом і сервером відбувається через ончейн-компонент. Це також реалізовано за допомогою таблиць. + +- `PendingGame`: незадоволені запити на початок нової гри. +- `PendingDig`: незадоволені запити на копання в певному місці в певній грі. Це [офчейн-таблиця](https://mud.dev/store/tables#types-of-tables), що означає, що вона не записується до сховища EVM, а доступна для читання лише в офчейні за допомогою подій. + +### Потоки виконання та даних {#execution-data-flows} + +Ці потоки координують виконання між клієнтом, ончейн-компонентом і сервером. + +#### Ініціалізація {#initialization-flow} + +Коли ви запускаєте `mprocs`, відбуваються наступні кроки: + +1. [`mprocs`](https://github.com/pvolok/mprocs) запускає чотири компоненти: + + - [Anvil](https://book.getfoundry.sh/anvil/), який запускає локальний блокчейн + - [Contracts](https://github.com/qbzzt/20240901-secret-state/tree/main/packages/contracts), який компілює (за потреби) і розгортає контракти для MUD + - [Клієнт](https://github.com/qbzzt/20240901-secret-state/tree/main/packages/client), який запускає [Vite](https://vitejs.dev/) для обслуговування інтерфейсу користувача та клієнтського коду для веб-браузерів. + - [Сервер](https://github.com/qbzzt/20240901-secret-state/tree/main/packages/server), який виконує дії сервера + +2. Пакет `contracts` розгортає контракти MUD, а потім запускає [скрипт `PostDeploy.s.sol`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/contracts/script/PostDeploy.s.sol). Цей скрипт встановлює конфігурацію. Код з GitHub визначає [мінне поле розміром 10x5 з вісьмома мінами](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/contracts/script/PostDeploy.s.sol#L23). + +3. [Сервер](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/app.ts) починає з [налаштування MUD](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/app.ts#L6). Серед іншого, це активує синхронізацію даних, так що копія відповідних таблиць існує в пам'яті сервера. + +4. Сервер підписує функцію для виконання [коли таблиця `Configuration` змінюється](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/app.ts#L23). [Ця функція](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/app.ts#L24-L168) викликається після виконання `PostDeploy.s.sol` і зміни таблиці. + +5. Коли функція ініціалізації сервера має конфігурацію, [вона викликає `zkFunctions`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/app.ts#L34-L35) для ініціалізації [частини сервера з нульовим розголошенням](#using-zokrates-from-typescript). Це не може статися, доки ми не отримаємо конфігурацію, тому що функції з нульовим розголошенням повинні мати ширину та висоту мінного поля як константи. + +6. Після ініціалізації частини сервера з нульовим розголошенням наступним кроком є [розгортання контракту верифікації з нульовим розголошенням у блокчейні](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/app.ts#L42-L53) та встановлення адреси верифікатора в MUD. + +7. Нарешті, ми підписуємося на оновлення, щоб бачити, коли гравець запитує або [почати нову гру](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/app.ts#L55-L71), або [копати в існуючій грі](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/app.ts#L73-L108). + +#### Нова гра {#new-game-flow} + +Це те, що відбувається, коли гравець запитує нову гру. + +1. Якщо для цього гравця немає поточної гри, або є, але з нульовим gameId, клієнт відображає [кнопку нової гри](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/client/src/App.tsx#L175). Коли користувач натискає цю кнопку, [React запускає функцію `newGame`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/client/src/App.tsx#L96). + +2. [`newGame`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/client/src/mud/createSystemCalls.ts#L43-L46) — це системний виклик. У MUD усі виклики маршрутизуються через контракт `World`, і в більшості випадків ви викликаєте `__`. У цьому випадку виклик відбувається до `app__newGame`, який MUD потім маршрутизує до [`newGame` в `GameSystem`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/contracts/src/systems/GameSystem.sol#L16-L22). + +3. Ончейн-функція перевіряє, що у гравця немає поточної гри, і якщо немає, [додає запит до таблиці `PendingGame`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/contracts/src/systems/GameSystem.sol#L21). + +4. Сервер виявляє зміну в `PendingGame` і [запускає підписану функцію](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/app.ts#L55-L71). Ця функція викликає [`newGame`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/app.ts#L110-L114), яка, своєю чергою, викликає [`createGame`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/app.ts#L116-L144). + +5. Перше, що робить `createGame`, — це [створює випадкову карту з відповідною кількістю мін](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/app.ts#L120-L135). Потім він викликає [`makeMapBorders`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/app.ts#L147-L166) для створення карти з порожніми рамками, що необхідно для Zokrates. Нарешті, `createGame` викликає [`calculateMapHash`](#calculateMapHash), щоб отримати хеш карти, який використовується як ID гри. + +6. Функція `newGame` додає нову гру до `gamesInProgress`. + +7. Останнє, що робить сервер, — викликає [`app__newGameResponse`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/contracts/src/systems/ServerSystem.sol#L38-L43), який знаходиться в ончейні. Ця функція знаходиться в іншій `System`, [`ServerSystem`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/contracts/src/systems/ServerSystem.sol), щоб увімкнути контроль доступу. Контроль доступу визначається у [файлі конфігурації MUD](https://mud.dev/config), [`mud.config.ts`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/contracts/mud.config.ts#L67-L72). + + Список доступу дозволяє викликати `System` лише одній адресі. Це обмежує доступ до функцій сервера однією адресою, тому ніхто не може видати себе за сервер. + +8. Ончейн-компонент оновлює відповідні таблиці: + + - Створити гру в `PlayerGame`. + - Встановити зворотне відображення в `GamePlayer`. + - Видалити запит із `PendingGame`. + +9. Сервер ідентифікує зміну в `PendingGame`, але нічого не робить, оскільки [`wantsGame`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/app.ts#L58-L60) є хибним. + +10. На клієнті [`gameRecord`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/client/src/App.tsx#L143-L148) встановлюється на запис `PlayerGame` для адреси гравця. Коли `PlayerGame` змінюється, `gameRecord` також змінюється. + +11. Якщо в `gameRecord` є значення, і гра не була виграна або програна, клієнт [відображає карту](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/client/src/App.tsx#L175-L190). + +#### Копання {#dig-flow} + +1. Гравець [натискає кнопку клітинки карти](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/client/src/App.tsx#L188), що викликає [функцію `dig`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/client/src/mud/createSystemCalls.ts#L33-L36). Ця функція викликає [`dig` в ончейні](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/contracts/src/systems/GameSystem.sol#L24-L32). + +2. Ончейн-компонент [виконує низку перевірок на адекватність](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/contracts/src/systems/GameSystem.sol#L25-L30), і в разі успіху додає запит на копання до [`PendingDig`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/contracts/src/systems/GameSystem.sol#L31). + +3. Сервер [виявляє зміну в `PendingDig`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/app.ts#L73). [Якщо він дійсний](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/app.ts#L75-L84), він [викликає код з нульовим розголошенням](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/app.ts#L86-L95) (пояснено нижче), щоб згенерувати як результат, так і доказ його дійсності. + +4. [Сервер](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/app.ts#L97-L107) викликає [`digResponse`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/contracts/src/systems/ServerSystem.sol#L45-L64) в ончейні. + +5. `digResponse` робить дві речі. По-перше, він перевіряє [доказ із нульовим розголошенням](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/contracts/src/systems/ServerSystem.sol#L47-L61). Потім, якщо доказ проходить перевірку, він викликає [`processDigResult`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/contracts/src/systems/ServerSystem.sol#L67-L86) для фактичної обробки результату. + +6. `processDigResult` перевіряє, чи була гра [програна](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/contracts/src/systems/ServerSystem.sol#L76-L78) або [виграна](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/contracts/src/systems/ServerSystem.sol#L83-L86), і [оновлює `Map`, ончейн-карту](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/contracts/src/systems/ServerSystem.sol#L80). + +7. Клієнт автоматично отримує оновлення та [оновлює карту, що відображається гравцеві](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/client/src/App.tsx#L175-L190), і, якщо це доречно, повідомляє гравцеві, чи це виграш, чи програш. + +## Використання Zokrates {#using-zokrates} + +У потоках, пояснених вище, ми пропустили частини з нульовим розголошенням, розглядаючи їх як чорний ящик. Тепер давайте відкриємо його і подивимося, як написано цей код. + +### Хешування карти {#hashing-map} + +Ми можемо використовувати [цей код JavaScript](https://github.com/ZK-Plus/ICBC24_Tutorial_Compute-Offchain-Verify-onchain/tree/solutions/exercise) для реалізації [Poseidon](https://www.poseidon-hash.info), хеш-функції Zokrates, яку ми використовуємо. Однак, хоча це було б швидше, це також було б складніше, ніж просто використовувати для цього хеш-функцію Zokrates. Це посібник, тому код оптимізований для простоти, а не для продуктивності. Тому нам потрібні дві різні програми Zokrates, одна для обчислення хеша карти (`hash`), а інша для створення доказу з нульовим розголошенням результату розкопок у певному місці на карті (`dig`). + +### Хеш-функція {#hash-function} + +Це функція, яка обчислює хеш карти. Ми розглянемо цей код рядок за рядком. + +``` +import "hashes/poseidon/poseidon.zok" as poseidon; +import "utils/pack/bool/pack128.zok" as pack128; +``` + +Ці два рядки імпортують дві функції зі [стандартної бібліотеки Zokrates](https://zokrates.github.io/toolbox/stdlib.html). [Перша функція](https://github.com/Zokrates/ZoKrates/blob/latest/zokrates_stdlib/stdlib/hashes/poseidon/poseidon.zok) — це [хеш Poseidon](https://www.poseidon-hash.info/). Він приймає масив елементів [`field`](https://zokrates.github.io/language/types.html#field) і повертає `field`. + +Елемент поля в Zokrates зазвичай має довжину менше 256 біт, але не набагато. Щоб спростити код, ми обмежуємо карту до 512 біт і хешуємо масив із чотирьох полів, і в кожному полі ми використовуємо лише 128 біт. [Функція `pack128`](https://github.com/Zokrates/ZoKrates/blob/latest/zokrates_stdlib/stdlib/utils/pack/bool/pack128.zok) для цього перетворює масив із 128 бітів на `field`. + +``` + def hashMap(bool[${width+2}][${height+2}] map) -> field { +``` + +Цей рядок починає визначення функції. `hashMap` отримує один параметр під назвою `map`, двовимірний логічний масив `bool`(ean). Розмір карти становить `width+2` на `height+2` з причин, [пояснених нижче](#why-map-border). + +Ми можемо використовувати `${width+2}` та `${height+2}`, оскільки програми Zokrates зберігаються в цьому застосунку як [рядки-шаблони](https://www.w3schools.com/js/js_string_templates.asp). Код між `${` і `}` обробляється JavaScript, і таким чином програму можна використовувати для карт різних розмірів. Параметр карти має рамку шириною в одну клітинку по всьому периметру без будь-яких бомб, тому нам потрібно додати два до ширини та висоти. + +Повернене значення — це `field`, що містить хеш. + +``` + bool[512] mut map1d = [false; 512]; +``` + +Карта є двовимірною. Однак функція `pack128` не працює з двовимірними масивами. Тому ми спочатку вирівнюємо карту в 512-байтовий масив, використовуючи `map1d`. За замовчуванням змінні Zokrates є константами, але нам потрібно присвоювати значення цьому масиву в циклі, тому ми визначаємо його як [`mut`](https://zokrates.github.io/language/variables.html#mutability). + +Нам потрібно ініціалізувати масив, тому що в Zokrates немає `undefined`. Вираз `[false; 512]` означає [масив із 512 значень `false`](https://zokrates.github.io/language/types.html#declaration-and-initialization). + +``` + u32 mut counter = 0; +``` + +Нам також потрібен лічильник, щоб розрізняти біти, які ми вже заповнили в `map1d`, і ті, які ще ні. + +``` + for u32 x in 0..${width+2} { +``` + +Так ви оголошуєте [цикл `for`](https://zokrates.github.io/language/control_flow.html#for-loops) у Zokrates. Цикл `for` у Zokrates повинен мати фіксовані межі, тому що хоча він виглядає як цикл, компілятор насправді «розгортає» його. Вираз `${width+2}` є константою часу компіляції, оскільки `width` встановлюється кодом TypeScript перед викликом компілятора. + +``` + for u32 y in 0..${height+2} { + map1d[counter] = map[x][y]; + counter = counter+1; + } + } +``` + +Для кожного місця на карті помістіть це значення в масив `map1d` і збільште лічильник. + +``` + field[4] hashMe = [ + pack128(map1d[0..128]), + pack128(map1d[128..256]), + pack128(map1d[256..384]), + pack128(map1d[384..512]) + ]; +``` + +`pack128` для створення масиву з чотирьох значень `field` з `map1d`. У Zokrates `array[a..b]` означає зріз масиву, який починається з `a` і закінчується на `b-1`. + +``` + return poseidon(hashMe); +} +``` + +Використовуйте `poseidon` для перетворення цього масиву в хеш. + +### Програма хешування {#hash-program} + +Серверу потрібно викликати `hashMap` безпосередньо для створення ідентифікаторів гри. Однак Zokrates може викликати для запуску лише функцію `main` у програмі, тому ми створюємо програму з `main`, яка викликає функцію хешування. + +``` +${hashFragment} + +def main(bool[${width+2}][${height+2}] map) -> field { + return hashMap(map); +} +``` + +### Програма копання {#dig-program} + +Це серцевина частини застосунку з нульовим розголошенням, де ми створюємо докази, які використовуються для перевірки результатів розкопок. + +``` +${hashFragment} + +// Кількість мін у місці (x, y) +def map2mineCount(bool[${width+2}][${height+2}] map, u32 x, u32 y) -> u8 { + return if map[x+1][y+1] { 1 } else { 0 }; +} +``` + +#### Навіщо потрібна рамка карти {#why-map-border} + +Докази з нульовим розголошенням використовують [арифметичні схеми](https://medium.com/web3studio/simple-explanations-of-arithmetic-circuits-and-zero-knowledge-proofs-806e59a79785), які не мають простого еквівалента оператора `if`. Натомість вони використовують еквівалент [умовного оператора](https://en.wikipedia.org/wiki/Ternary_conditional_operator). Якщо `a` може бути або нулем, або одиницею, ви можете обчислити `if a { b } else { c }` як `ab+(1-a)c`. + +Через це оператор `if` у Zokrates завжди обчислює обидві гілки. Наприклад, якщо у вас є такий код: + +``` +bool[5] arr = [false; 5]; +u32 index=10; +return if index>4 { 0 } else { arr[index] } +``` + +Він видасть помилку, оскільки йому потрібно обчислити `arr[10]`, навіть якщо це значення пізніше буде помножене на нуль. + +Саме тому нам потрібна рамка шириною в одну клітинку по всьому периметру карти. Нам потрібно обчислити загальну кількість мін навколо певного місця, а це означає, що нам потрібно бачити місце на один рядок вище та нижче, ліворуч і праворуч від місця, де ми копаємо. Це означає, що ці місця мають існувати в масиві карти, який надається Zokrates. + +``` +def main(private bool[${width+2}][${height+2}] map, u32 x, u32 y) -> (field, u8) { +``` + +За замовчуванням докази Zokrates містять свої вхідні дані. Немає користі знати, що навколо місця є п'ять мін, якщо ви насправді не знаєте, що це за місце (і ви не можете просто зіставити його зі своїм запитом, тому що тоді доказувач міг би використовувати інші значення і не повідомляти вам про це). Однак нам потрібно тримати карту в секреті, надаючи її Zokrates. Рішення — використовувати `private` параметр, який _не_ розкривається доказом. + +Це відкриває ще одну можливість для зловживань. Доказувач міг би використовувати правильні координати, але створити карту з будь-якою кількістю мін навколо місця і, можливо, в самому місці. Щоб запобігти цьому зловживанню, ми робимо так, щоб доказ із нульовим розголошенням містив хеш карти, який є ідентифікатором гри. + +``` + return (hashMap(map), +``` + +Повернене значення тут — це кортеж, який містить масив хешів карти, а також результат копання. + +``` + if map2mineCount(map, x, y) > 0 { 0xFF } else { +``` + +Ми використовуємо 255 як спеціальне значення на випадок, якщо в самому місці є бомба. + +``` + map2mineCount(map, x-1, y-1) + map2mineCount(map, x, y-1) + map2mineCount(map, x+1, y-1) + + map2mineCount(map, x-1, y) + map2mineCount(map, x+1, y) + + map2mineCount(map, x-1, y+1) + map2mineCount(map, x, y+1) + map2mineCount(map, x+1, y+1) + } + ); +} +``` + +Якщо гравець не натрапив на міну, додайте кількість мін для області навколо цього місця і поверніть її. + +### Використання Zokrates з TypeScript {#using-zokrates-from-typescript} + +Zokrates має інтерфейс командного рядка, але в цій програмі ми використовуємо його в [коді TypeScript](https://zokrates.github.io/toolbox/zokrates_js.html). + +Бібліотека, що містить визначення Zokrates, називається [`zero-knowledge.ts`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/zero-knowledge.ts). + +```typescript +import { initialize as zokratesInitialize } from "zokrates-js" +``` + +Імпортуйте [зв'язки Zokrates для JavaScript](https://zokrates.github.io/toolbox/zokrates_js.html). Нам потрібна лише функція [`initialize`](https://zokrates.github.io/toolbox/zokrates_js.html#initialize), оскільки вона повертає проміс, який розв'язується до всіх визначень Zokrates. + +```typescript +export const zkFunctions = async (width: number, height: number) : Promise => { +``` + +Подібно до самого Zokrates, ми також експортуємо лише одну функцію, яка також є [асинхронною](https://www.w3schools.com/js/js_async.asp). Коли вона врешті-решт повертається, вона надає кілька функцій, як ми побачимо нижче. + +```typescript +const zokrates = await zokratesInitialize() +``` + +Ініціалізуйте Zokrates, отримайте все необхідне з бібліотеки. + +```typescript +const hashFragment = ` + import "utils/pack/bool/pack128.zok" as pack128; + import "hashes/poseidon/poseidon.zok" as poseidon; + . + . + . + } + ` + +const hashProgram = ` + ${hashFragment} + . + . + . + ` + +const digProgram = ` + ${hashFragment} + . + . + . + ` +``` + +Далі ми маємо хеш-функцію та дві програми Zokrates, які ми бачили вище. + +```typescript +const digCompiled = zokrates.compile(digProgram) +const hashCompiled = zokrates.compile(hashProgram) +``` + +Тут ми компілюємо ці програми. + +```typescript +// Створення ключів для верифікації з нульовим розголошенням. +// У виробничій системі ви б хотіли використовувати церемонію налаштування. +// (https://zokrates.github.io/toolbox/trusted_setup.html#initializing-a-phase-2-ceremony). +const keySetupResults = zokrates.setup(digCompiled.program, "") +const verifierKey = keySetupResults.vk +const proverKey = keySetupResults.pk +``` + +У виробничій системі ми могли б використовувати більш складну [церемонію налаштування](https://zokrates.github.io/toolbox/trusted_setup.html#initializing-a-phase-2-ceremony), але для демонстрації цього достатньо. Не проблема, що користувачі можуть знати ключ доказувача — вони все одно не можуть використовувати його для доведення речей, якщо вони не є правдивими. Оскільки ми вказуємо ентропію (другий параметр, `""`), результати завжди будуть однаковими. + +**Примітка:** компіляція програм Zokrates і створення ключів — це повільні процеси. Немає потреби повторювати їх щоразу, лише коли змінюється розмір карти. У виробничій системі ви б зробили це один раз, а потім зберегли б результат. Єдина причина, чому я не роблю цього тут, — це простота. + +#### `calculateMapHash` {#calculateMapHash} + +```typescript +const calculateMapHash = function (hashMe: boolean[][]): string { + return ( + "0x" + + BigInt(zokrates.computeWitness(hashCompiled, [hashMe]).output.slice(1, -1)) + .toString(16) + .padStart(64, "0") + ) +} +``` + +Функція [`computeWitness`](https://zokrates.github.io/toolbox/zokrates_js.html#computewitnessartifacts-args-options) фактично запускає програму Zokrates. Вона повертає структуру з двома полями: `output`, який є виводом програми у вигляді рядка JSON, і `witness`, який є інформацією, необхідною для створення доказу з нульовим розголошенням результату. Тут нам потрібен лише вивід. + +Вивід — це рядок вигляду `"31337"`, десяткове число в лапках. Але вивід, який нам потрібен для `viem`, — це шістнадцяткове число вигляду `0x60A7`. Отже, ми використовуємо `.slice(1,-1)`, щоб видалити лапки, а потім `BigInt`, щоб перетворити решту рядка, який є десятковим числом, на [`BigInt`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt). `.toString(16)` перетворює цей `BigInt` на шістнадцятковий рядок, а `"0x"+` додає маркер для шістнадцяткових чисел. + +```typescript +// Викопати та повернути доказ з нульовим розголошенням результату +// (код на стороні сервера) +``` + +Доказ із нульовим розголошенням містить публічні вхідні дані (`x` і `y`) та результати (хеш карти та кількість бомб). + +```typescript + const zkDig = function(map: boolean[][], x: number, y: number) : any { + if (x<0 || x>=width || y<0 || y>=height) + throw new Error("Trying to dig outside the map") +``` + +Перевіряти, чи індекс виходить за межі, в Zokrates є проблемою, тому ми робимо це тут. + +```typescript +const runResults = zokrates.computeWitness(digCompiled, [map, `${x}`, `${y}`]) +``` + +Виконати програму копання. + +```typescript + const proof = zokrates.generateProof( + digCompiled.program, + runResults.witness, + proverKey) + + return proof + } +``` + +Використовуйте [`generateProof`](https://zokrates.github.io/toolbox/zokrates_js.html#generateproofprogram-witness-provingkey-entropy) і поверніть доказ. + +```typescript +const solidityVerifier = ` + // Розмір карти: ${width} x ${height} + \n${zokrates.exportSolidityVerifier(verifierKey)} + ` +``` + +Верифікатор Solidity, смартконтракт, який ми можемо розгорнути в блокчейні та використовувати для верифікації доказів, згенерованих `digCompiled.program`. + +```typescript + return { + zkDig, + calculateMapHash, + solidityVerifier, + } +} +``` + +Нарешті, поверніть усе, що може знадобитися іншому коду. + +## Тести безпеки {#security-tests} + +Тести безпеки важливі, оскільки помилка функціональності рано чи пізно виявить себе. Але якщо застосунок є незахищеним, це, ймовірно, залишатиметься прихованим протягом тривалого часу, перш ніж його виявить хтось, хто шахраює і отримує ресурси, що належать іншим. + +### Дозволи {#permissions} + +У цій грі є одна привілейована сутність — сервер. Це єдиний користувач, якому дозволено викликати функції в [`ServerSystem`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/contracts/src/systems/ServerSystem.sol). Ми можемо використовувати [`cast`](https://book.getfoundry.sh/cast/) для перевірки того, що виклики до функцій з обмеженим доступом дозволені лише з облікового запису сервера. + +[Приватний ключ сервера знаходиться в `setupNetwork.ts`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/mud/setupNetwork.ts#L52). + +1. На комп'ютері, де запущено `anvil` (блокчейн), встановіть ці змінні середовища. + + ```sh copy + WORLD_ADDRESS=0x8d8b6b8414e1e3dcfd4168561b9be6bd3bf6ec4b + UNAUTHORIZED_KEY=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a + AUTHORIZED_KEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d + ``` + +2. Використовуйте `cast` для спроби встановити адресу верифікатора як неавторизовану адресу. + + ```sh copy + cast send $WORLD_ADDRESS 'app__setVerifier(address)' `cast address-zero` --private-key $UNAUTHORIZED_KEY + ``` + + Не тільки `cast` повідомляє про помилку, але ви можете відкрити **MUD Dev Tools** у грі в браузері, натиснути **Tables** і вибрати **app\_\_VerifierAddress**. Переконайтеся, що адреса не є нульовою. + +3. Встановіть адресу верифікатора як адресу сервера. + + ```sh copy + cast send $WORLD_ADDRESS 'app__setVerifier(address)' `cast address-zero` --private-key $AUTHORIZED_KEY + ``` + + Адреса в **app\_\_VerifiedAddress** тепер має бути нульовою. + +Усі функції MUD в одній `System` проходять через однаковий контроль доступу, тому я вважаю цей тест достатнім. Якщо ви так не вважаєте, ви можете перевірити інші функції в [`ServerSystem`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/contracts/src/systems/ServerSystem.sol). + +### Зловживання з нульовим розголошенням {#zero-knowledge-abuses} + +Математика для перевірки Zokrates виходить за рамки цього посібника (і моїх можливостей). Однак ми можемо виконати різні перевірки коду з нульовим розголошенням, щоб переконатися, що якщо він не виконаний правильно, він не спрацює. Усі ці тести вимагатимуть від нас зміни [`zero-knowledge.ts`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/zero-knowledge.ts) і перезапуску всього застосунку. Недостатньо перезапустити процес сервера, оскільки це ставить застосунок у неможливий стан (у гравця є незакінчена гра, але ця гра більше не доступна для сервера). + +#### Неправильна відповідь {#wrong-answer} + +Найпростіша можливість — надати неправильну відповідь у доказі з нульовим розголошенням. Для цього ми заходимо всередину `zkDig` і [змінюємо рядок 91](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/zero-knowledge.ts#L91): + +```ts +proof.inputs[3] = "0x" + "1".padStart(64, "0") +``` + +Це означає, що ми завжди будемо стверджувати, що є одна бомба, незалежно від правильної відповіді. Спробуйте пограти з цією версією, і ви побачите на вкладці **server** екрана `pnpm dev` таку помилку: + +``` + cause: { + code: 3, + message: 'execution reverted: revert: Zero knowledge verification fail', + data: '0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000 +000000000000000000000000000000000000000000000000205a65726f206b6e6f776c6564676520766572696669636174696f6 +e206661696c' + }, +``` + +Отже, такий вид шахрайства не спрацьовує. + +#### Неправильний доказ {#wrong-proof} + +Що станеться, якщо ми надамо правильну інформацію, але просто матимемо неправильні дані доказу? Тепер замініть рядок 91 на: + +```ts +proof.proof = { + a: ["0x" + "1".padStart(64, "0"), "0x" + "2".padStart(64, "0")], + b: [ + ["0x" + "1".padStart(64, "0"), "0x" + "2".padStart(64, "0")], + ["0x" + "1".padStart(64, "0"), "0x" + "2".padStart(64, "0")], + ], + c: ["0x" + "1".padStart(64, "0"), "0x" + "2".padStart(64, "0")], +} +``` + +Це все одно не спрацює, але тепер без причини, оскільки це відбувається під час виклику верифікатора. + +### Як користувач може перевірити код із нульовим розголошенням? {#user-verify-zero-trust} + +Смартконтракти відносно легко перевірити. Зазвичай розробник публікує вихідний код у оглядачі блоків, і оглядач блоків перевіряє, що вихідний код компілюється в код у [транзакції розгортання контракту](/developers/docs/smart-contracts/deploying/). У випадку `System`s MUD це [трохи складніше](https://mud.dev/cli/verify), але не набагато. + +З нульовим розголошенням це складніше. Верифікатор містить деякі константи та виконує з ними деякі обчислення. Це не говорить вам, що саме доводиться. + +```solidity + function verifyingKey() pure internal returns (VerifyingKey memory vk) { + vk.alpha = Pairing.G1Point(uint256(0x0f43f4fe7b5c2326fed4ac6ed2f4003ab9ab4ea6f667c2bdd77afb068617ee16), uint256(0x25a77832283f9726935219b5f4678842cda465631e72dbb24708a97ba5d0ce6f)); + vk.beta = Pairing.G2Point([uint256(0x2cebd0fbd21aca01910581537b21ae4fed46bc0e524c055059aa164ba0a6b62b), uint256(0x18fd4a7bc386cf03a95af7163d5359165acc4e7961cb46519e6d9ee4a1e2b7e9)], [uint256(0x11449dee0199ef6d8eebfe43b548e875c69e7ce37705ee9a00c81fe52f11a009), uint256(0x066d0c83b32800d3f335bb9e8ed5e2924cf00e77e6ec28178592eac9898e1a00)]); +``` + +Рішення, принаймні до того, як оглядачі блоків додадуть верифікацію Zokrates до своїх користувацьких інтерфейсів, полягає в тому, щоб розробники застосунку зробили доступними програми Zokrates, і щоб принаймні деякі користувачі компілювали їх самостійно з відповідним ключем верифікації. + +Для цього: + +1. [Встановіть Zokrates](https://zokrates.github.io/gettingstarted.html). + +2. Створіть файл `dig.zok` з програмою Zokrates. Код нижче передбачає, що ви зберегли початковий розмір карти, 10x5. + + ```zokrates + import "utils/pack/bool/pack128.zok" as pack128; + import "hashes/poseidon/poseidon.zok" as poseidon; + + def hashMap(bool[12][7] map) -> field { + bool[512] mut map1d = [false; 512]; + u32 mut counter = 0; + + for u32 x in 0..12 { + for u32 y in 0..7 { + map1d[counter] = map[x][y]; + counter = counter+1; + } + } + + field[4] hashMe = [ + pack128(map1d[0..128]), + pack128(map1d[128..256]), + pack128(map1d[256..384]), + pack128(map1d[384..512]) + ]; + + return poseidon(hashMe); + } + + + // Кількість мін у місці (x,y) + def map2mineCount(bool[12][7] map, u32 x, u32 y) -> u8 { + return if map[x+1][y+1] { 1 } else { 0 }; + } + + def main(private bool[12][7] map, u32 x, u32 y) -> (field, u8) { + return (hashMap(map) , + if map2mineCount(map, x, y) > 0 { 0xFF } else { + map2mineCount(map, x-1, y-1) + map2mineCount(map, x, y-1) + map2mineCount(map, x+1, y-1) + + map2mineCount(map, x-1, y) + map2mineCount(map, x+1, y) + + map2mineCount(map, x-1, y+1) + map2mineCount(map, x, y+1) + map2mineCount(map, x+1, y+1) + } + ); + } + ``` + +3. Скомпілюйте код Zokrates і створіть ключ верифікації. Ключ верифікації має бути створений з тією ж ентропією, що й у вихідному сервері, [у цьому випадку — порожній рядок](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/zero-knowledge.ts#L67). + + ```sh copy + zokrates compile --input dig.zok + zokrates setup -e "" + ``` + +4. Створіть верифікатор Solidity самостійно і переконайтеся, що він функціонально ідентичний тому, що знаходиться в блокчейні (сервер додає коментар, але це неважливо). + + ```sh copy + zokrates export-verifier + diff verifier.sol ~/20240901-secret-state/packages/contracts/src/verifier.sol + ``` + +## Рішення щодо дизайну {#design} + +У будь-якому достатньо складному застосунку існують конкуруючі цілі дизайну, що вимагають компромісів. Давайте розглянемо деякі з компромісів і чому поточне рішення є кращим за інші варіанти. + +### Чому нульове розголошення? {#why-zero-knowledge} + +Для гри «Сапер» вам насправді не потрібне нульове розголошення. Сервер завжди може тримати карту, а потім просто розкрити її всю, коли гра закінчиться. Тоді, наприкінці гри, смартконтракт може обчислити хеш карти, перевірити, чи він збігається, і якщо ні — покарати сервер або повністю проігнорувати гру. + +Я не використовував це простіше рішення, оскільки воно працює лише для коротких ігор з чітко визначеним кінцевим станом. Коли гра потенційно нескінченна (як у випадку з [автономними світами](https://0xparc.org/blog/autonomous-worlds)), вам потрібне рішення, яке доводить стан, _не_ розкриваючи його. + +Як посібник, ця стаття потребувала короткої гри, яку легко зрозуміти, але ця техніка є найбільш корисною для довших ігор. + +### Чому Zokrates? {#why-zokrates} + +[Zokrates](https://zokrates.github.io/) — не єдина доступна бібліотека з нульовим розголошенням, але вона схожа на звичайну, [імперативну](https://en.wikipedia.org/wiki/Imperative_programming) мову програмування та підтримує логічні змінні. + +Для вашого застосунку з іншими вимогами ви можете віддати перевагу використанню [Circum](https://docs.circom.io/getting-started/installation/) або [Cairo](https://www.cairo-lang.org/tutorials/getting-started-with-cairo/). + +### Коли компілювати Zokrates {#when-compile-zokrates} + +У цій програмі ми компілюємо програми Zokrates [кожного разу, коли запускається сервер](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/zero-knowledge.ts#L60-L61). Це, безумовно, марна трата ресурсів, але це посібник, оптимізований для простоти. + +Якби я писав застосунок для виробництва, я б перевірив, чи є у мене файл зі скомпільованими програмами Zokrates для цього розміру мінного поля, і якщо так, то використовував би його. Те саме стосується розгортання контракту верифікатора в ончейні. + +### Створення ключів верифікатора та доказувача {#key-creation} + +[Створення ключів](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/zero-knowledge.ts#L63-L69) — це ще одне чисте обчислення, яке не потрібно виконувати більше одного разу для даного розміру мінного поля. Знову ж таки, це робиться лише один раз заради простоти. + +Крім того, ми могли б використовувати [церемонію налаштування](https://zokrates.github.io/toolbox/trusted_setup.html#initializing-a-phase-2-ceremony). Перевага церемонії налаштування полягає в тому, що для шахрайства з доказом з нульовим розголошенням вам потрібна або ентропія, або якийсь проміжний результат від кожного учасника. Якщо принаймні один учасник церемонії чесний і видаляє цю інформацію, докази з нульовим розголошенням захищені від певних атак. Однак _немає механізму_ для перевірки того, що інформація була видалена звідусіль. Якщо докази з нульовим розголошенням є критично важливими, ви захочете взяти участь у церемонії налаштування. + +Тут ми покладаємося на [perpetual powers of tau](https://github.com/privacy-scaling-explorations/perpetualpowersoftau), у яких були десятки учасників. Це, ймовірно, достатньо безпечно і набагато простіше. Ми також не додаємо ентропію під час створення ключа, що полегшує користувачам [перевірку конфігурації з нульовим розголошенням](#user-verify-zero-trust). + +### Де перевіряти {#where-verification} + +Ми можемо перевіряти докази з нульовим розголошенням або в ончейні (що коштує газу), або в клієнті (використовуючи [`verify`](https://zokrates.github.io/toolbox/zokrates_js.html#verifyverificationkey-proof)). Я вибрав перший варіант, оскільки це дозволяє [перевірити верифікатор](#user-verify-zero-trust) один раз, а потім довіряти, що він не зміниться, доки адреса контракту для нього залишається незмінною. Якби перевірка проводилася на клієнті, вам довелося б перевіряти код, який ви отримуєте кожного разу, коли завантажуєте клієнт. + +Крім того, хоча ця гра є однокористувацькою, багато блокчейн-ігор є багатокористувацькими. Ончейн-перевірка означає, що ви перевіряєте доказ з нульовим розголошенням лише один раз. Робити це в клієнті вимагало б, щоб кожен клієнт перевіряв незалежно. + +### Вирівнювати карту в TypeScript чи Zokrates? {#where-flatten} + +Загалом, коли обробка може бути виконана або в TypeScript, або в Zokrates, краще робити це в TypeScript, який набагато швидший і не вимагає доказів з нульовим розголошенням. З цієї причини, наприклад, ми не надаємо Zokrates хеш і не змушуємо його перевіряти, чи він правильний. Хешування має виконуватися всередині Zokrates, але зіставлення повернутого хеша з хешем в ончейні може відбуватися поза ним. + +Однак ми все одно [вирівнюємо карту в Zokrates](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/zero-knowledge.ts#L15-L20), тоді як могли б зробити це в TypeScript. Причина в тому, що інші варіанти, на мою думку, гірші. + +- Надати одновимірний масив логічних значень коду Zokrates і використовувати вираз, такий як `x*(height+2) + +y`, щоб отримати двовимірну карту. Це зробило б [код](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/zero-knowledge.ts#L44-L47) дещо складнішим, тому я вирішив, що приріст продуктивності не вартий цього для посібника. + +- Надіслати Zokrates і одновимірний, і двовимірний масив. Однак це рішення нічого нам не дає. Код Zokrates мав би перевіряти, чи наданий одновимірний масив дійсно є правильним представленням двовимірного масиву. Тож приросту продуктивності не було б. + +- Вирівняти двовимірний масив у Zokrates. Це найпростіший варіант, тому я його вибрав. + +### Де зберігати карти {#where-store-maps} + +У цьому застосунку [`gamesInProgress`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/app.ts#L20) — це просто змінна в пам'яті. Це означає, що якщо ваш сервер вийде з ладу і потребуватиме перезапуску, вся збережена в ньому інформація буде втрачена. Гравці не тільки не зможуть продовжити свою гру, вони навіть не зможуть розпочати нову гру, оскільки ончейн-компонент вважає, що у них все ще є незавершена гра. + +Це, безумовно, поганий дизайн для виробничої системи, в якій ви б зберігали цю інформацію в базі даних. Єдина причина, чому я використав тут змінну, — це те, що це посібник, і простота є головним міркуванням. + +## Висновок: за яких умов ця техніка є доречною? {#conclusion} + +Отже, тепер ви знаєте, як написати гру з сервером, який зберігає секретний стан, що не належить до ончейну. Але в яких випадках варто це робити? Є два основних міркування. + +- _Довготривала гра_: [як згадувалося вище](#why-zero-knowledge), у короткій грі ви можете просто опублікувати стан, коли гра закінчиться, і все перевірити тоді. Але це не варіант, коли гра триває довго або невизначений час, і стан повинен залишатися секретним. + +- _Деяка централізація є прийнятною_: докази з нульовим розголошенням можуть перевіряти цілісність, тобто те, що суб'єкт не підробляє результати. Чого вони не можуть зробити, так це гарантувати, що суб'єкт все ще буде доступним і відповідатиме на повідомлення. У ситуаціях, коли доступність також потребує децентралізації, докази з нульовим розголошенням не є достатнім рішенням, і вам потрібні [багатосторонні обчислення](https://en.wikipedia.org/wiki/Secure_multi-party_computation). + +[Більше моїх робіт дивіться тут](https://cryptodocguy.pro/). + +### Подяки {#acknowledgements} + +- Альваро Алонсо прочитав чернетку цієї статті та роз'яснив деякі з моїх непорозумінь щодо Zokrates. + +Відповідальність за будь-які помилки, що залишилися, лежить на мені. diff --git a/public/content/translations/uk/developers/tutorials/secure-development-workflow/index.md b/public/content/translations/uk/developers/tutorials/secure-development-workflow/index.md new file mode 100644 index 00000000000..73ae0568d22 --- /dev/null +++ b/public/content/translations/uk/developers/tutorials/secure-development-workflow/index.md @@ -0,0 +1,52 @@ +--- +title: "Список безпеки смарт-контрактів" +description: "Запропонований робочий процес для запису безпечних смарт-контрактів" +author: "Trailofbits" +tags: [ "Смарт-контракти", "захист", "мова програмування" ] +skill: intermediate +lang: uk +published: 2020-09-07 +source: Building secure contracts +sourceUrl: https://github.com/crytic/building-secure-contracts/blob/master/development-guidelines/workflow.md +--- + +## Контрольний список розробки смарт-контрактів {#smart-contract-development-checklist} + +Ось процес високого рівня, який ми рекомендуємо дотримуватися при написанні смарт-контрактів. + +Перевірте відомі проблеми безпеки: + +- Перевірте свої контракти за допомогою [Slither](https://github.com/crytic/slither). Він має більше 40 вбудованих детекторів для поширених вразливостей. Запускайте його під час кожної реєстрації з новим кодом і переконайтесь, що він отримує чистий звіт (або використовуйте режим сортування, щоб приглушити певні проблеми). +- Перевірте свої контракти за допомогою [Crytic](https://crytic.io/). Він перевіряє 50 проблем, які не робить Slither. Crytic також може допомогти вашій команді залишатися один на одному, шляхом легкого виявлення проблем безпеки в Pull Requests на GitHub. + +Розглянемо особливі можливості договору: + +- Ваші контракти оновлені? Перевірте свій код оновлюваності на наявність недоліків за допомогою [`slither-check-upgradeability`](https://github.com/crytic/slither/wiki/Upgradeability-Checks) або [Crytic](https://blog.trailofbits.com/2020/06/12/upgradeable-contracts-made-safer-with-crytic/). Ми задокументували 17 способів, як оновлення можуть піти шкереберть. +- Чи передбачають ваші контракти відповідність ERC? Перевірте їх за допомогою [`slither-check-erc`](https://github.com/crytic/slither/wiki/ERC-Conformance). Цей інструмент миттєво виявляє відхилення від шести поширених видів. +- Чи інтегруєте ви з токенами сторонніх виробників? Перегляньте наш [контрольний список інтеграції токенів](/developers/tutorials/token-integration-checklist/), перш ніж покладатися на зовнішні контракти. + +Візуально перевірте важливі функції безпеки вашого коду: + +- Перегляньте принтер [inheritance-graph](https://github.com/trailofbits/slither/wiki/Printer-documentation#inheritance-graph) від Slither. Уникайте ненавмисного затінення та проблем лінеаризації C3. +- Перегляньте принтер [function-summary](https://github.com/trailofbits/slither/wiki/Printer-documentation#function-summary) від Slither. Він повідомляє про функцію видимість та контроль доступу. +- Перегляньте принтер [vars-and-auth](https://github.com/trailofbits/slither/wiki/Printer-documentation#variables-written-and-authorization) від Slither. Програма повідомляє про контроль доступу до змінних стану. + +Документуйте критичні властивості безпеки та використовуйте автоматичні генератори тестів для їх оцінки: + +- Навчіться [документувати властивості безпеки для вашого коду](/developers/tutorials/guide-to-smart-contract-security-tools/). Для початку це важко, але це єдина найважливіша діяльність для досягнення гарного результату. Також це необхідна умова для використання будь-якої з передових технік у цьому підручнику. +- Визначте властивості безпеки в Solidity для використання з [Echidna](https://github.com/crytic/echidna) та [Manticore](https://manticore.readthedocs.io/en/latest/verifier.html). Зосередьтеся на вашій машині стану, контролі доступом, арифметичних операціях, зовнішній взаємодії та відповідності стандартам. +- Визначте властивості безпеки за допомогою [Python API від Slither](/developers/tutorials/how-to-use-slither-to-find-smart-contract-bugs/). Зосередьтеся на успадкуванні, змінних залежностях, контролі доступу та інших структурних питаннях. +- Запускайте тести властивостей для кожного коміту за допомогою [Crytic](https://crytic.io). Crytic може споживати і оцінювати тести на властивість безпеки, щоб кожен з вашої команди міг легко побачити, що вони передають на GitHub. Помилкові тести можуть блокувати коміти. + +Врешті, пам’ятайте про проблеми, які автоматизовані інструменти не можуть легко знайти: + +- Відсутність приватності: будь-хто може бачити ваші транзакції, поки вони стоять у черзі в пулі +- Фронтальні операції +- Криптографічні операції +- Ризиковані взаємодії із зовнішніми компонентами DeFi + +## Зверніться по допомогу {#ask-for-help} + +[Приймальні години Ethereum](https://calendly.com/dan-trailofbits/office-hours) проводяться щовівторка в другій половині дня. Ці 1-годинні сеанси 1: 1 - це можливість поставити нам будь-які питання щодо безпеки, усунення недоліків за допомогою наших інструментів і отримати відгуки від експертів провашого поточного підходу. Ми допоможемо вам працювати з цим посібником. + +Приєднуйтеся до нашого Slack: [Empire Hacking](https://join.slack.com/t/empirehacking/shared_invite/zt-h97bbrj8-1jwuiU33nnzg67JcvIciUw). Якщо у вас є запитання, ми завжди доступні на каналах #crytic та #ethereum. diff --git a/public/content/translations/uk/developers/tutorials/send-token-ethersjs/index.md b/public/content/translations/uk/developers/tutorials/send-token-ethersjs/index.md new file mode 100644 index 00000000000..9cea9d74bf9 --- /dev/null +++ b/public/content/translations/uk/developers/tutorials/send-token-ethersjs/index.md @@ -0,0 +1,210 @@ +--- +title: "Надсилання токенів за допомогою ethers.js" +description: "Посібник для початківців із надсилання токенів за допомогою ethers.js." +author: Kim YongJun +tags: [ "ETHERS.JS", "ERC-20", "ТОКЕНИ" ] +skill: beginner +lang: uk +published: 2021-04-06 +--- + +## Надсилання токену за допомогою ethers.js(5.0) {#send-token} + +### У цьому посібнику ви дізнаєтеся, як {#you-learn-about} + +- Імпортувати ethers.js +- Переказати токен +- Установити ціну на газ відповідно до ситуації з трафіком у мережі + +### Для початку {#to-get-started} + +Для початку ми маємо спочатку імпортувати бібліотеку ethers.js у наш javascript +Підключіть ethers.js(5.0) + +### Установлення {#install-ethersjs} + +```shell +/home/ricmoo> npm install --save ethers +``` + +ES6 у браузері + +```html + +``` + +ES3(UMD) у браузері + +```html + +``` + +### Параметри {#param} + +1. **`contract_address`**: адреса контракту токена (адреса контракту потрібна, якщо токен, який ви хочете переказати, — не ether) +2. **`send_token_amount`**: сума, яку ви хочете надіслати одержувачу +3. **`to_address`**: адреса одержувача +4. **`send_account`**: адреса відправника +5. **`private_key`**: приватний ключ відправника для підпису транзакції та фактичного переказу токенів + +## Примітка {#notice} + +`signTransaction(tx)` видалено, оскільки `sendTransaction()` робить це внутрішньо. + +## Процедури надсилання {#procedure} + +### 1. Підключення до мережі (тестової мережі) {#connect-to-network} + +#### Налаштувати постачальника (Infura) {#set-provider} + +Підключитися до тестової мережі Ropsten + +```javascript +window.ethersProvider = new ethers.providers.InfuraProvider("ropsten") +``` + +### 2. Створити гаманець {#create-wallet} + +```javascript +let wallet = new ethers.Wallet(private_key) +``` + +### 3. Підключити гаманець до мережі {#connect-wallet-to-net} + +```javascript +let walletSigner = wallet.connect(window.ethersProvider) +``` + +### 4. Отримати поточну ціну на газ {#get-gas} + +```javascript +window.ethersProvider.getGasPrice() // ціна на газ +``` + +### 5. Визначити транзакцію {#define-transaction} + +Змінні, визначені нижче, залежать від `send_token()` + +### Параметри транзакції {#transaction-params} + +1. **`send_account`**: адреса відправника токена +2. **`to_address`**: адреса одержувача токена +3. **`send_token_amount`**: сума токенів для надсилання +4. **`gas_limit`**: ліміт газу +5. **`gas_price`**: ціна на газ + +[Див. нижче, як використовувати](#how-to-use) + +```javascript +const tx = { + from: send_account, + to: to_address, + value: ethers.utils.parseEther(send_token_amount), + nonce: window.ethersProvider.getTransactionCount(send_account, "latest"), + gasLimit: ethers.utils.hexlify(gas_limit), // 100000 + gasPrice: gas_price, +} +``` + +### 6. Переказ {#transfer} + +```javascript +walletSigner.sendTransaction(tx).then((transaction) => { + console.dir(transaction) + alert("Надсилання завершено!") +}) +``` + +## Як це використовувати {#how-to-use} + +```javascript +let private_key = + "41559d28e936dc92104ff30691519693fc753ffbee6251a611b9aa1878f12a4d" +let send_token_amount = "1" +let to_address = "0x4c10D2734Fb76D3236E522509181CC3Ba8DE0e80" +let send_address = "0xda27a282B5B6c5229699891CfA6b900A716539E6" +let gas_limit = "0x100000" +let wallet = new ethers.Wallet(private_key) +let walletSigner = wallet.connect(window.ethersProvider) +let contract_address = "" +window.ethersProvider = new ethers.providers.InfuraProvider("ropsten") + +send_token( + contract_address, + send_token_amount, + to_address, + send_address, + private_key +) +``` + +### Успіх! {#success} + +![зображення успішно виконаної транзакції](./successful-transaction.png) + +## send_token() {#send-token-method} + +```javascript +function send_token( + contract_address, + send_token_amount, + to_address, + send_account, + private_key +) { + let wallet = new ethers.Wallet(private_key) + let walletSigner = wallet.connect(window.ethersProvider) + + window.ethersProvider.getGasPrice().then((currentGasPrice) => { + let gas_price = ethers.utils.hexlify(parseInt(currentGasPrice)) + console.log(`ціна на газ: ${gas_price}`) + + if (contract_address) { + // загальне надсилання токена + let contract = new ethers.Contract( + contract_address, + send_abi, + walletSigner + ) + + // Скільки токенів? + let numberOfTokens = ethers.utils.parseUnits(send_token_amount, 18) + console.log(`кількість токенів: ${numberOfTokens}`) + + // Надіслати токени + contract.transfer(to_address, numberOfTokens).then((transferResult) => { + console.dir(transferResult) + alert("токен надіслано") + }) + } // надсилання ether + else { + const tx = { + from: send_account, + to: to_address, + value: ethers.utils.parseEther(send_token_amount), + nonce: window.ethersProvider.getTransactionCount( + send_account, + "latest" + ), + gasLimit: ethers.utils.hexlify(gas_limit), // 100000 + gasPrice: gas_price, + } + console.dir(tx) + try { + walletSigner.sendTransaction(tx).then((transaction) => { + console.dir(transaction) + alert("Надсилання завершено!") + }) + } catch (error) { + alert("не вдалося надіслати!!") + } + } + }) +} +```