Skip to main contentA-REX Core

Whitepaper · 11

Руководство по реализации

От чистого клона до развёрнутого, снабжённого хуками и аттестованного токена партии, шаг за шагом. Каждый блок JavaScript в этой главе скопирован из test/standard/ImplementationGuide.test.js и выполняется в CI; если бы блок здесь был неверным, сборка завершилась бы ошибкой.

Как пользоваться этим руководством

Шаги опираются друг на друга и используют общие переменные: token, attestations, подписантов и id, созданные по ходу. При первом чтении их следует читать по порядку. Запустить их самостоятельно можно командой:

npx hardhat test test/standard/ImplementationGuide.test.js
ШагЧто получаетсяКто действуетОсновные вызовы
1 · Подготовкаскомпилированная, протестированная рабочая копияразработчикnpm ci, npm test
2 · Развёртываниепрокси токена + реестр аттестацийразвёртывающая сторонаupgrades.deployProxy, ARexAttestations
3 · Ролиоператоры с минимальными привилегиямиадминистраторgrantRole
4 · Созданиепартия с коммитментом метаданныхэмитентcreateBatch
5 · Чтениезапись партии и предложениелюбойbatchOf, totalSupply(uint256), parentsOf
6 · Переводпроверенный переводдержательtransferCheck, safeTransferFrom
7 · Родословнаяразделение, объединение, переработкадержатель / преобразовательtransform
8 · Статусобновления жизненного цикла и метаданныхроль статусаsetBatchStatus, setBatchMetadata
9 · Хукиупорядоченный конвейер правиладминистратор хуковregisterHook, getHooks
10 · Аттестациядоказательства, разблокирующие переводыдоверенный эмитентsetTrustedIssuer, attest
11 · Санкциидопуск с указанием источника и TTLменеджер допускаsetSanctioned
12 · Принудительное исполнениезаморозка, приостановка, принудительный переводроли ENFORCER / PAUSERsetFrozenTokens, forcedTransfer
13 · Погашениеконец жизненного цикла лотароль статуса, держательsetBatchStatus(…, 255, …), burn
14 · Индексацияграф родословной из событийиндексаторqueryFilter(BatchTransformed)
15 · Обновлениепроверенное обновление реализациироль обновленияvalidateUpgrade, upgradeProxy

1 · Подготовка

Node.js 20 или 22 LTS и npm. Зависимости зафиксированы в package-lock.json (OpenZeppelin 5.3.0); следует использовать npm ci, чтобы собираемая версия совпадала с той, которую собирает CI.

git clone https://github.com/Arex-Corp/A-REX.git && cd A-REX
npm ci
npm run compile
npm test                     # 46 tests: reference implementation, hooks, extensions, invariants, this guide
npm run interface-ids        # prints the ERC-165 ids; they must match the specification

Приведённые ниже фрагменты используют три небольших вспомогательных определения и называют десять аккаунтов. explain преобразует код отказа обратно в его название; список — это набор кодов, определённых в ARexCodes.

const commodity = (scheme, code) => ethers.keccak256(ethers.toUtf8Bytes(`${scheme}:${code}`));
const unit = (u) => ethers.encodeBytes32String(u);
const ref = (text) => ethers.keccak256(ethers.toUtf8Bytes(text));
const REASONS = Object.fromEntries(
  ["PAUSED", "FROZEN", "SENDER_INELIGIBLE", "RECEIVER_INELIGIBLE", "SANCTIONED", "BATCH_NOT_FOUND",
   "BATCH_RETIRED", "BATCH_EXPIRED", "BATCH_NOT_PRODUCED", "ATTESTATION_MISSING", "SEASON_CLOSED",
   "IN_TRANSIT", "LIMIT_EXCEEDED", "DOCUMENT_MISSING"].map((n) => [ethers.keccak256(ethers.toUtf8Bytes(`AREX:${n}`)), n]),
);
const explain = (code) => (code === ethers.ZeroHash ? "OK" : REASONS[code] ?? code);


const [admin, issuer, farmer, buyer, coop, mill, warehouse, certifier, enforcer, custodian] = await ethers.getSigners();

2 · Развёртывание

Эталонный токен обновляется по схеме UUPS, поэтому он развёртывается за прокси ERC-1967 с помощью плагина OpenZeppelin upgrades, который также проверяет раскладку хранилища. Реестр аттестаций — обычный контракт, привязанный к токену.

