Skip to main contentA-REX Foundation

Whitepaper · 07

Хуки и эталонные модули

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

Модули — это демонстрации, а не стандарт

Отношение между A-REX Core и его модулями такое же, как между ERC-3643 и модулями комплаенса T-REX: интерфейс, через который подключаются модули, стандартизирован; модули — нет. Конформный токен без хуков является допустимым токеном A-REX. Хук, написанный кем угодно, совместим с любым конформным токеном. Именно это позволяет экосистеме расти без изменения стандарта.

graph LR
    T["ARexToken<br/>конформный токен"] -->|"check / onExecuted / canSend / canReceive"| H1[EligibilityHook]
    T --> H2[ExpiryHook]
    T --> H3[AttestationHook]
    T --> H4["Пользовательский хук"]
    H3 --> A[ARexAttestations]

Контракт хука

Обратный вызовКогдаДолжен
checkдо изменения балансов, для каждого затронутого id, хуки в порядке регистрации, остановка на первом ненулевом значениибыть view-функцией; никогда не откатываться из-за политики
onExecutedпосле изменения балансов, для каждого id, по порядкуне вызывать функции токена, изменяющие баланс (токен заблокирован)
canSend / canReceiveпри каждом переводе и для view-функций ERC-7943 токенабыть view-функциями

op принимает одно из значений OP_MINT (1), OP_TRANSFER (2), OP_BURN (3), OP_FORCED_TRANSFER (4), OP_TRANSFORM (5). В преобразовании входы приходят с to == 0, а выходы — с from == 0. data — это данные перевода ERC-1155, abi.encode(processRef) для преобразований и пусто для принудительных переводов.

Вызывающий токен — это msg.sender. Хук, которому нужна партия, читает IARexBatch(msg.sender).batchOf(id). Хук, привязанный к одному токену при развёртывании (ADR-025, Предложено), сообщает его через token() и отклоняет вызовы с любого другого адреса.

Когда data читают несколько правил, ADR-023 (Предложено) делает их помеченным конвертом: до 8 записей (bytes32 key, bytes32 value), ключи с пространствами имён, как у кодов (keccak256("AREX:DELIVERY_ID"), keccak256("AREX:DOCUMENT_REF")), и каждое правило читает только свой ключ. data длиной 32 байта сохраняют единственное значение, которое они раньше имели для каждого правила; всё остальное означает отсутствие значения, и чтение никогда не откатывается. Правило, написанное до появления конверта, читает его как отсутствие значения, поэтому отправитель строит конверт, только если все правила токена, читающие data, читают конверты. Эталонные DeliveryHook и TraceabilityHook делают это начиная с релиза 2.0.0-alpha.7.

Эталонные хуки

EligibilityHook — разрешение, блокировка, санкции

Демонстрирует HOOK_ELIGIBILITY. Ведёт необязательный список разрешённых адресов, список заблокированных адресов и санкционный список; canSend/canReceive возвращают их конъюнкцию; check возвращает SANCTIONED, SENDER_INELIGIBLE или RECEIVER_INELIGIBLE, чтобы интерфейсы могли объяснить отказ. Принудительные переводы разрешены всегда: изъятие товара со счёта, попавшего под санкции, и есть смысл принудительного исполнения. В промышленной эксплуатации списки наполняются из реестра идентичности (ONCHAINID, EAS, верифицируемые учётные данные) или санкционного оракула; хуку безразлично, из какого.

ExpiryHook — скоропортящесть без отдельного модуля

Не хранит состояния. Читает expiresAt и producedAt из партии. Блокирует эмиссию и переводы просроченных партий (BATCH_EXPIRED), но не сжигание (списание) и не принудительные переводы (принудительное исполнение). Опционально блокирует переводы ещё не произведённых партий (BATCH_NOT_PRODUCED): платформа, финансирующая до уборки урожая, устанавливает флаг так, чтобы разрешать их; спотовая торговая площадка — так, чтобы блокировать. Этот единственный хук покрывает блокирующую часть скоропортящести; кривые порчи становятся вспомогательной view-функцией или офчейн-оценкой.

