Прочитав Часть I, мы теперь должны немного лучше знать, в чем проблемы с аирдропами и почему было бы неплохо использовать историческое состояние из такого проекта, как Relic, давайте разберемся, как именно это реализовать.

В этом примере будет показано, как создать аирдроп для ненадежного распространения нашего нового токена аирдропа в соотношении 1:1 для любых токенов, хранящихся у пользователя в блоке 15 000 000. Наш код будет разделен на сетевой компонент и внешний интерфейс, с которым пользователи смогут легко взаимодействовать.

Мы не будем рассматривать каждую строчку исходника, но по крайней мере рассмотрим основные аспекты того, как что-то делается, чтобы его было легко модифицировать под свои нужды. Вы также можете увидеть готовый пример в разделе action.

Смарт-контракт

Хотя код доступен онлайн, мы рассмотрим его шаг за шагом здесь.

В Solidity мы импортируем несколько вещей, предоставленных Relic:

  • interfaces/IReliquary.sol предоставляет интерфейс к Реликварию, в котором хранятся проверенные исторические данные о состоянии, к которым мы хотим получить доступ.
  • lib/Storage.sol предоставляет помощников для взаимодействия со слотами хранения контрактов
  • lib/FactSigs.sol предоставляет подписи, которые мы должны передать в Реликварий, чтобы указать, какие данные мы пытаемся проверить

И так как мы делаем токен ERC-20 для аирдропа, мы просто импортируем шаблон ERC-20 из OpenZeppelin.

/// SPDX-License-Identifier: MIT
pragma solidity >=0.8.12;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@relicprotocol/contracts/lib/FactSigs.sol";
import "@relicprotocol/contracts/lib/Storage.sol";
import "@relicprotocol/contracts/interfaces/IReliquary.sol";
contract Token is ERC20 {
}

Теперь первое, что нам нужно сделать, это выяснить структуру слота хранилища для нашего токена. Предполагая довольно стандартный токен ERC-20, это, вероятно, просто вопрос определения смещения слота хранилища для чего-то, например, карты balance владельцев для их токенов. Примечательно, что не имеет значения, public это или private, в любом случае он все еще находится в блокчейне!

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

Для USDT мы просто вводим адрес USDT и текущего держателя токена и получаем смещение 2.

Мы можем вставить это в простую вспомогательную функцию public на случай, если пользователи нашего контракта захотят проверить какие-либо внутренние вычисления.

function slotForUSDTBalance(address who) public pure returns (bytes32) {
    return Storage.mapElemSlot(
        bytes32(uint(2)),
        bytes32(uint256(uint160(who)))
    );
}

Имея это в руках, мы можем начать программировать наш смарт-контракт. Мы можем просто запросить у Relic значение в этом слоте хранилища для интересующего нас блока и интерпретировать его как uint256. Поскольку мы запрашиваем значение слота хранилища, мы используем storageSlotFactSig для создания нашего запроса.

Затем мы воспользуемся запросом Реликвария с помощью функции verifyFactNoFee.

(
    bool exists, uint64 version, bytes memory data
) = reliquary.verifyFactNoFee(
    USDT,
    FactSigs.storageSlotFactSig(
        slotForUSDTBalance(who),
        blockNum
    )
);

Здесь нас интересуют exists (чтобы убедиться, что кто-то проверил этот слот для хранения) и data (чтобы увидеть, что находится в слоте для хранения). Поле version можно использовать, если нам важно, какой внутренний доказывающий Relic использовал для доказательства факта, но здесь это не имеет особого значения.

Предполагая, что значение существует, мы можем легко проанализировать data, используя

require(exists, "storage proof missing");
uint priorUSDTBalance = Storage.parseUint256(data);

Это в основном все, что нам нужно — осталось всего несколько кусочков клея и проверки работоспособности. Мы должны гарантировать, что после того, как люди получат свои токены, они не смогут повторить это несколько раз. Это просто требует простой карты, такой как mapping(address => bool) public claimed, чтобы легко отслеживать. Мы можем объединить все вместе в функцию mint и вызвать _mintфункцию OpenZeppelin с priorUSDTBalance, чтобы фактически перевести сумму нашему пользователю.