const ARexToken = await ethers.getContractFactory("ARexToken");
const token = await upgrades.deployProxy(ARexToken, [admin.address, "ipfs://<cid>/{id}.json"], { kind: "uups" });
await token.waitForDeployment();
const attestations = await (await ethers.getContractFactory("ARexAttestations"))
  .deploy(admin.address, await token.getAddress());

Из командной строки тот же стек вместе с хуками допуска и срока годности развёртывается так:

npx hardhat node                                                     # terminal 1
npx hardhat run scripts/deploy-standard.js --network localhost       # token, attestations, 2 hooks
npx hardhat run scripts/deploy-full-standard.js --network localhost  # every hook in order + extensions

или с помощью Hardhat Ignition:

npx hardhat ignition deploy ignition/modules/ARexToken.js --network localhost \
  --parameters '{"ARexTokenModule":{"baseURI":"ipfs://<cid>/{id}.json","allowPreHarvestTransfers":true}}'

Для тестовой сети или L2 в hardhat.config.js определены sepolia, polygon и arbitrum; URL RPC и PRIVATE_KEY задаются в .env (шаблон — в .env.example). ARexToken компилируется в 24,136 байт при optimizer.runs = 1, что укладывается в лимит EIP-170 в 24,576 байт.

3 · Роли

initialize(admin, baseURI) выдаёт все роли admin, что удобно для первого развёртывания и неверно для промышленной эксплуатации. Каждую операционную роль следует выдавать сервису или мультиподписи, которые её исполняют:

await token.grantRole(await token.ISSUER_ROLE(), issuer.address);
await token.grantRole(await token.TRANSFORMER_ROLE(), mill.address);
await token.grantRole(await token.STATUS_ROLE(), warehouse.address);
await token.grantRole(await token.ENFORCER_ROLE(), enforcer.address);
РольЧто разрешаетРекомендуемый держатель
DEFAULT_ADMIN_ROLEвыдача и отзыв любых ролей; setBaseURIмультиподпись управления за таймлоком
UPGRADER_ROLEзамена реализациитот же таймлок
HOOK_ADMIN_ROLEregisterHook, removeHookтот же таймлок (изменения хуков становятся видны до применения)
ENFORCER_ROLEsetFrozenTokens, forcedTransferмультиподпись принудительного исполнения (комплаенс и юристы)
PAUSER_ROLEpause, unpauseключ реагирования на инциденты
ISSUER_ROLEcreateBatchсервис первичного оформления партий
STATUS_ROLEsetBatchStatus, setBatchMetadataсклад, инспектор или реестр
TRANSFORMER_ROLEперерабатывающие преобразования (изменение товара, единицы измерения или количества)переработчики
ATTEST_ADMIN_ROLE (в ARexAttestations)setTrustedIssuer, отзыв любой аттестациисертифицирующий орган или платформа

Когда у каждой роли появился держатель, развёртывающая сторона отказывается от ненужных ей ролей с помощью renounceRole(role, deployer).

4 · Создание партии

Партия — это один идентификатор ERC-1155. Её ончейн-запись мала и фиксирована; всё описательное находится в документе метаданных, хеш которого зафиксирован ончейн (схема: standard/schema/batch-metadata.schema.json).

const metadata = { schemaVersion: "1.0.0", commodity: { scheme: "HS", code: "100199" }, unit: "KGM", quantity: 250000 };
const canonical = JSON.stringify(metadata);                 // RFC 8785 canonicalization in production
const metadataHash = ethers.keccak256(ethers.toUtf8Bytes(canonical));
const uri = "ipfs://<cid>/batch-1.json";                    // wherever you stored `canonical`


const init = {
  commodity: commodity("HS", "100199"),                     // wheat
  unit: unit("KGM"),
  producedAt: Math.floor(Date.parse("2026-03-18T00:00:00Z") / 1000), // 0 = not yet harvested
  expiresAt: 0,                                             // 0 = no expiry
  originRef: ref("DDS-2026-AR-000123"),                     // a reference, never coordinates
  metadataHash,
  uri,
};
const id = await token.connect(issuer).createBatch.staticCall(init, 250_000, farmer.address, "0x");
await (await token.connect(issuer).createBatch(init, 250_000, farmer.address, "0x")).wait();