AttestationHook — регуляторные ограничения по товарам

Сопоставляет код товара с обязательной темой аттестации. Перед переводом читает партию, находит требование, запрашивает у реестра аттестаций isAttested и в противном случае возвращает ATTESTATION_MISSING. Правила «кофе нужна ссылка на должную осмотрительность по EUDR», «партиям с маркировкой органической продукции нужен действующий сертификат органической продукции», «скоту нужен санитарный сертификат» — это по одному вызову setRequirement каждое. Начиная с эталонного релиза 2.0.0-alpha.7 его владелец может включить checksTransform (по умолчанию выключено): тогда входным партиям transform тоже нужен их сертификат, и держатель с TRANSFORMER_ROLE не может передать несертифицированную партию другому аккаунту в виде новой партии. Выходы — новые партии, и они не проверяются.

PerishabilityHook — модели порчи

Конфигурация порчи по коду товара с переопределением для отдельной партии; модели NONE, LINEAR (до нижнего порога к моменту истечения срока), EXPONENTIAL (период полураспада) и STEP (ступенчатое снижение через каждый интервал). View-функции valueBps, secondsRemaining, isExpired. Блокирует эмиссию и переводы просроченных партий (BATCH_EXPIRED) и переводы партий, остаточная стоимость которых равна нулю (PERISH:NO_VALUE); никогда не блокирует сжигание или принудительное исполнение.

SeasonalHook — окна сбора урожая и торговли

Абсолютные окна [start, end] по товару и операции (не более 16 на ключ). Сочетание товара и операции без окон не ограничено. Типичное использование: OP_MINT ограничивается окном сбора урожая, OP_TRANSFER — окном торговли; в остальных случаях возвращается SEASON_CLOSED. Начиная с эталонного релиза 2.0.0-alpha.7 менеджер удаляет одно окно по его границам (removeWindow), а не по позиции, а завершившиеся окна освобождают место: когда ключ заполнен, добавление окна сначала удаляет его завершившиеся окна. Каждое удаление генерирует WindowRemoved, поэтому индексатор может восстановить окна по событиям.

DeliveryHook — хранение и расчёты

Отправитель планирует доставку amount единиц получателю через перевозчика; единицы блокируются (счётчик O(1), поэтому check никогда не перебирает историю). Перевозчик и датчики фиксируют забор груза, транзит и показания температуры (флаг нарушения); получатель подтверждает доставку; споры передаются арбитру. Расчёт — это сам перевод ERC-1155: отправитель → получатель, точное количество, с номером доставки в data (32 байта, abi.encode(deliveryId), или начиная с релиза 2.0.0-alpha.7 запись AREX:DELIVERY_ID конверта); check разрешает его только тогда, когда доставка находится в состоянии DELIVERED, а onExecuted переводит её в SETTLED и снимает блокировку. Свободные (неблокированные) единицы продолжают перемещаться. Заблокированные единицы нельзя перевести, сжечь или использовать как входы transform: check возвращает IN_TRANSIT для любой операции, которая опустила бы баланс держателя ниже заблокированного количества, кроме расчётного перевода, эмиссии и принудительных переводов. После того как принудительный перевод изъял заблокированные единицы, check по-прежнему возвращает код, а не откатывается. Начиная с эталонного релиза 2.0.0-alpha.7 единицы, замороженные токеном (getFrozenTokens из ERC-7943), нельзя запланировать к доставке (freeUnits показывает, сколько единиц свободно); доставку, единицы которой забрал принудительный перевод, нельзя подтвердить или принять как доставленную (LockedUnitsMoved), поскольку её расчёт уже никогда не состоится, но её всё ещё можно оспорить и вернуть; а диапазон температур, минимум которого не ниже максимума, отклоняется (InvalidRange).