function mint(address who) external {
    require(claimed[who] == false, "already claimed");
    (bool exists, , bytes memory data) = reliquary.verifyFactNoFee(
        USDT,
        FactSigs.storageSlotFactSig(
            slotForUSDTBalance(who),
            blockNum
        )
    );

    require(exists, "storage proof missing");
    claimed[who] = true;
    uint priorUSDTBalance = Storage.parseUint256(data);
    _mint(who, priorUSDTBalance);
}

Затем несколько других полей, чтобы настроить и установить адрес для USDT и reliquary, а также установить блок, который будет использоваться для нашего снимка окна претензии. Поскольку мы используем USDT, мы также должны установить decimals в нашем контракте, чтобы он возвращал 6, чтобы соответствовать собственному значению USDT.

Вот и все! Вы можете ознакомиться с полным кодом здесь.

Нам также нужно развернуть наш контракт, обязательно передав адреса для Реликвария и USDT, а также указав, какой блок мы хотим использовать для наших проверок.

Внешний интерфейс

Еще раз не стесняйтесь переходить к готовому исходному коду, если хотите.

Чтобы упростить работу пользователей, нам также нужен внешний интерфейс, который они могут использовать для взаимодействия с этим. Главной необходимой функцией здесь является возможность для пользователей подтверждать свои претензии в USDT и запускать аирдроп, не беспокоясь о деталях.

Опять же, SDK Relic упрощают эту задачу. Мы также будем использовать эфиры, чтобы легко взаимодействовать с Ethereum.

import * as ethers from "ethers";
import { RelicClient, utils } from "@relicprotocol/client";

Нам нужно будет сослаться на наши адреса для USDT и наш недавно развернутый контракт Airdrop. Нам также понадобится контракт на несколько вызовов чуть позже.

const addresses = {
    ADUSDT: "0xc2Ed14521e009FDe80FC610375769E0C292FC12d",
    USDT: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
    MAKERDAOMULTICALL: "0xeefBa1e63905eF1D7ACbA5a8513c70307C1cE441",
};

Создать фактическое доказательство довольно просто:

const client = await RelicClient.fromProvider(provider.value);
const prover = await client.storageSlotProver();
const storageSlot = utils.mapElemSlot(
    2,           // The slot we found earlier
    userAddress, // The customer's wallet address
);
const proveTx = await prover.prove(
    BLOCK,
    addresses.USDT,
    storageSlot,
);

Это будет использовать API Web2 Relic для создания доказательства наличия указанного слота хранилища в контракте USDT в указанном блоке. Обратите внимание, что хотя для удобства используется конечная точка Web2, контролируемая Relic, фактическое доказательство проверяется в цепочке, поэтому нет места для манипуляций со стороны централизованного органа. Также можно создать такое же доказательство самостоятельно с помощью узла архива Ethereum, просто это более утомительно.

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

const ADUSDT = new ethers.Contract(addresses.ADUSDT, [
    "function mint(address who) public"
]);
const Multicall = new ethers.Contract(addresses.MAKERDAOMULTICALL, [
    "function aggregate(tuple(address target, bytes callData)[] memory calls) public returns (uint256 blockNumber, bytes[] memory returnData)"
]);
const mintTx = await adusdt.populateTransaction.mint(userAddress);
const multicall = Multicall.connect(signer.value);
proveAndMintTx = [
    { target: proveTx.to, callData: proveTx.data },
    { target: mintTx.to, callData: mintTx.data },
]
const res = await multicall.aggregate(proveAndMintTx);
await res.wait();

Имейте в виду, что функция mint в цепочке должна знать только адрес пользователя. Фактическая стоимость распределенных им токенов полностью зависит от того, что они доказали в предыдущей транзакции.

Чтобы превратить это в удобный веб-интерфейс, нужно еще немного, но это касается основ для выдачи доказательства в Реликварий.

Вот несколько вещей, которые могут помочь пользователю:

  • остановка пользователя, если он уже забрал свои токены (пример кода обрабатывает это)
  • показывая, какими будут результаты проверки и монетного двора, прежде чем делать это в цепочке (пример кода обрабатывает это)
  • только чеканка и не доказательство, если пользователь уже проверил слот