createBatch.staticCall возвращает id, который назначит транзакция; идентификаторы начинаются с 1 и растут на единицу. Токен отклоняет:

УсловиеОшибка
у вызывающего нет ISSUER_ROLEAccessControlUnauthorizedAccount(account, role)
to — нулевой адресARexInvalidRecipient()
quantity == 0ARexInvalidBatch("ZERO_QUANTITY")
commodity == 0 или unit == 0ARexInvalidBatch("COMMODITY_REQUIRED") / ("UNIT_REQUIRED")
обе даты заданы и expiresAt <= producedAtARexInvalidBatch("EXPIRY_BEFORE_PRODUCTION")
токен приостановлен, получатель не допущен или хук отклоняет OP_MINTEnforcedPause(), ERC7943CannotReceive(to), ARexHookRejected(hook, code)

Соглашения по полям:

ПолеСоглашениеПример
commoditykeccak256("<scheme>:<code>"), схема из HS, FAO, GTIN, UNSPSC или собственнаяkeccak256("HS:100199") — пшеница
unitкод Рекомендации UN/CEFACT 20 в виде bytes32"KGM", "TNE", "LTR", "BX"
producedAtUnix-секунды; 0 для лота, урожай которого ещё не убранфорвардная продажа, дебиторская задолженность по урожаю
expiresAtUnix-секунды; 0 — без срока годностискоропортящаяся продукция
originRefхеш ссылки на доказательства происхождения; никогда не координатыссылка на DDS по EUDR, идентификатор реестра
metadataHashkeccak256 канонического (RFC 8785) документа метаданных
uriгде находится документ; следует делать его коротким, он хранится ончейнipfs://<cid>, GS1 Digital Link

5 · Чтение партии

const batch = await token.batchOf(id);                      // reverts ARexBatchNotFound if unknown
await token.batchExists(id);                                // true / false, never reverts
await token["totalSupply(uint256)"](id);                    // units in circulation (overloaded name)
await token.parentsOf(id);                                  // [] for an original lot
await token.uri(id);                                        // batch URI, or the base URI if empty
await token.isExpired(id);                                  // reference-implementation helper

totalSupply перегружена в ERC-1155 Supply (totalSupply() и totalSupply(uint256)), поэтому в ethers v6 её следует вызывать по сигнатуре. Batch.status — это 0 (ACTIVE), 255 (RETIRED) или определяемое эмитентом значение между ними.

6 · Перевод

Всегда следует сначала выполнять проверку. transferCheck воспроизводит конвейер переводов как view-функция и возвращает первый код отказа; 0x00…00 означает «разрешено». canTransfer — булева версия того же ответа по ERC-7943.

const code = await token.transferCheck(farmer.address, buyer.address, id, 10_000);
if (code !== ethers.ZeroHash) throw new Error(`transfer blocked: ${explain(code)}`);
await token.connect(farmer).safeTransferFrom(farmer.address, buyer.address, id, 10_000, "0x");

Порядок конвейера фиксирован, поэтому код показывает, какое правило не пройдено:

ПорядокПроверкаКод, возвращаемый transferCheckОткат транзакции
1токен приостановленAREX:PAUSEDEnforcedPause()
2отправитель допущен (каждый хук ELIGIBILITY)AREX:SENDER_INELIGIBLEERC7943CannotSend(from)
3получатель допущенAREX:RECEIVER_INELIGIBLEERC7943CannotReceive(to)
4партия существуетAREX:BATCH_NOT_FOUNDARexBatchNotFound(id)
5партия не погашенаAREX:BATCH_RETIREDARexBatchRetired(id)
6amount <= balance − frozenAREX:FROZENERC7943InsufficientUnfrozenBalance(from, id, amount, unfrozen)
7каждый хук CHECK в порядке регистрациикод хукаARexHookRejected(hook, code)

View-функция и транзакция вычисляют проверки в одном и том же порядке, поэтому код всегда указывает проверку, на которой транзакция откатывается первой.

На практике важны две детали. AREX:FROZEN возвращается и тогда, когда у отправителя просто недостаточно единиц, поскольку проверка выполняется по незамороженному балансу. Кроме того, transferCheck передаёт хукам пустые data; хук, читающий данные перевода (например, расчёт в DeliveryHook), может ответить иначе для реальной транзакции.

