Транзакция на создание смарт-контракта
В данной статье мы познакомимся с тем, как задеплоить очень простой смарт-контракт на локальный блокчейн Ganache. После развёртывания смарт-контракта, мы научимся взаимодействовать с ним путём отправки транзакций в его адрес. Для простоты я буду использовать фреймворк Truffle, так как он упрощает деплой и взаимодействие со смарт-контрактом.
В качестве контракта мы создадим самый простой Faucet. В реальных условиях Faucet используется как хранилище криптовалюты, с которого любой желающий может перевести некоторое количество средств на свой баланс. Используются Faucet в тестовых сетях.
Логика смарт-контракта
Наш контракт будет иметь всего две функции:- Одна для пополнения баланса самого смарт-контракта
- Другая для перевода средств с баланса смарт-контракта на аккаунт, который вызвал эту функцию. В аргументе функции будет передаваться требуемое количество Ether.
Примечание: даже в случае завершения транзакции с ошибкой, с отправителя всё равно будет списано некоторе количество Ether за отправку транзакции. Это плата за Gas, который потребовался для обработки транзакции на нодах. Таким образом исключается недобросовестная нагрузка на сеть, когда злоумышленник нагружает сеть заведомо ошибочными транзакциями, и попусту использует её вычислительную мощность. За такие транзакции он всё равно будет платить, и рано или поздно израсходует все свои Ether. Поэтому, например, контракт с бесконечным циклом не нанесёт особого вреда ресурсам блокчейна.
План
- Создадим контракт
- Пополним баланс контракта с аккаунта A
- Обратимся с аккаунта Б к контракту с просьбой перечислить средства на баланс в желаемом количестве
Итак, приступим и создадим контракт.
Шаг 1. Создание смарт-контракта
Создадим рабочую директорию и инициализируем truffle-проект: Код:
$ mkdir simple-faucet
$ cd simple-faucet
$ truffle init
После инициализации проекта, наш рабочий каталог будет выглядеть следующим образом:
Код:
.
├── contracts
├── migrations
├── test
└── truffle-config.js
4 directories, 1 file
JavaScript:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;
contract Faucet {
// Accept any incoming amount
receive() external payable {}
// Give out ether to anyone who asks
function withdraw(uint withdraw_amount) public {
// Limit withdrawal amount
require(withdraw_amount <= 0.01 ether);
// Send the amount to the address that requested it
payable(msg.sender).transfer(withdraw_amount);
}
}
Я компилировал контракт на версии Solidity 0.8.20, это была последняя версия на момент написания статьи. Вы можете указать версию компилятора в файле truffle-config.js, и тогда при компиляции фреймворк Truffle сам подтянет нужную версию.
Фрагмент файла конфигурации truffle-config.js с версией компилятора 0.8.20:
JavaScript:
// Configure your compilers
compilers: {
solc: {
version: "0.8.20",
// ...
Код:
$ truffle compile
Чтобы фреймворк Truffle понял какой контракт ему задеплоить, мы должны указать ему на наш контракт. Для этого в папке migrations создадим файл
JavaScript:
1_faucet_migration.js и добавим в него следующий код:
var Faucet = artifacts.require("Faucet");
module.exports = function(deployer) {
deployer.deploy(Faucet);
};
Контракт и структура проекта Truffle
У нас почти всё готово для деплоя контракта. Осталось запустить Ganache и добавить одну настройку.Запустим Ganache:
Начальное состояние блокчейна
Для того чтобы Ganache смог визуализировать смарт-контракт, мы должны указать ему на файл конфигурации проекта с контрактом. Имя файла конфигурации: truffle-config.js. Для этого зайдём на вкладку Contracts, и нажмём на LINK TRUFFLE PROJECTS:Вкладка Contracts пока без контрактов
В появившемся окне нажимаем ADD PROJECT, находим папку с нашим проектом и указываем на файл конфигурации truffle-config.js:Окно добавления проекта с контрактом
Затем нажимаем на SAVE AND RESTART.После рестарта во вкладке Contracts должна появиться информация о нашем контракте. Как видим, он ещё не задеплоен:
Контракт появлися, но ещё не задеплоен
Отлично. Теперь у нас всё готово для деплоя контракта. Для этого в консоли набираем: Код:
$ truffle migrate
Код:
Starting migrations...
======================
> Network name: 'ganache'
> Network id: 5777
> Block gas limit: 6721975 (0x6691b7)
1_faucet_migration.js
=====================
⠇ Fetching solc version list from solc-bin. Attempt #1
Deploying 'Faucet'r. Attempt #1.
------------------
> transaction hash: 0x326495056d8c8ae784b1bb35c448f65817bda7a6d0f7d4eb6db2b06b7dd02fbdg compiler. Attempt #1.
> Blocks: 0 Seconds: 0
> contract address: 0x38f409e4A974A7A0B19F707BDCFF56dC3F6Eb0a4
> block number: 1
> block timestamp: 1686649344
> account: 0x5591B981a1133b044B36d82502e838f597b0af6D
> balance: 99.999573572125
> gas used: 126349 (0x1ed8d)
> gas price: 3.375 gwei
> value sent: 0 ETH
> total cost: 0.000426427875 ETH
> Saving artifacts
-------------------------------------
> Total cost: 0.000426427875 ETH
Summary
=======
> Total deployments: 1
> Final cost: 0.000426427875 ETH
Если посмотреть выше, то мы увидим поле account: 0x5591B981a1133b044B36d82502e838f597b0af6D, а под ним есть поле balance: 99.999573572125. Зайдём в Ganache и найдём этот аккаунт. Это будет первый аккаунт. Почему именно первый аккаунт, ведь мы не указывали никаких дополнительных данных в Truffle при деплое контракта? Ответ заключается в том, что у Ganache есть аккаунт по-умолчанию, и по дефолту это первый аккаунт. Ниже я покажу как осуществлять транзакции от имени других аккаунтов.
Раз контракт задеплоен, и мы разобрались с кого была списана пошлина за создание контракта, тогда давайте зайдём в Ganache и убедимся, что баланс первого аккаунта уменьшился:
Ganache GUI не отражает списание мелких сумм
Кажется, что-то тут не так. Как видим, ни один баланс не изменился. Дело в том, что Ganache при отрисовке баланса округляет его в большую сторону, и поэтому мы не видим списания мелких сумм. Для того чтобы точно узнать баланс, мы можем посмотреть его из консоли простым вызовом web3.js библиотеки, которая входит в состав фреймворка Truffle. Для этого войдём в консоль Truffle: Код:
$ truffle console
Код:
truffle(ganache)>
Проверяем баланс:
JavaScript:
> web3.eth.getBalance('0x5591B981a1133b044B36d82502e838f597b0af6D');
// Out: '99999573572125000000'
Давайте ещё раз зайдём в Ganache и посмотрим на статус нашего контракта:
Статус контракта: DEPLOYED
Статус сменился на DEPLOYED. Провалимся в сам контракт. Видим адрес контракта и его баланс:Баланс созданного контракта: 0 ETH
Можем зайти во вкладку Transactions и посмотреть на саму транзакцию, создавшую контракт:Транзакция, которая создала контракт
Контракт задеплоен, а это значит, что пора переходить к пополнению его баланса.Шаг 2. Пополнение баланса контракта
Пополним баланс нашего контракта, скажем, на 70 Ether. Переведём средства с первого аккаунта. Адрес отправителя и адрес контракта можно взять из Ganache GUI.Транзакция на пополнение баланса контракта:
Код:
> web3.eth.sendTransaction({from: "0x5591B981a1133b044B36d82502e838f597b0af6D", to: "0x38f409e4A974A7A0B19F707BDCFF56dC3F6Eb0a4", value: web3.utils.toWei("70", "ether")});
Код:
{
transactionHash: '0xf07947fc8346f297cde01e75ce241c090cc7beeee285b2df13fce213f4c416e5',
transactionIndex: 0,
blockNumber: 2,
blockHash: '0x7d3aae3081f531a87026ab3d7b52c87fe33ded0e4ab65e1e56a0fbb997265cff',
from: '0x5591b981a1133b044b36d82502e838f597b0af6d',
to: '0x38f409e4a974a7a0b19f707bdcff56dc3f6eb0a4',
cumulativeGasUsed: 21055,
gasUsed: 21055,
contractAddress: null,
logs: [],
logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
status: true,
effectiveGasPrice: 3269736716,
type: '0x2'
}
Баланс первого аккаунта:
JavaScript:
> web3.eth.getBalance("0x5591B981a1133b044B36d82502e838f597b0af6D");
// Out: '29999504727818444620'
JavaScript:
> web3.eth.getBalance("0x38f409e4A974A7A0B19F707BDCFF56dC3F6Eb0a4");
// Out: '70000000000000000000'
Баланс первого аккаунта после отправки 70 ETH на контракт
А баланс контракта пополнился на 70 Ether:Баланс контракта пополнился на 70 ETH
Кстати, зачисление средств на контракт мы произвели обычной транзакцией, которая ничем не отличается от перевода средств с EOA аккаунта на другой EOA аккаунт, и мы явно не вызывали никаких функций контракта. Это вовсе не означает, что контракт так же как и EOA, всегда может принимать Ether на свой счёт. Здесь в дело вступила разновидность fallback функций - функция receive(), которая была специально добавлена в Solidity именно для целей явного перечисления Ether на контракт.Существует ещё одна разновидность fallback функции, и она существовала до введения функции receive(). C помощью неё можно точно так же перевести средства на контракт:
fallback() external payable {}
Функция fallback() отрабатывает тогда, когда мы вызываем конкретный метод, но его нет в вызываемом контракте, а если в транзакции в поле value были ещё и Ether, то они зачисляются на баланс контракта. Средства будут зачислены на контракт, только в случае наличия ключевого слова payable.
Если же мы решили отправить Ether на контракт в котором нет ни одной fallback функции, то данный контракт не может принимать Ether через обычные транзакции, а транзакция завершится с ошибкой.
Контракт может содержать и две fallback функции одновременно. Логику работы этих функций можно кратко изобразить схемой:
Код:
Ether sent to contract
|
msg.data empty ?
/ \
yes no
/ \
receive() exists? fallback()
/ \
yes no
/ \
receive() fallback()
Таким образом функция receive() была специально создана именно для случаев намеренного пополнения баланса контракта, чтобы более старая функция fallback() не выполняла несколько задач одновременно (принцип единой ответственности).
Отлично, наш Faucet теперь имеет средства на балансе, и может выполнять своё назначение, а именно - позволить остальным воспользоваться этими средствами.Примечание: при изучении, вы можете воспользоваться Web IDE, такой например как Remix IDE, чтобы быстро изучить поведение контракта, и вручную протестировать интересующие вас кейсы. Такие инструменты как Truffle и Ganache уже нужны для процесса разработки и автоматического тестирования.
Шаг 3. Снятие Ether с баланса контракта
На этом шаге мы обратимся со второго аккаунта к контракту, и вызовем его метод withdraw(), чтобы снять с контракта 0.01 Ether. Для взаимодействия с контрактом я воспользуюсь абстракцией над контрактом, которую получу при помощи фреймворка Truffle, так как это упростит отправку транзакции.Для получения абстракции нашего контракта, в консоли Truffle выполним следующий код:
JavaScript:
> const instance = await Faucet.deployed();
JavaScript:
> instance.withdraw(web3.utils.toWei("0.01", "ether"), {from: '0xc2AdBa94A888cB8c25f48b4b3dAd91F751617157'});
Квитанция транзакции:
JavaScript:
{
tx: '0x0dda5e8f6c1ec125629a4c295765ce143d1be6d9ce91914cfe014b117ba00ef8',
receipt: {
transactionHash: '0x0dda5e8f6c1ec125629a4c295765ce143d1be6d9ce91914cfe014b117ba00ef8',
transactionIndex: 0,
blockNumber: 3,
blockHash: '0xf65ea4b0e29960bd175eb61e7d32766bac7aee1ad33404d17b69070b131e4179',
from: '0xc2adba94a888cb8c25f48b4b3dad91f751617157',
to: '0x38f409e4a974a7a0b19f707bdcff56dc3f6eb0a4',
cumulativeGasUsed: 28565,
gasUsed: 28565,
contractAddress: null,
logs: [],
logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
status: true,
effectiveGasPrice: 3174122382,
type: '0x2',
rawLogs: []
},
logs: []
}
EOA аккаунт после зачисления 0.01 Ether с контракта:
JavaScript:
> web3.eth.getBalance("0xc2AdBa94A888cB8c25f48b4b3dAd91F751617157");
// '100009909331194158170'
Контракт после снятия с него 0.01 Ether:
JavaScript:
> web3.eth.getBalance("0x38f409e4A974A7A0B19F707BDCFF56dC3F6Eb0a4");
// '69990000000000000000'
Транзакция вызова метода withdraw() у контракта
Баланс контракта после снятия 0.01 Ether:С баланса контракта ушло 0.01 ETH на EOA аккаунт
Баланс второго аккаунта после зачисления с контракта 0.01 Ether:Баланс второго аккаунта после перечисления ему 0.01 ETH с контракта
Все наши транзакции снизу вверх. Создание контракта, перевод средств на контракт и вызов метода контракта для снятия средств:Все транзакции
Кстати, если мы зедеплоим наш смарт-контракт в тестовой сети, то любой желающий может списать с него Ether, ну или пополнить баланс контракта.На этом всё. Наше введение во взаимодействие с контрактом подошло к концу. Мы научились создавать контракт в локальном блокчейне Ganache, узнали как взаимодействовать с контрактом, и познакомились с fallback функциями.
Если осталось что то непонятно или не знаете как подключить к локальной сети эфира - в следующих статьях расскажу
Благодарность все так же выражаю - wakarimasen