«Умные контракты» - неправильное название. Несмотря на слово «умный» (перевод с английского слова ‘smart’), смарт-контракты в сети Ethereum не являются самоисполняющимися цифровыми соглашениями. Код смарт-контракта запускается только при участии внешнего источника воздействия. Другими словами, для запуска смарт-контракта необходим внешний процесс.

В этой статье мы выстроим решение этой проблемы. Вы узнаете:
- Зачем нам нужна внесетевая автоматизация смарт-контрактов;
- Где можно применить автоматизацию смарт-контрактов;
- Как развернуть бессерверные функции с помощью бессерверной платформы.
Наконец, мы рассмотрим «бессерверные эфиры», готовый к запуску и развертыванию полнофункциональный сервис автоматизации смарт-контрактов. Вы можете использовать этот продукт в качестве основы для создания автоматизированных смарт-контрактов, соответствующих вашим потребностям.
Бессерверные эфиры (открытый исходный код доступен на Github. Просто скопируйте и нажмите кнопку развернуть!)
Дальше вы узнаете, зачем нам нужна автоматизация и как она работает.
Проблема: смарт-контракты не являются самоисполняющимися
Давайте представим, что нам нужно исполнить смарт-контракт с функцией, которая должна автоматически выполняться каждый час.
Как это можно сделать?
Никак. Это невозможно сделать с помощью простых смарт-контрактов в Solidity. Несмотря на свое название, «умные» контракты в Ethereum не являются самоисполняющимися. Вам нужен внешний источник (человек или машина), чтобы вызвать смарт-контракт и выполнить его код.
Максимум, что может сделать контракт, - это обеспечить 1-часовой интервал между исполнениями, например:
function runMe() public {
require(block.timestamp >= lastTriggeredAt + 1 hour);
...
}
Приведенный выше оператор require () гарантирует, что между исполнениями есть по крайней мере 1 час. В противном случае транзакция возвращается.
Тем не менее, кто-то все еще должен вызвать смарт-контракт в первую очередь для запуска кода.
Небольшое отступление на тему самоисполнения
Чисто технически для автоматического выполнения определенных операций можно использовать модификаторы функций. Одним из примеров этого является распределение блокчейн-компанией Compound управляющих токенов COMP среди пользователей одноименного протокола. Как только условно взятый адрес заработает 0,001 COMP, при каждой транзакции в сети Compound (будь то отправка актива или перевод токена) на его кошелек автоматически будут поступать токены COMP.
Вы можете реализовать описанную выше логику в модификаторе функции (декораторе), обернуть модификатор вокруг функции и заставить логику автоматически выполняться всякий раз, когда вызывается функция. Тот, кто вызывает функцию, платит за газ, необходимый для реализации дополнительной логики.
Однако не все системы смарт-контрактов следуют этому подходу. Одна из причин заключается в том, что это может привести к непредвиденному расходу газа, так как эти модификаторы могут работать только при определенных условиях. Это также приводит к дополнительным издержкам у случайно выбранной подгруппы пользователей, которым просто не повезло оказаться в числе выбранных для «перебалансировки» контракта.
Наконец, кто-то все еще должен вызвать смарт-контракт для запуска кода.
Области, где можно применить автоматизацию смарт-контрактов
Протоколы DeFi уже в некоторой степени прибегают к внесетевой автоматизации смарт-контрактов. MakerDAO использует сторонних Киперов для мониторинга коэффициентов обеспечения долговых позиций и ликвидации любых недозагруженных позиций. Другие протоколы DeFi имеют аналогичные потребности.
Существуют два часто пересекающихся варианта использования внесетевой автоматизации смарт-контрактов:
- Автоматические триггеры: вы хотите выполнить контракт на определенных условиях.
- Мониторинг состояний и событий: вы хотите знать, когда контракт находится в определенном состоянии.
1. Автоматические триггеры
Рассматрим примеры, когда вам нужно исполнять контракт периодически или на определенных условиях, а именно:
- Периодически проводить перебалансировку в пуле;
- Закрывать раунды голосования в процессе управления или DAO;
- Использовать оракулов для обновления данных;
- Выплачивать пропорционально распределяемые дивиденды на инвестиционные токены.
2. Мониторинг состояний и событий
Актуально, если вы хотите знать, соблюдены ли определенные условия. Например:
- Вы хотите знать, изменилось ли значение в смарт-контракте;
- Вы хотите получать уведомления обо всех изменениях Контроля Доступа;
- Вы хотите знать, когда было выпущено конкретное Событие смарт-контракта.
Решение – бессерверные функции?
Приведенные выше примеры использования хорошо подходят для бессерверной функции. Переходя на бессерверный режим, мы можем развертывать код, не подготавливая ничего заранее и не управляя ничем впоследствии. Это максимально упрощает реализацию ваших идей.
Быстрый старт: переход на бессерверную платформу с бессерверным фреймворком
Бессерверная платформа предоставляет вам все необходимое для разработки, развертывания, мониторинга и обеспечения безопасности бессерверных приложений. Мы будем использовать ее, чтобы ускорить развитие нашего проекта и уменьшить интеллектуальные затраты.
> npm install -g serverless
> serverless -v
x.x.x
Давайте быстро рассмотрим, как работает бессерверная платформа.
Вы можете пропустить этот раздел, если вам просто интересен результат. Или читайте дальше, чтобы узнать больше о бессерверной платформе.
0. serverless.yml
Все лямбда-функции и события в вашем Бессерверном сервисе можно найти в конфигурационном файле, называемом serverless.yml. В нем определены функции и события сервиса.
service: serverless-ethers
provider:
name: aws
runtime: nodejs12.x
environment:
CHAIN_ID: 3
DEFAULT_GAS_PRICE: 60000000000
functions:
myFunc:
handler: functions/myFunc.handler
events:
- schedule: rate(2 hours)
Под свойством functions вы устанавливаете свои бессерверные функции. В приведенном выше примере:
- У нас есть функция под названием myFunc;
- Свойство handler указывает на файл и модуль, содержащий код, который вы хотите запустить в своей функции;
- Свойство Events задает триггеры событий для выполнения функции.
У вас может быть несколько функций в одном сервисе.
1. Функции
Функция – это функция AWS Lambda. Это самостоятельная единица развертывания вроде микросервиса. Это просто код, развернутый в облаке, который чаще всего пишется для выполнения одной задачи.
// functions/myFunc.js
exports.handler = async function(event, context) {
// Do anything
};
Функции - это просто обычные функции JS. Они могут принимать объект события в качестве полезной нагрузки.
2. События
События – это то, что запускает ваши функции. События принадлежат каждой функции и могут быть найдены в свойстве events в serverless.yml.
Триггер Запланированных Событий можно использовать для автоматизации периодического выполнения функций. Например, для запуска функции myFunc каждые 2 часа мы указываем:
# serverless.yml
functions:
myFunc:
handler: functions/myFunc.handler
events:
- schedule: rate(2 hours)
Вы также можете задать график с помощью выражений cron schedule:
# serverless.yml
events:
- schedule: cron(0 12 * * ? *) # 12PM UTC
Если вы используете AWS в качестве провайдера, все события в сервисе - это все, что может вызвать функцию AWS Lambda, например:
- Запрос конечной точки HTTP шлюза API AWS (например, для REST API);
- Загрузка корзины AWS S3 (например, для изображения);
- Таймер CloudWatch (например, для запуска каждые 5 минут);
- Тема AWS SNS (например, сообщение);
- И так далее…
Это все, что вам нужно знать на данный момент.
Чтобы узнать больше о бессерверной платформе, ознакомьтесь с этими документами.
С азами бессерверного фреймворка закончили, давайте теперь перейдем к сервису serverless-ethers.
Введение в serverless-ethers (бессерверные эфиры)
serverless-ethers - это полнофункциональный бессерверный сервис, который можно развернуть и запустить из коробки.
git clone [email protected]:yosriady/serverless-ethers.git
cd serverless-ethers
nvm use
npm install
Вы можете использовать этот продукт в качестве основы для создания автоматических пользовательских смарт-контрактов. Он поставляется предварительно настроенным под AWS, но может быть изменен для работы с другими облачными провайдерами, такими как GCP, Azure и многие другие.
Проект serverless-ethers структурирован следующим образом:
├── contracts/
│ ├── abis/
│ ├── abis.js
│ └── addresses.js
├── functions/
│ └── exec.js
└── serverless.yml
- contracts/ содержат ABI и адреса смарт-контрактов;
- functions/ содержат функции JS, реализующие бизнес-логику;
- serverless.yml описывает конфигурацию сервиса.
В качестве отступления приведу несколько примеров смарт-контрактов.
Я написал и развернул образец смарт-контракта для тестирования:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.6.10;
contract DummyStorage {
event Write(address indexed source, uint256 value);
uint internal _currentValue;
function get() public view returns (uint) {
return _currentValue;
}
function put(uint value) public {
emit Write(msg.sender, value);
_currentValue = value;
}
}
Смарт-контракт DummyStorage имеет следующие функции:
- get – это функция только для чтения, которая возвращает текущее значение контракта.
- put – это функция записи, которая обновляет текущее значение контракта.
Проверенный образец контракта доступен на Ropsten. Можете смело использовать его, чтобы проверить свои функции!
1. Интерфейсы ABI смарт-контракта
Каталог контрактов содержит ABI контрактов, с которыми взаимодействуют функции. В образце проекта он содержит ABI для контракта DummyStorage.
├── contracts/
│ ├── abis/
│ │ └── DummyStorage.json
│ ├── abis.js
│ └── addresses.js
ABI можно еще представить как публичную API-спецификацию смарт-контракта, что-то вроде спецификации OpenAPI. Вам нужен ABI для вызова функций контракта.
Структура контрактов / каталогов позволяет нам импортировать как ABI, так и адрес контракта следующим образом:
// functions/exec.js
const { abis, addresses } = require('../contracts');
const DummyStorageABI = abis.DummyStorage;
const DummyStorageAddress = addresses.DummyStorage;
Они нам понадобятся в нашей работе.
2. Функции
Функция exec использует эфиры для загрузки ABI контракта и вызова смарт-контракта:
// Initialize contract
const contract = new ethers.Contract(
DummyStorageAddress,
DummyStorageABI,
wallet,
)
// Call smart contract function `put(uint)`
const RANDOM_INTEGER = Math.floor(Math.random() * 100); // returns a random integer from 0 to 99
const tx = await contract.put(RANDOM_INTEGER)
Загрузка ABI и адреса контракта дает нам абстракцию ethers.Contract со всеми функциями нашего смарт-контракта, включая get() и put().
На примере функции exec мы вызываем contract.put() с помощью случайного целого числа.
3. serverless.yml
Прежде чем вы сможете запустить функцию exec, вам нужно будет указать некоторые переменные среды в вашем serverless.yml:
# serverless.yml
service: serverless-ethers
provider:
name: aws
runtime: nodejs12.x
region: ap-southeast-1
timeout: 30
environment:
DEFAULT_GAS_PRICE: 60000000000
MNEMONIC: ...
SLACK_HOOK_URL: ...
serverless-ethers использует следующие переменные среды:
- DEFAULT_GAS_PRICE: Цена газа по умолчанию используется при совершении операций записи;
- MNEMONIC: 12-словесная мнемоника, используемая для получения адреса Ethereum. Убедитесь в наличии эфира, если собираетесь записывать данные в Ethereum!
- SLACK_HOOK_URL: Он отправляет сообщения в Slack с помощью входящих веб-крючков. Вы можете извлечь этот URL-адрес из вашей панели мониторинга Slack. (На усмотрение)
Вы можете на ходу менять переменные среды развернутой функции с помощью консоли AWS Lambda.
Важное примечание: убедитесь, что ваши ключи хранятся не в открытом источнике. Используйте безопасное хранилище типа AWS Secrets Manager для хранения учетных данных, таких как мнемоника и ключи API. Поскольку каждый проект имеет свои собственные требования по безопасности и настройке, мы оставляем читателям право решать, как они хотят подойти к вопросу хранения чувствительной информации.
Локальный запуск
Вы можете использовать бессерверную команду CLI invoke local для локального запуска ваших функций. Она отлично подходит для тестирования!
> serverless invoke local -f exec
Starting...
Contract ABIs loaded
Ethers wallet loaded
Contract loaded
Sending transaction...
:white_check_mark: Transaction sent https://ropsten.etherscan.io/tx/0x72204f07911a319b4e5f7eb54ad15ed666cfc1403b53def40c9d60188b176383
Completed
true
Развертывание в AWS
Развертывание выполняется так же просто, как и бессерверное развертывание:
> serverless deploy
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Creating Stack...
Serverless: Checking Stack create progress...
........
Serverless: Stack create finished...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service serverless-ethers.zip file to S3 (2.95 MB)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
.....................
Serverless: Stack update finished...
Service Information
service: serverless-ethers
stage: dev
region: ap-southeast-1
stack: serverless-ethers-dev
resources: 8
api keys:
None
endpoints:
None
functions:
exec: serverless-ethers-dev-exec
layers:
None
Вот и все! Теперь у вас есть бессерверная функция, которую вы можете использовать для автоматизации и мониторинга ваших смарт-контрактов. Вы можете использовать этот шаблон в качестве основы для создания своих собственных автоматизированных смарт-контрактов.
В заключение
Поздравляю! Теперь вы знаете:
- Зачем нужна оффчейн-автоматизация смарт-контрактов;
- Где применима автоматизация смарт-контрактов
- Что такое бессерверная платформа;
- Как работает приложение serverless-ethers.
Если у вас остались какие-то вопросы или есть желание поделиться своим мнением, обращайтесь! Мне было бы интересно узнать, как вы намерены использовать автоматизацию смарт-контрактов.
Пример приложения serverless-ethers с открытым исходным кодом доступен на Github. Добавляйте репозитарий в избранное, если вы нашли его полезным!
Ну и на десерт: поддержка ChatOps с помощью Slack
В комплект serverless-ethers входит функция postToSlack, которая поможет вам интегрироваться с Slack.
const successMessage = `:white_check_mark: Transaction sent https://ropsten.etherscan.io/tx/${tx.hash}`;
await postToSlack(successMessage);
Функция postToSlack позволяет использовать переменную среды SLACK_HOOK_URL, которую вы можете извлечь из консоли Slack. После настройки вы сможете оправлять уведомления в Slack о каждом успешном выполнении операции.
Это удобный и простой способ контролировать свои функции.
Дополнительно: мониторинг Событий смарт-контрактов
Пока мы успели реализовать только вариант использования «автоматического триггера». А как насчет мониторинга состояний и событий смарт-контракта?
Вы можете использовать API событий Ethers v5 для периодического мониторинга определенных событий. В своей функции вы можете сделать следующее:
// Given the following Event:
// event Transfer(bytes32 indexed node, address owner)
// Get the filter (the second null could be omitted)
const filter = contract.filters.Transfer(userAccount, null);
// Query the filter
const logs = contract.queryFilter(filter, 0, "latest"); // from block 0 to latest block
// Print out all the values:
logs.forEach((log) => {
console.log(log.args._to, log.args._value);
});
Предполагая, что вы хотите, чтобы эта функция выполнялась периодически (например, каждые 5 минут), вам также нужно будет сохранить флаг, который отслеживает последний блок, который функция видела с момента последнего выполнения.
Мониторинг событий особенно полезен, если у вас имеется белый список для Контроля Доступа, который вам нужно отслеживать. С помощью функции мониторинга событий вы можете уведомлять канал Slack всякий раз, когда новые адреса будут заноситься в белый список для целей администрирования. Очень удобно!