7 · Родословная: разделение, объединение, переработка

transform — единственный способ получить партию из партий. Он сжигает входы у from, эмитирует выходы на to, записывает parentsOf(output) = inputIds и генерирует одно событие BatchTransformed, которое соответствует EPCIS TransformationEvent. transform.staticCall возвращает новые id.

Переформирование партий сохраняет товар, единицу измерения и общее количество и не требует роли — только владения или одобрения ERC-1155. Разделение одного лота на два для покупателя:

const [lotA, lotB] = await token.connect(farmer).transform.staticCall(
  farmer.address, buyer.address, [id], [400], [init, init], [250, 150], ref("SALE-42"));
await token.connect(farmer).transform(
  farmer.address, buyer.address, [id], [400], [init, init], [250, 150], ref("SALE-42"));

Объединение двух лотов в один совместный лот для кооператива:

const [pooled] = await token.connect(buyer).transform.staticCall(
  buyer.address, coop.address, [lotA, lotB], [250, 150], [init], [400], ref("POOL-7"));
await token.connect(buyer).transform(
  buyer.address, coop.address, [lotA, lotB], [250, 150], [init], [400], ref("POOL-7"));

Переработка меняет товар, единицу измерения или количество, поэтому требует TRANSFORMER_ROLE в дополнение к одобрению. Мельница превращает 1,000 кг пшеницы фермера в 750 кг муки:

const now = (await ethers.provider.getBlock("latest")).timestamp;
const flourInit = { ...init, commodity: commodity("HS", "110100"), producedAt: now };
await token.connect(farmer).setApprovalForAll(mill.address, true);
const [flour] = await token.connect(mill).transform.staticCall(
  farmer.address, mill.address, [id], [1_000], [flourInit], [750], ref("MILL-RUN-7"));
await token.connect(mill).transform(
  farmer.address, mill.address, [id], [1_000], [flourInit], [750], ref("MILL-RUN-7"));
ПравилоЧем обеспечивается
вызывающий — from или одобрен fromtransform
каждый вход существует, не погашен и имеет достаточный незамороженный баланс_update
переформирование: тот же товар и единица измерения, Σ входов = Σ выходовроль не нужна
всё остальноеTRANSFORMER_ROLE, иначе AccessControlUnauthorizedAccount
хуки видят каждое звеноOP_TRANSFORM для сжиганий и эмиссий

Родословная, построенная в этом руководстве:

flowchart LR
    B1["#1 пшеница 250,000 кг<br/>фермер"] -->|"разделение · SALE-42"| B2["#2 пшеница 250"]
    B1 -->|"разделение · SALE-42"| B3["#3 пшеница 150"]
    B2 -->|"объединение · POOL-7"| B4["#4 пшеница 400<br/>кооператив"]
    B3 -->|"объединение · POOL-7"| B4
    B1 -->|"переработка · MILL-RUN-7"| B5["#5 мука 750<br/>мельница"]

8 · Статус и метаданные

Статус меняет только STATUS_ROLE; владение единицами ничего не даёт. Значения 1..254 определяются самостоятельно; их смысл следует публиковать в документе метаданных (status.table). Обновление метаданных заменяет хеш и URI и генерирует BatchMetadataUpdated, поэтому история документов остаётся проверяемой.

await token.connect(warehouse).setBatchStatus(flour, 1, ref("IN_STORAGE"));  // 1..254 are yours
const labReport = { ...metadata, lab: { moisture: 14.2 } };
await token.connect(warehouse).setBatchMetadata(
  flour, ethers.keccak256(ethers.toUtf8Bytes(JSON.stringify(labReport))), "ipfs://<cid>/flour-lab.json");

9 · Хуки

Хуки — независимые контракты, которые токен вызывает в порядке регистрации. Здесь первым выполняется хук допуска, затем хук срока годности, затем правило о том, что кофе требует аттестованного заявления о должной осмотрительности по EUDR до того, как его можно продать.

const eligibility = await (await ethers.getContractFactory("EligibilityHook")).deploy(admin.address);
const expiry = await (await ethers.getContractFactory("ExpiryHook")).deploy(true); // allow pre-harvest
const attestationHook = await (await ethers.getContractFactory("AttestationHook"))
  .deploy(admin.address, await attestations.getAddress());