TraceabilityHook — журнал событий в форме EPCIS

Наблюдает за каждой операцией токена и классифицирует её по бизнес-шагам CBV (commissioning, shipping, destroying, transforming) и состояниям (active, in_transit, destroyed, seized); RECORDER_ROLE записывает критические события отслеживания, не связанные с токеном (охлаждение, упаковка, приёмка), с readPoint и bizLocation. Два режима: только генерация событий (около +17 k газа на перевод; индексатор восстанавливает журнал) или сохранение (около +184 k; getEvents отвечает ончейн, постранично). Версия 2 хука, вошедшая в эталонный релиз 2.0.0-alpha.7, при развёртывании привязывается к одному токену: только этот токен сообщает об операциях, а recordEvent принимает только партии, существующие в нём, поэтому никто не может делать записи от имени токена. Его EPCISEvent содержит все поля записи (операцию, точку считывания, местоположение и того, кто сделал запись), поэтому режим только генерации событий ничего не теряет. Ссылка на документ перемещения берётся из data длиной 32 байта или из записи AREX:DOCUMENT_REF конверта, но никогда из записи другого правила. Хук нельзя обновить на месте: экземпляры версии 1 и их события остаются как были, а версия 2 — это новое развёртывание.

CompanyFactsHook — собственные записи компании

Демонстрирует только HOOK_EXECUTED: никогда не отклоняет операцию, его onExecuted никогда не откатывается, и он привязан к одному токену. Ведёт собственные записи компании так, как их описывает дескриптор словаря стандарта (interop/VOCABULARY.md, «Records on the ledger»): факты о партии, площадке или компании (recordHash и не более 8 открытых значений), отметки этапов (солёные ссылки на этапы в битовых индексах 0–255, одна или до 50 за транзакцию), этапы, полученные из токена другой компании, ежедневные якоря (корень Меркла и количество) и аннулирования. Каждая запись несёт approvalHash — хеш записи компании о том, кто её утвердил. Safe компании регистрирует определения и этапы; записи пишет ключ регистратора, который можно делегировать. При transform он переносит на выходные партии этапы, пройденные каждой исходной партией. Этап типа процесса добавляется только для processRef, зафиксированного в том же блоке (commitRun), и каждая фиксация засчитывается один раз. Так же переносятся модель цепочки поставок (cocModel, наименьшая из исходных) и покрытие EUDR (eudrCovered, логическое И исходных). События: FactRecorded, StageRecorded, ReceivedStageRecorded, Anchored, FactVoided, RunCommitted, StagesCarried, CustodyDeclared. Эталонный релиз 2.0.0-alpha.9.

RequireStageHook — никаких поставок без обязательных этапов

Демонстрирует HOOK_CHECK поверх CompanyFactsHook. До 16 требований, в каждом указаны операции, на которые оно распространяется (поставки и, по желанию, входы transform), товары и этапы, которые партия должна пройти. Партия, у которой нет одного из них, отклоняется с REQUIRE_STAGE:MISSING; партия или модуль этапов, который он не может прочитать, — с REQUIRE_STAGE:UNREADABLE, но никогда не откатом. Эмиссия, сжигание и перемещения по решению суда или органа власти никогда не отклоняются. У каждого требования свой appliesFrom: партии, созданные раньше, не проверяются. Safe компании задаёт требования через configure, поэтому начальная точка может измениться, когда требования заменяются; каждое изменение порождает событие Configured. Эталонный релиз 2.0.0-alpha.9.

Сопутствующие расширения

Три задачи никогда не требовали оформления в виде правил перевода; это обычные контракты, которые читают или записывают аттестации:

РасширениеЧто делает
CommodityPriceOracleрепортёры передают цены по товарам; раунды нумеруются и финализируются по кворуму с медианой либо вручную по достижении минимума; latestAnswer, isStale
CarbonImmobilizationWrapperфиксирует кредиты, иммобилизованные в реестре (реестр, диапазон серийных номеров, винтаж, тонны, доказательство), связывает тонны с партиями, отражает погашение и высвобождение, записывает аттестацию AREX:CARBON:IMMOBILIZED; никогда не эмитирует кредиты
ParametricInsuranceпул ERC-20; полис на партию с темой-триггером; страховое требование выплачивается, когда триггер аттестован до истечения срока

Старые и новые лоты

Правило, включённое для токена, у которого уже есть лоты, распространяется либо на все лоты, либо только на лоты, созданные начиная с определённого момента. Эталонные ExpiryHook, AttestationHook, SeasonalHook и PerishabilityHook получают этот момент (appliesFrom, 0 — для всех лотов) при развёртывании. Изменить его потом нельзя, поэтому никто не сможет позже вывести из-под правила дополнительные лоты; другой выбор означает новое развёртывание правила. Исключение — RequireStageHook: у каждого его требования свой appliesFrom, и Safe компании может заменить требования через configure.

Описание правила для систем, которые его используют

Правило или дополнение может опубликовать рядом со своим кодом дескриптор arex.module/1. Это версионируемый JSON-документ, в котором названы параметры и настройки контракта, поля партии, которые он читает, ожидаемые типы сертификатов, его события и коды отказа, а также то, как его состояние переносится в новую версию. Платформа, кошелёк, индексатор или аудитор читает его, чтобы показать правило, настроить его и объяснить отказ без кода, написанного специально для этого правила. Начиная с редакции 1.1 дескриптор может также перечислять операции, которыми управляет правило (ops: mint, transfer, burn, forced_transfer, transform). Поле носит информационный характер: если его нет, операции неизвестны, и ни один потребитель не может опираться на него для принуждения к правилам, потому что решают собственные проверки хука. Редакция 1.2 добавляет две вещи. Настройка может указывать место (ref:site): хранится originRef, который несут созданные там партии, или ссылка на внецепочечное свидетельство происхождения, которую они несут вместо него (например, декларация о должной осмотрительности по EUDR), но никогда не адрес и не координаты. Кроме того, заявленный газ check и onExecuted измеряется с заполненными настройками: перед измерением валидатор вызывает каждую операцию, которая задаёт или добавляет значение, setup раз на ключ с тестовыми значениями (8, если дескриптор не указывает иное, не более 32), так что правило, перебирающее настраиваемый список, заявляет свою стоимость при заполненном списке. Ревизия 1.3 фиксирует то, что первый потребитель дескриптора уже использует, чтобы его дескрипторы проходили проверку по опубликованной схеме: действия, которые люди записывают через утверждение (записи дополнения, каждая с ролью, которая может её вызвать), собственные ошибки контракта словами, показатели по его записям, данные по каждой поставке, которые читает правило, полную форму его событий с полями и текстами, а также новые типы значений (партия, запись модуля, короткий текст, текст, хранимый только по отпечатку). Каждое добавленное поле необязательно, поэтому любой прежний дескриптор остаётся действительным. Ревизия 1.4 описывает каждое значение настолько полно, что по нему можно построить форму: новые типы значений (сумма с валютой, дата и время, документ, хранимый по отпечатку, код с форматом, например GTIN, страна или регион, единица измерения, человек или сторона справочника), что попадает в реестр (само значение, только его отпечаток или ничего; персональные данные никогда), заголовок, подсказка, пример, раздел и условия каждого поля, проверки между полями из закрытого списка, вычисляемые значения, сведения вне реестра, представления только для чтения рядом с партией и то, как показывается запись: всегда как заявление организации, которая её записала, и никогда как проверенная. Редакция 1.5 добавляет соль к каждому отпечатку: дескриптор, объявляющий fingerprint: { "scheme": "salted-jcs/1" }, записывает хеш каждого значения вместе с собственной 32-байтовой солью, поэтому короткие тексты, числа или контуры участков нельзя подобрать перебором. Ревизия 1.6 добавляет показатели, которые не являются суммами: помимо count и sum, показателем может быть среднее, наименьшее или наибольшее значение (avg, min, max) числового поля записей. Им нужно это числовое поле, и они никогда не складываются между группами или компаниями: среднее по двум компаниям — это среднее их записей, а не среднее их средних. Показатели также можно группировать по стороне, создавшей партию (party), и по её площадке (site). Шаблон дополнения влажности в эталонном релизе 2.0.0-alpha.10 объявляет такой показатель: среднюю влажность при приёмке по продукту и площадке. Показатель над значением, записанным в базисных пунктах, объявляет unit: "bp", и потребитель показывает его делённым на 100, в процентах. Редакция 1.7 позволяет visibleWhen поля использовать дерево условий дескриптора словаря над другими полями той же формы: листья { field, op, value }, соединённые через all, any и not, не более 3 уровней и 12 листьев, никогда не выражение и никогда не контекст записи. Одиночное условие редакции 1.4 остаётся допустимым, считается устаревшим и читается как равнозначный лист.

