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]Контракт хука
interface IARexHook is IERC165 {
function hookFlags() external view returns (uint8); // CHECK=1 · EXECUTED=2 · ELIGIBILITY=4
function check(uint8 op, address operator, address from, address to, uint256 id, uint256 amount, bytes calldata data)
external view returns (bytes32 code); // 0 = allowed
function onExecuted(uint8 op, address operator, address from, address to, uint256 id, uint256 amount, bytes calldata data) external;
function canSend(address account) external view returns (bool);
function canReceive(address account) external view returns (bool);
}
| Обратный вызов | Когда | Должен |
|---|---|---|
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).
Эталонные хуки
EligibilityHook — разрешение, блокировка, санкции
Демонстрирует HOOK_ELIGIBILITY. Ведёт необязательный список разрешённых адресов, список заблокированных адресов и санкционный список; canSend/canReceive возвращают их конъюнкцию; check возвращает SANCTIONED, SENDER_INELIGIBLE или RECEIVER_INELIGIBLE, чтобы интерфейсы могли объяснить отказ. Принудительные переводы разрешены всегда: изъятие товара со счёта, попавшего под санкции, и есть смысл принудительного исполнения. В промышленной эксплуатации списки наполняются из реестра идентичности (ONCHAINID, EAS, верифицируемые учётные данные) или санкционного оракула; хуку безразлично, из какого.
ExpiryHook — скоропортящесть без отдельного модуля
Не хранит состояния. Читает expiresAt и producedAt из партии. Блокирует эмиссию и переводы просроченных партий (BATCH_EXPIRED), но не сжигание (списание) и не принудительные переводы (принудительное исполнение). Опционально блокирует переводы ещё не произведённых партий (BATCH_NOT_PRODUCED): платформа, финансирующая до уборки урожая, устанавливает флаг так, чтобы разрешать их; спотовая торговая площадка — так, чтобы блокировать. Этот единственный хук покрывает блокирующую часть скоропортящести; кривые порчи становятся вспомогательной view-функцией или офчейн-оценкой.
AttestationHook — регуляторные ограничения по товарам
Сопоставляет код товара с обязательной темой аттестации. Перед переводом читает партию, находит требование, запрашивает у реестра аттестаций isAttested(id, topic) и в противном случае возвращает ATTESTATION_MISSING. Правила «кофе нужна ссылка на должную осмотрительность по EUDR», «партиям с маркировкой органической продукции нужен действующий сертификат органической продукции», «скоту нужен санитарный сертификат» — это по одному вызову setRequirement каждое.
PerishabilityHook — модели порчи
Конфигурация порчи по коду товара с переопределением для отдельной партии; модели NONE, LINEAR (до нижнего порога к моменту истечения срока), EXPONENTIAL (период полураспада) и STEP (ступенчатое снижение через каждый интервал). View-функции valueBps, secondsRemaining, isExpired. Блокирует эмиссию и переводы просроченных партий (BATCH_EXPIRED) и переводы партий, остаточная стоимость которых равна нулю (PERISH:NO_VALUE); никогда не блокирует сжигание или принудительное исполнение.
SeasonalHook — окна сбора урожая и торговли
Абсолютные окна [start, end] по товару и операции (не более 16 на ключ). Сочетание товара и операции без окон не ограничено. Типичное использование: OP_MINT ограничивается окном сбора урожая, OP_TRANSFER — окном торговли; в остальных случаях возвращается SEASON_CLOSED.
DeliveryHook — хранение и расчёты
Отправитель планирует доставку amount единиц получателю через перевозчика; единицы блокируются (счётчик O(1), поэтому check никогда не перебирает историю). Перевозчик и датчики фиксируют забор груза, транзит и показания температуры (флаг нарушения); получатель подтверждает доставку; споры передаются арбитру. Расчёт — это сам перевод ERC-1155: отправитель → получатель, точное количество, с data = abi.encode(deliveryId); check разрешает его только тогда, когда доставка находится в состоянии DELIVERED, а onExecuted переводит её в SETTLED и снимает блокировку. Свободные (неблокированные) единицы продолжают перемещаться.
TraceabilityHook — журнал событий в форме EPCIS
Наблюдает за каждой операцией токена и классифицирует её по бизнес-шагам CBV (commissioning, shipping, destroying, transforming) и состояниям (active, in_transit, destroyed, seized); RECORDER_ROLE записывает критические события отслеживания, не связанные с токеном (охлаждение, упаковка, приёмка), с readPoint и bizLocation. Два режима: только генерация событий (около +17 k газа на перевод; индексатор восстанавливает журнал) или сохранение (около +184 k; getEvents отвечает ончейн, постранично).
Сопутствующие расширения
Три задачи никогда не требовали оформления в виде правил перевода; это обычные контракты, которые читают или записывают аттестации:
| Расширение | Что делает |
|---|---|
CommodityPriceOracle | репортёры передают цены по товарам; раунды нумеруются и финализируются по кворуму с медианой либо вручную по достижении минимума; latestAnswer, isStale |
CarbonImmobilizationWrapper | фиксирует кредиты, иммобилизованные в реестре (реестр, диапазон серийных номеров, винтаж, тонны, доказательство), связывает тонны с партиями, отражает погашение и высвобождение, записывает аттестацию AREX:CARBON:IMMOBILIZED; никогда не эмитирует кредиты |
ParametricInsurance | пул ERC-20; полис на партию с темой-триггером; страховое требование выплачивается, когда триггер аттестован до истечения срока |
Написание собственного хука
Хук ниже — это contracts/examples/SeasonHook.sol; он компилируется в CI, а test/standard/ImplementationGuide.test.js проверяет его на токене. Для промышленного использования предназначен SeasonalHook, поддерживающий несколько окон на товар и на операцию.
contract SeasonHook is ARexHookBase, Ownable {
mapping(bytes32 => uint64[2]) public window; // commodity → [open, close]
constructor(address owner_) Ownable(owner_) {}
function setWindow(bytes32 commodity, uint64 open, uint64 close) external onlyOwner {
window[commodity] = [open, close];
}
function hookFlags() public pure override returns (uint8) {
return ARexCodes.HOOK_CHECK;
}
function check(uint8 op, address, address, address, uint256 id, uint256, bytes calldata)
external
view
override
returns (bytes32)
{
if (op != ARexCodes.OP_TRANSFER) return ARexCodes.OK; // never block mints, burns or enforcement
IARexBatch.Batch memory b = IARexBatch(msg.sender).batchOf(id);
uint64[2] memory w = window[b.commodity];
if (w[1] == 0) return ARexCodes.OK; // no window configured
return (block.timestamp >= w[0] && block.timestamp <= w[1]) ? ARexCodes.OK : ARexCodes.SEASON_CLOSED;
}
}
Практические правила:
- Возвращать
OKдляOP_FORCED_TRANSFER, если хук сам не регулирует принудительное исполнение. - Не блокировать сжигание без явного намерения: сжигание — это доставки и списания.
- Держать
checkдешёвым и ограниченным; он выполняется при каждом переводе каждого id. Никогда не перебирать историю. - Использовать пространства имён для кодов:
ARexCodes.code("MYNS", "NAME"). - Сохранять флаги константными; токен их кэширует.
- Изменения состояния размещать в
onExecuted, но никогда вcheck.
Регистрация и порядок
await token.registerHook(sanctionsHook); // evaluated first
await token.registerHook(expiryHook);
await token.registerHook(attestationHook); // evaluated last
await token.getHooks(); // ordered list
await token.removeHook(expiryHook); // order of the rest preserved
Дешёвые правила с высокой долей отказов следует ставить первыми. HOOK_ADMIN_ROLE следует размещать за мультиподписью или таймлоком: тот, кто контролирует конвейер, контролирует, какие переводы возможны.
Тестирование
test/standard/helpers.js развёртывает эталонный стек. RecordingHook записывает каждый обратный вызов и может получить указание отклонять с любым кодом; ReentrantHook пытается повторно войти в токен из onExecuted и подтверждает работу блокировки. DomainHooks.test.js и Extensions.test.js покрывают перенесённые хуки и сопутствующие контракты; Invariants.test.js выполняет случайную последовательность операций с зафиксированным seed и проверяет сохранение предложения, терминальность статуса и то, что canTransfer предсказывает каждый исход.
Взаимодействие с комплаенсом ERC-3643
Модуль комплаенса ERC-3643 предоставляет moduleCheck(from, to, value, compliance). Хук, который вызывает его для OP_TRANSFER и сопоставляет false с кодом, делает ограничения по странам, лимиты баланса и временные блокировки, написанные для токенов-ценных бумаг, пригодными для повторного использования на токенах партий. Обратное — предоставление хука A-REX токену ERC-3643 — столь же механично. Chainlink Automated Compliance Engine интегрируется по тому же шаблону: его движок политик становится одним хуком.