Полный исходный код добавляет больше накладных расходов на использование Vue и чистое взаимодействие с веб-кошельками. Однако основная часть кода проста.

Модификации

Хотя этот пример был простым, можно легко добавить небольшие изменения, чтобы лучше соответствовать их потребностям.

Диапазон блоков

В то время как наш аирдроп обрабатывал только вариант использования держателей токенов из одного блока, вы можете расширить его на более широкий диапазон блоков. К счастью, это довольно просто! Пользователи будут заинтересованы в том, чтобы выбрать для себя блок с наибольшим балансом, поэтому нам просто нужно обновить наш код, чтобы проверить нижний и верхний предел диапазона, а затем изменить функцию mint, чтобы она также принимала номер блока, для которого пользователь требует их airdrop.

Структура выплат

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

import "@openzeppelin/contracts/utils/math/Math.sol";
...
_mint(who, Math.sqrt(priorUSDTBalance));  // sqrt of balance

Любая детерминированная структура выплат может быть легко добавлена ​​с помощью простой модификации нашей функции mint, такой как эта модификация квадратного корня. И что важно, поскольку структура выплат должна быть привязана к блокчейну, она полностью прозрачна и децентрализована. Если создатель аирдропа попытается выплатить больше своим друзьям, логика для этого исключения должна быть зафиксирована.

Оптимизация газа

Команда Relic планирует реализовать две основные оптимизации газа, которые должны снизить стоимость этих доказательств. Во-первых, разрешить проектам фиксировать промежуточные значения доказательства для последующего использования. Например, состояние учетной записи Merkle Root для учетной записи USDT в блоке 15 000 000 может быть зафиксировано и повторно использовано всеми пользователями этого примера раздачи, что значительно сократит расход газа.

К счастью, это изменение не потребует внесения изменений в код. Это изменение может быть сделано прозрачно Relic и обновить код API после завершения, чтобы proveTx использовал наиболее эффективный метод.

Вторая основная оптимизация заключается в том, чтобы не требовать, чтобы проверенное значение слота хранилища хранилось в цепочке для доступа к нему. Поскольку мы выполняем операции проверки и чеканки в одной и той же транзакции, мы можем оптимизировать некоторые из этих затрат. Тем не менее, контракт Airdrop необходимо будет изменить, чтобы воспользоваться преимуществами этой оптимизации.

Критерии приемлемости

Критерии для этого аирдропа были очень простыми, они основывались только на владении одним токеном. Однако существует ряд других критериев, которые можно использовать.

Любой другой тип активности, который можно просмотреть из слота хранилища, можно легко преобразовать в тот же формат — просто измените, какой слот хранилища проверяется, и все готово.

Другие типы активности в настоящее время находятся в разработке для Relic, такие как квитанции о транзакциях и события журнала. (Подпишитесь на нас в Twitter или Discord, чтобы быть в курсе, когда они будут доступны в SDK). С этими критериями, такими как все пользователи, которые взаимодействовали с определенным контрактом, будет так же просто использовать, как и проверку слота хранилища.

Для объединения нескольких критериев приемлемости все немного сложнее. Основная трудность заключается в том, чтобы просто создать для пользователей простой способ выбора применимых к ним критериев. Объединить вместе несколько фактов из Relic в смарт-контракте несложно, но это может быть неэффективно.

Заворачивать

В целом, код, необходимый для работы этого примера, довольно мал! Большая часть сложности просто возникает из-за создания веб-интерфейса, который прост в использовании. Использование Relic для проверки фактов в цепочке — это всего пара строк кода, и генерация доказательств в Javascript аналогична.

Хотя это уже очень мощный примитив для сетевых приложений, Relic находится в стадии интенсивной разработки, чтобы добавить больше функций. Если вы хотите оставаться на связи, чтобы получать обновления, предлагать новые функции, задавать вопросы или просто общаться, присоединяйтесь к нашему Discord или следите за нами в Twitter.

Напоминаем, что вы можете просмотреть исходный код этого (и других!) примеров на нашем Github. Регулярно проверяйте, как добавляются новые проекты, чтобы получить представление о том, что можно построить с помощью Relic.