Транзакция на создание смарт-контракта
Транзакция на создание смарт-контракта

В данной статье мы познакомимся с тем, как задеплоить очень простой смарт-контракт на локальный блокчейн Ganache. После развёртывания смарт-контракта, мы научимся взаимодействовать с ним путём отправки транзакций в его адрес. Для простоты я буду использовать фреймворк Truffle, так как он упрощает деплой и взаимодействие со смарт-контрактом.

В качестве контракта мы создадим самый простой Faucet. В реальных условиях Faucet используется как хранилище криптовалюты, с которого любой желающий может перевести некоторое количество средств на свой баланс. Используются Faucet в тестовых сетях. В одной из своих прошлых статей я показывал как использовать реальный Faucet.

Логика смарт-контракта

Наш контракт будет иметь всего две функции:

  • Одна для пополнения баланса самого смарт-контракта

  • Другая для перевода средств с баланса смарт-контракта на аккаунт, который вызвал эту функцию. В аргументе функции будет передаваться требуемое количество Ether.

Во второй функции будет стоять ограничение - не более 0.01 ETH за одну транзакцию. Если будет запрошено больше, то транзакция завершится с ошибкой.

Примечание: даже в случае завершения транзакции с ошибкой, с отправителя всё равно будет списано некоторе количество Ether за отправку транзакции. Это плата за Gas, который потребовался для обработки транзакции на нодах. Таким образом исключается недобросовестная нагрузка на сеть, когда злоумышленник нагружает сеть заведомо ошибочными транзакциями, и попусту использует её вычислительную мощность. За такие транзакции он всё равно будет платить, и рано или поздно израсходует все свои Ether. Поэтому, например, контракт с бесконечным циклом не нанесёт особого вреда ресурсам блокчейна.

План

  1. Создадим контракт

  2. Пополним баланс контракта с аккаунта A

  3. Обратимся с аккаунта Б к контракту с просьбой перечислить средства на баланс  в желаемом количестве

Подразумевается, что у вас уже установлен Ganache и Truffle. Если нет, вы можете ознакомиться с установкой в моей предыдущей статье. Если такие понятия как EOA, Contract account, транзакция, Gas для вас незнакомы, то вы можете обратится к моим предыдущим статьям, ссылку на которые я оставлю в конце руководства.

Итак, приступим и создадим контракт.

Шаг 1. Создание смарт-контракта

Создадим рабочую директорию и инициализируем truffle-проект:

$ mkdir simple-faucet
$ cd simple-faucet
$ truffle init

После инициализации проекта, наш рабочий каталог будет выглядеть следующим образом:

.
├── contracts
├── migrations
├── test
└── truffle-config.js

4 directories, 1 file

С назначением этих каталогов мы разберёмся по ходу дела, а пока в папке contracts создадим наш контракт и назовём его Faucet.sol:

// 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);
    }
}

Вторая строка pragma solidity определяет совместимость контракта с разными версиями компиляторов. В моём случае я указал все версии 0.8.X. Solidity очень динамично развивающийся язык, и как правило версии 0.X и 0.Y несовместимы друг с другом. Если вы попытаетесь скомпилировать наш контракт, скажем версией компилятора 0.7.6, то компилятор после прочтения строки pragma solidity сразу выдаст ошибку совместимости, и не приступит к дальнейшей компиляции.

Я компилировал контракт на версии Solidity 0.8.20, это была последняя версия на момент написания статьи. Вы можете указать версию компилятора в файле truffle-config.js, и тогда при компиляции фреймворк Truffle сам подтянет нужную версию.