Дескриптор информативен и имеет статус «Предложено»: его предложила A-REX Platform, его первый потребитель, и он станет рекомендуемым форматом, когда его начнёт использовать второй, независимый разработчик реализации. Поля, которые правило добавляет в документ метаданных партии, располагались бы в extensions.<key> и проверялись бы фрагментом схемы из дескриптора (ADR-017, Предложено, для схемы метаданных 1.3.0).

Организация может также опубликовать собственный словарь в виде определений arex.vocab/1: виды полевых работ (обрезка, отбор проб почвы), виды процессов (72-часовая ферментация, сортировка по размеру сита) и списки этапов для каждого продукта (сбор урожая, сушка, шелушение, сортировка, экспорт). Это данные, а не код. Каждое определение использует объект поля из дескриптора, указывает свою версию и сопоставлено с эталонным классом этапа и бизнес-шагом GS1 CBV, поэтому читатель, не знающий организацию, всё равно его понимает. Вид процесса — это transform с типом, а не новая операция: его processRef несёт 4-байтовую метку типа и отпечаток значений запуска, поэтому правило может действовать по типу, а любой может проверить запуск. Записи попадают в реестр по одной или в виде ежедневного корня Меркла, каждое значение зафиксировано со своей солью, так что организация может раскрыть одни поля и сохранить другие. Совпадающий отпечаток показывает, что запись не менялась с момента внесения, но никогда не показывает, что работа выполнена так, как заявлено. Формат носит информативный характер и имеет статус «Предложено» (редакция 1.5 дескриптора словаря). Редакция 1.1 добавляет необязательный form, который раскладывает поля по разделам и ширинам и задаёт поведение каждого поля (условия, значения по умолчанию, значения только для чтения и вычисляемые значения, маски и шаблоны); он лишь сужает то, что принимают поля, поэтому читатель, который его игнорирует, читает каждую запись как прежде. Редакция 1.2 даёт условиям один небольшой язык, общий с дескриптором правила: not, до 3 уровней и 12 листьев, а также листья, которые читают то, что платформа знает о записи, в кодах, одинаково читаемых на любом языке (культура как код EPPO, дни с посадки, вид площадки, страна, товарная позиция ТН ВЭД (HS) продукта партии, её последний этап). Неизвестный контекст читается как пустой, и каждое условие редакции 1.1 остаётся допустимым в том виде, в каком оно записано. Редакция 1.3 позволяет списку этапов указать свой порядок (order): при strict, значении по умолчанию, партия не записывается на этапе, пока для неё не записан обязательный предыдущий этап, считая с того места, где партия вошла в список; при flexible этапы записываются в любом порядке, а пропуски остаются отчётам. Этап, который следует из собственной истории партии (её регистрация, преобразование, приёмка), никогда не отклоняется, и каждый прежний список читается как strict. Редакция 1.4 позволяет типу процесса указать, какой метод обработки кофе несут его выходы (washed, natural, honey с жёлтым, красным и чёрным вариантами, pulped_natural, wet_hulled, anaerobic, other), взятый из поля запуска или заданный типом, а метаданные process партии могут нести метод (method; mixed только для партии). Это заявление организации, а не сертификация, и оно сопровождает партию даже там, где значения шагов остаются закрытыми. Редакция 1.5 позволяет сопоставлению с одним из широких классов separating, conditioning, sorting_grading или heat_treating добавить subclass из закрытого списка для каждого класса (например, hulling, curing, screen, roasting), и читатель сопоставляет этап другой организации из широкого класса только по его подклассу или по явному сопоставлению для каждого отправителя. Ревизия 1.6 добавляет to_processor, подкласс transporting: товар, отправленный переработчику, который обрабатывает его для владельца (давальческая переработка), так что меняется владение, но не право собственности.