const COFFEE = commodity("HS", "090111");
const EUDR_DDS = ref("AREX:EUDR:DDS");
await attestationHook.setRequirement(COFFEE, EUDR_DDS);     // coffee needs a due-diligence statement


await token.registerHook(await eligibility.getAddress());      // evaluated first
await token.registerHook(await expiry.getAddress());
await token.registerHook(await attestationHook.getAddress());  // evaluated last
await token.getHooks();                                        // the ordered list
ХукФлагиКонструкторНастраивается через
EligibilityHookCHECK, ELIGIBILITY(admin)setAllowlistEnabled, setAllowlisted, setBlocklisted, setSanctioned
ExpiryHookCHECK(allowPreHarvestTransfers)— (читает запись партии)
AttestationHookCHECK(owner, attestationRegistry)setRequirement(commodity, topic)
PerishabilityHookCHECK(admin)setCommodityDecay, setBatchDecay
SeasonalHookCHECK(admin)addWindow(commodity, op, start, end), clearWindows
DeliveryHookCHECK, EXECUTED(admin)schedule, pickUp, markInTransit, recordReading, confirmDelivered, dispute, resolve
TraceabilityHookEXECUTED(admin, persist)recordEvent (RECORDER_ROLE)

Регистрация отклоняет EOA, контракты без поддержки ERC-165 для IARexHook и хуки, у которых флаги равны 0. Дешёвые хуки, часто выдающие отказ, следует ставить первыми: отказ останавливает конвейер. Написание собственного хука: docs/HOOK-DEVELOPMENT.md.

10 · Аттестации

Аттестация — это (batch, topic) → (issuer, reference, issuedAt, expiresAt, revoked), действительная лишь пока её эмитенту доверяют по данной теме. При действующем требовании из шага 9 лот кофе не может перемещаться, пока заявление не аттестовано:

const coffee = await token.connect(issuer).createBatch.staticCall({ ...init, commodity: COFFEE }, 60, farmer.address, "0x");
await token.connect(issuer).createBatch({ ...init, commodity: COFFEE }, 60, farmer.address, "0x");
explain(await token.transferCheck(farmer.address, buyer.address, coffee, 1));   // "ATTESTATION_MISSING"


await attestations.setTrustedIssuer(EUDR_DDS, certifier.address, true);         // ATTEST_ADMIN_ROLE
await attestations.connect(certifier).attest(coffee, EUDR_DDS, ref("DDS-2026-AR-000123"), 0);
await attestations.isAttested(coffee, EUDR_DDS);                                 // true
explain(await token.transferCheck(farmer.address, buyer.address, coffee, 1));   // "OK"

isAttested ложен, когда аттестации нет, когда она отозвана, когда срок expiresAt прошёл или когда эмитенту больше не доверяют по данной теме. Отозвать может эмитент или ATTEST_ADMIN_ROLE. Темы — произвольные значения bytes32; следует использовать keccak256("<NAMESPACE>:<NAME>"), например AREX:EUDR:DDS или AREX:CERT:ORGANIC_EU.

11 · Санкции и допуск

EligibilityHook отвечает на canSend и canReceive для токена. Санкция записывает свой источник и необязательный срок действия, поэтому санкционная запись может утрачивать силу автоматически.

await eligibility.setSanctioned(buyer.address, true, ethers.encodeBytes32String("OFAC-SDN"), 0);
explain(await token.transferCheck(farmer.address, buyer.address, id, 1));       // "RECEIVER_INELIGIBLE"
await eligibility.setSanctioned(buyer.address, false, ethers.encodeBytes32String("OFAC-SDN"), 0);

12 · Принудительное исполнение

Заморозка и принудительные переводы — это поверхность принудительного исполнения ERC-7943, предназначенная для обременений, судебных постановлений и арестов. Заморозка может превышать текущий баланс и тогда распространяется и на будущие зачисления. Принудительный перевод пропускает допуск и проверку замороженного, работает при приостановленном токене, уменьшает замороженное количество до остатка и всё равно запускает хуки с OP_FORCED_TRANSFER.