Фрагмент файла конфигурации truffle-config.js с версией компилятора 0.8.20:

 // Configure your compilers
  compilers: {
    solc: {
      version: "0.8.20",
      // ...

Скомпилируем наш контракт:

$ truffle compile

После успешной компиляции в папке build/contracts должен появиться файл Faucet.json - это файл для внутренних нужд фреймворка, который нужен для деплоя смарт-контракта в блокчейн, а также для упрощения взаимодействия с контрактом из кода приложения.

Чтобы фреймворк Truffle понял какой контракт ему задеплоить, мы должны указать ему на наш контракт. Для этого в папке migrations создадим файл 1_faucet_migration.js и добавим в него следующий код:

var Faucet = artifacts.require("Faucet");

module.exports = function(deployer) {
  deployer.deploy(Faucet);
};

После проделанных шагов, ваше окружение должно выглядеть так:

Контракт и структура проекта Truffle
Контракт и структура проекта Truffle

У нас почти всё готово для деплоя контракта. Осталось запустить Ganache и добавить одну настройку.

Запустим Ganache:

Начальное состояние блокчейна
Начальное состояние блокчейна

Для того чтобы Ganache смог визуализировать смарт-контракт, мы должны указать ему на файл конфигурации проекта с контрактом. Имя файла конфигурации: truffle-config.js. Для этого зайдём на вкладку Contracts, и нажмём на LINK TRUFFLE PROJECTS:

Вкладка Contracts пока без контрактов
Вкладка 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

Отлично. Контракт задеплоен. В самом низу, в разделе 'Summary' видим поле 'Final cost', а рядом количество в Ether, равное 0.000426427875 ETH. Это пошлина в виде Gas за создание контракта. А теперь вопрос. Кто заплатил эту пошлину?

Если посмотреть выше, то мы увидим поле account: 0x5591B981a1133b044B36d82502e838f597b0af6D, а под ним есть поле balance: 99.999573572125. Зайдём в Ganache и найдём этот аккаунт. Это будет первый аккаунт. Почему именно первый аккаунт, ведь мы не указывали никаких дополнительных данных в Truffle при деплое контракта? Ответ заключается в том, что у Ganache есть аккаунт по-умолчанию, и по дефолту это первый аккаунт. Ниже я покажу как осуществлять транзакции от имени других аккаунтов.

Раз контракт задеплоен, и мы разобрались с кого была списана пошлина за создание контракта, тогда давайте зайдём в Ganache и убедимся, что баланс первого аккаунта уменьшился:

Ganache GUI не отражает списание мелких сумм
Ganache GUI не отражает списание мелких сумм

Кажется, что-то тут не так. Как видим, ни один баланс не изменился. Дело в том, что Ganache при отрисовке баланса округляет его в большую сторону, и поэтому мы не видим списания мелких сумм. Для того чтобы точно узнать баланс, мы можем посмотреть его из консоли простым вызовом web3.js библиотеки, которая входит в состав фреймворка Truffle. Для этого войдём в консоль Truffle:

$ truffle console

Должна появится строка:

truffle(ganache)>

В примерах кода ниже, если речь будет идти именно о вызове команд в консоли Truffle, я буду писать символ > перед вызовом команды.

Проверяем баланс:

> web3.eth.getBalance('0x5591B981a1133b044B36d82502e838f597b0af6D');

// Out: '99999573572125000000'

Отлично, видим, что баланс изменился ровно на то количество Ether, которое было в поле 'Final cost'. Для выхода из консоли Truffle можно использовать Ctrl + D. Но она нам ещё пригодится на следующих шагах, поэтому можно её пока не закрывать.

Давайте ещё раз зайдём в Ganache и посмотрим на статус нашего контракта:

Статус контракта: DEPLOYED
Статус контракта: DEPLOYED

Статус сменился на DEPLOYED. Провалимся в сам контракт. Видим адрес контракта и его баланс:

Баланс созданного контракта: 0 ETH
Баланс созданного контракта: 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'
}

Транзакции прошла успешно. Проверим балансы EOA и контракта.

Баланс первого аккаунта:

web3.eth.getBalance("0x5591B981a1133b044B36d82502e838f597b0af6D")

// Out: '29999504727818444620'

Баланс контракта:

web3.eth.getBalance("0x38f409e4A974A7A0B19F707BDCFF56dC3F6Eb0a4")

// Out: '70000000000000000000'

В Ganache мы тоже видим, что с отправителя списалось 70 Ether:

Баланс первого аккаунта после отправки 70 ETH на контракт
Баланс первого аккаунта после отправки 70 ETH на контракт

А баланс контракта пополнился на 70 Ether:

Баланс контракта пополнился на 70 ETH
Баланс контракта пополнился на 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()

msg.data - это поле data в теле транзакции, в которой обычно передаётся информация о вызываемом методе и его аргументах, а в случае создания контракта, то код самого контракта.

Таким образом функция receive() была специально создана именно для случаев намеренного пополнения баланса контракта, чтобы более старая функция fallback() не выполняла несколько задач одновременно (принцип единой ответственности).

Примечание: при изучении, вы можете воспользоваться Web IDE, такой например как Remix IDE, чтобы быстро изучить поведение контракта, и вручную протестировать интересующие вас кейсы. Такие инструменты как Truffle и Ganache уже нужны для процесса разработки и автоматического тестирования.

Отлично, наш Faucet теперь имеет средства на балансе, и может выполнять своё назначение, а именно - позволить остальным воспользоваться этими средствами.

Шаг 3. Снятие Ether с баланса контракта

На этом шаге мы обратимся со второго аккаунта к контракту, и вызовем его метод withdraw(), чтобы снять с контракта 0.01 Ether. Для взаимодействия с контрактом я воспользуюсь абстракцией над контрактом, которую получу при помощи фреймворка Truffle, так как это упростит отправку транзакции.

Для получения абстракции нашего контракта, в консоли Truffle выполним следующий код:

> const instance = await Faucet.deployed();

Теперь можно обратиться к методу контракта withdraw() простым вызовом:

> instance.withdraw(web3.utils.toWei("0.01", "ether"), {from: '0xc2AdBa94A888cB8c25f48b4b3dAd91F751617157'});

Обратим внимание на дополнительный объект с полем from, который мы передали вторым аргументом в вызов метода. Его нет в сигнатуре оригинального метода в контракте. При помощи этого объекта мы указали фреймворку, что хотим выполнить транзакцию с конкретного аккаунта, в нашем случае со второго. Если бы мы не передали этот объект, то транзакция ушла бы с аккаунта по-умолчанию, то есть с первого.

Квитанция транзакции:

{
  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 с контракта:

> web3.eth.getBalance("0xc2AdBa94A888cB8c25f48b4b3dAd91F751617157");

// '100009909331194158170'

Видим, что зачислилось чуть меньше 0.01 Ether, так как при вызове метода контракта нужно платить за Gas.

Контракт после снятия с него 0.01 Ether:

> web3.eth.getBalance("0x38f409e4A974A7A0B19F707BDCFF56dC3F6Eb0a4");

// '69990000000000000000'

Транзакция с вызовом метода withdraw():

Транзакция вызова метода withdraw() у контракта
Транзакция вызова метода withdraw() у контракта

Баланс контракта после снятия 0.01 Ether:

С баланса контракта ушло 0.01 ETH на EOA аккаунт
С баланса контракта ушло 0.01 ETH на EOA аккаунт

Баланс второго аккаунта после зачисления с контракта 0.01 Ether:

Баланс второго аккаунта после перечисления ему 0.01 ETH с контракта
Баланс второго аккаунта после перечисления ему 0.01 ETH с контракта

Все наши транзакции снизу вверх. Создание контракта, перевод средств на контракт и вызов метода контракта для снятия средств:

Все транзакции
Все транзакции

Кстати, если мы зедеплоим наш смарт-контракт в тестовой сети, то любой желающий может списать с него Ether, ну или пополнить баланс контракта. Как подключаться к тестовой сети я рассказывал здесь.

На этом всё. Наше введение во взаимодействие с контрактом подошло к концу. Мы научились создавать контракт в локальном блокчейне Ganache, узнали как взаимодействовать с контрактом, и познакомились с fallback функциями.

Предыдущие части

Комментарии (0)