Написание собственного хука

Пример contracts/examples/SeasonHook.sol остаётся в репозитории. test/standard/ImplementationGuide.test.js проверяет его для читателей с доступом. SeasonalHook — эталонный модуль для нескольких окон на товар и на операцию.

Практические правила:

  1. Символы OK и OP_FORCED_TRANSFER определены в нормативном тексте.
  2. Поведение сжигания определено в нормативном тексте.
  3. Обратный вызов check определён в нормативном тексте.
  4. Имена кодов, такие как ARexCodes.code("MYNS", "NAME"), определены в нормативном тексте.
  5. Значения флагов определены в нормативном тексте.
  6. Обратные вызовы onExecuted и check определены в нормативном тексте.

Регистрация и порядок

Дешёвые правила с высокой долей отказов следует ставить первыми. HOOK_ADMIN_ROLE следует размещать за мультиподписью или таймлоком: тот, кто контролирует конвейер, контролирует, какие переводы возможны.

Тестирование

test/standard/helpers.js развёртывает эталонный стек. RecordingHook записывает каждый обратный вызов и может получить указание отклонять с любым кодом; ReentrantHook пытается повторно войти в токен из onExecuted и подтверждает работу блокировки. DomainHooks.test.js и Extensions.test.js покрывают перенесённые хуки и сопутствующие контракты; Invariants.test.js выполняет случайную последовательность операций с зафиксированным seed и проверяет сохранение предложения, терминальность статуса и то, что canTransfer предсказывает каждый исход. Начиная с эталонного релиза 2.0.0-alpha.8 эталонная реализация также содержит набор для хуков (conformance/hooks/) — наборы тестов Foundry, которые должно пройти любое правило, — и шаблон модуля (templates/module/). Релиз 2.0.0-alpha.9 добавляет набор для дополнений (conformance/addons/) и три шаблона дополнений, проверенных на нём: влажность при приёмке (GrainMoisture), сертификат справедливой торговли, записанный как заявленный компанией факт (FairTradeCertificate), и премия, выплаченная производителю, сумма и получатель которой записываются только как солёные отпечатки (PremiumPayments).

Взаимодействие с комплаенсом ERC-3643

Модуль комплаенса ERC-3643 предоставляет moduleCheck(from, to, value, compliance). Хук, который вызывает его для OP_TRANSFER и сопоставляет false с кодом, делает ограничения по странам, лимиты баланса и временные блокировки, написанные для токенов-ценных бумаг, пригодными для повторного использования на токенах партий. Обратное — предоставление хука A-REX токену ERC-3643 — столь же механично. Chainlink Automated Compliance Engine интегрируется по тому же шаблону: его движок политик становится одним хуком. Оба этих направления — нереализованные проекты. Существующий эталонный адаптер работает в обратную сторону и только читает: он позволяет кошелькам и инструментам комплаенса ERC-3643 видеть каждую партию как токен ERC-3643 через хук, который только получает уведомления и не может разрешить или запретить перевод (ADR-020, Предложено).