await token.connect(enforcer).setFrozenTokens(buyer.address, id, 6_000);          // may exceed the balance
await token.getFrozenTokens(buyer.address, id);                                   // 6_000
explain(await token.transferCheck(buyer.address, farmer.address, id, 5_000));    // "FROZEN"
await token.pause();                                                              // PAUSER_ROLE
await token.connect(enforcer).forcedTransfer(buyer.address, custodian.address, id, 6_000); // works while paused
await token.unpause();

Процедуру, по которой действует ENFORCER_ROLE (судебное постановление, договор залога, уведомление о санкциях), следует документировать, а саму роль держать за мультиподписью. Каждое действие генерирует Frozen или ForcedTransfer.

13 · Погашение

RETIRED (255) терминален: никаких переводов, эмиссии и преобразований, и пути назад нет. Сжигание остаётся возможным, чтобы доставленные или уничтоженные товары выбывали из предложения.

await token.connect(warehouse).setBatchStatus(flour, 255, ref("DELIVERED"));    // RETIRED is terminal
explain(await token.transferCheck(mill.address, buyer.address, flour, 1));       // "BATCH_RETIRED"
await token.connect(mill).burn(mill.address, flour, 750);                         // burning still works

14 · Индексация

Граф родословной — это список событий BatchTransformed; ничего не нужно восстанавливать из отдельных переводов.

const transforms = await token.queryFilter(token.filters.BatchTransformed());
const edges = transforms.flatMap((e) =>
  e.args.inputIds.flatMap((input) => e.args.outputIds.map((output) => [input, output, e.args.processRef])));
// edges: [1 → 2], [1 → 3], [2 → 4], [3 → 4], [1 → 5]
СобытиеКем генерируетсяНазначение
BatchCreated(id, issuer, commodity, unit, quantity, to)createBatchновый лот; EPCIS ObjectEvent ADD
BatchTransformed(operator, inputIds, inputAmounts, outputIds, outputAmounts, processRef)transformребро родословной; EPCIS TransformationEvent
BatchStatusChanged(id, previousStatus, newStatus, reason)setBatchStatusжизненный цикл
BatchMetadataUpdated(id, metadataHash, uri)setBatchMetadataистория документов
TransferSingle, TransferBatchкаждое изменение балансавладения; EPCIS ObjectEvent OBSERVE
Frozen(account, tokenId, amount), ForcedTransfer(from, to, tokenId, amount)принудительное исполнениеобременения, аресты
HookRegistered(hook, flags, position), HookRemoved(hook)администратор хуковизменения правил
Attested, AttestationRevoked, TrustedIssuerUpdatedARexAttestationsдоказательства

15 · Обновление

Хранилище использует пространство имён ERC-7201 (erc7201:arex.storage.token); поля следует только добавлять в TokenStorage, никогда не переупорядочивая и не удаляя их. Перед каждым обновлением необходимо выполнять проверку; плагин отклоняет раскладки, которые повредили бы хранилище.

await upgrades.validateUpgrade(await token.getAddress(), ARexToken, { kind: "uups" });
await upgrades.upgradeProxy(await token.getAddress(), ARexToken);               // UPGRADER_ROLE

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

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

npx hardhat test test/standard/ImplementationGuide.test.js   # this chapter, executed
npx hardhat test test/standard/ARexToken.test.js             # token behaviour and conformance
npx hardhat run scripts/smoke-standard.js                    # narrated walkthrough of one lot
npx hardhat run scripts/gas-report-standard.js               # gas with 0, 1 and 2 hooks, and per hook

test/standard/helpers.js экспортирует deployStack(), batchInit(), константы OP, HOOK, STATUS и CODES и supply() для собственных тестов.

Переход в промышленную эксплуатацию: контрольный список

  1. Воспроизводимая сборка (npm ci), CI успешен, идентификаторы интерфейсов совпадают со спецификацией.
  2. Роли перераспределены, как на шаге 3; развёртывающая сторона отказалась от своих ролей.
  3. Хуки проверены аудитом, порядок задокументирован, getHooks() опубликован.
  4. Версия схемы метаданных и таблица статусов опубликованы; originRef и данные с ограниченным доступом свободны от персональных данных.
  5. Правовые инструменты указаны в метаданных; классификация предложения согласована с юристами.
  6. Аудит токена, реестра аттестаций и каждого зарегистрированного хука третьей стороной (пакет: docs/AUDIT-READINESS.md).
  7. Обновление отрепетировано на форке с validateUpgrade.