Source mirror · commit 369512f · 2026-10-26 · MIT · canonical repo: gitlab.com/jiapunk/upgrade-airlock
git clone https://upgrade-airlock.pages.dev/source/upgrade-airlock.git
upgrade-airlock / README.ru.md
README.ru.md319 lines · 60039 bytes

Upgrade Airlock

Каждое обновление прокси проходит через две двери: период ожидания и допуск Chainlink CRE, который может получить только код, выдержавший симуляцию атаки.

English · 简体中文 · 繁體中文 · 日本語 · 한국어 · Español · Français · Deutsch · Português (Brasil) · Русский · Tiếng Việt

Source: GitLab License: MIT Live demo Demo video Ethereum Sepolia Chainlink CRE

Ключевые особенности · Как это работает · Правила · Chainlink CRE · В сети Sepolia · Быстрый старт · Модель безопасности

Обзор

Upgrade Airlock ставит перед каждым обновлением контракта две двери. Внешняя дверь — период ожидания, отсчитываемый в блоках. Внутренняя дверь открывается только по допуску, записанному в блокчейне. Автоматически это значит, что воркфлоу Chainlink CRE атаковал новую реализацию в симуляторе, опросив четыре независимых RPC-провайдера, ответили как минимум два и все ответы совпали, шаг консенсуса воркфлоу (consensusIdenticalAggregation) согласовал вердикт, а подписанный отчёт Clear оказался в блокчейне. Если CRE вообще не может ответить, ключ утверждающего (approver) может выдать допуск вручную с записанными доказательствами; обходчик всё равно оценивает такое предложение, пока оно входит в 20 последних id, а вето CRE или стража имеет приоритет. Вредоносная реализация, до которой доходит поиск атак, вместо этого получает подписанное Veto и не выполняется, даже если у атакующего есть ключ обновления; то, до чего ограниченный поиск не дотягивается, закрывает строгий режим с ручным ревью кода. Сегодня вердикты выносит официальный CRE CLI (cre workflow simulate --broadcast), который запускает воркфлоу на сервере; контракты и воркфлоу написаны в расчёте на DON, а для перехода туда потребуется новое развёртывание с продакшн-форвардером Chainlink. Проект создан для ETH Shenzhen 2026 на базе Ethereum Sepolia.

[!TIP] Онлайн-демо: upgrade-airlock.pages.dev · Демо-видео: upgrade-airlock.pages.dev/video (на упрощённом китайском и английском)

Ситуационный центр (war room) читает Ethereum Sepolia прямо из браузера, в режиме только для чтения. Каждое предложение изображено в виде шлюза: внешняя дверь фиксирует предложение, шлюзовая камера отсчитывает период ожидания, а внутренняя дверь остаётся запертой, пока в блокчейне не окажется допуск (Clear от Chainlink CRE или ручной допуск утверждающего, который ситуационный центр так и помечает). Veto выбрасывает предложение из шлюза — с указанием правила, хеша доказательств и транзакции. При первом посещении открывается выбор атаки: каждая карточка (шесть атак и честное обновление v2) воспроизводит самое свежее реальное предложение своего вида в Sepolia и помечена как повтор. Страница открывается на упрощённом китайском: ?lang=en переключает её на английский, а ?lang=zh-TW — на традиционный китайский. Транзакции, которые она показывает (предложения #1–#12), перечислены в доказательствах реального прогона.

Ключевые особенности

  • Fail-closed по построению. Обновление выполняется только при наличии допуска в блокчейне. Пропущенный лог, сбой RPC, расхождение провайдеров или неудачный прогон оставляют предложение без допуска или под вето: обновление может задержаться, но никогда не пройдёт без проверки. Вето всегда имеет приоритет над допуском.
  • Атака, а не чек-лист. Воркфлоу (при развёртывании в DON — на каждом узле) прогоняет обновление на реальном состоянии прокси через eth_simulateV1 у четырёх RPC-провайдеров. Посторонний адрес с пополненным балансом вызывает каждый селектор из нового байткода, аргументы с подстановкой держателя подаются в каждую новую функцию, а эмиссия, владение и балансы сравниваются до и после. Ответить должны как минимум два провайдера, и все ответы должны совпасть.
  • Вердикты привязаны к предложению. Шаг идентичного консенсуса воркфлоу согласует {verdict, rule}. Отчёт содержит аттестацию EIP-712, привязанную к точному содержимому предложения (proposalDigest), к сети, к получателю и к предельному блоку, а также хеш доказательств, который любой может пересчитать по данным блокчейна.
  • Пропущенные логи подбираются. Cron-обходчик (sweeper) каждые 60 секунд проверяет 20 последних предложений и выносит вердикт по всем, которые пропустил log trigger, включая обновления, которым утверждающий (approver) выдал допуск вручную; его вето имеет приоритет. Предложение, вытесненное за пределы 20 последних id, больше не оценивается: оно остаётся без допуска, если только утверждающий не выдал его вручную (окно обхода).
  • Работает в Ethereum Sepolia. Официальный CRE CLI выполнял воркфлоу на сервере и ответил в блокчейне на каждое предложение (двенадцать на 2026-10-25): одно честное обновление получило допуск и было выполнено, одиннадцать атак получили вето (R1, R2, R3, R3B, R7, R9). Все контракты верифицированы на Sourcify.
  • Проверено сквозными тестами. В CI запускаются 117 тестов Foundry, 118 юнит-тестов воркфлоу и 387 из 394 самопроверок ситуационного центра (остальным 7 нужны результаты сборки forge и cast, которых в CI-задаче нет); e2e-скрипт на форке прогоняет весь цикл через симулятор CRE.

Проблема

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

  • PAID Network, март 2021. С помощью ключей владельца прокси проверенный аудиторами код токена был заменён версией с функциями burn и mint, и атакующий выпустил около 59,5 млн PAID. (разбор инцидента CertiK)
  • Ankr, декабрь 2022. С помощью скомпрометированного ключа деплоера атакующий заменил реализацию aBNBc на версию, которая выпускала токены без проверки авторизации: 60 трлн aBNBc. (отчёт Ankr)
  • Radiant Capital, октябрь 2024. Атакующий, контролировавший 3 из 11 подписантов мультисига, захватил провайдер пулов кредитования и обновил реализацию пулов, выведя более $53 млн. (rekt.news)

Таймлок лишь заставляет атакующего подождать, и никто не обязан что-либо проверять, пока идёт ожидание. К тому же долгие задержки блокируют экстренные исправления. Upgrade Airlock заставляет время ожидания работать: обновление не может выполниться, пока его не пропустит независимая проверка (децентрализованная после развёртывания в CRE DON), и эта проверка автоматически запускается для каждого предложения.

Как это работает

sequenceDiagram
    actor Owner as Ключ владельца (считаем, что он утёк)
    participant Admin as AirlockAdmin
    participant CRE as Воркфлоу Chainlink CRE
    participant RPC as 4 RPC-провайдера
    participant Fwd as KeystoneForwarder
    participant Recv as AirlockReceiver
    actor Anyone as Кто угодно

    Owner->>Admin: proposeUpgrade(newImpl, data)
    Note over Admin: Внешняя дверь: период ожидания 25 блоков, около 5 минут в Sepolia
    Admin-->>CRE: Лог Proposed (EVM log trigger) или cron-обходчик каждые 60 с
    CRE->>Admin: Чтения EVM на блоке предложения
    loop На каждом узле (в симуляторе один, при развёртывании каждый узел DON), для каждого провайдера
        CRE->>RPC: eth_getCode, затем симуляция атаки через eth_simulateV1
        RPC-->>CRE: вердикт и правило
    end
    Note over CRE: Кворум: не менее 2 ответов, все совпадают, иначе Veto R6<br/>идентичный консенсус по вердикту и правилу<br/>хеш доказательств и аттестация EIP-712
    CRE->>Fwd: writeReport
    Fwd->>Recv: onReport(metadata, report)
    alt Все проверки пройдены
        Recv->>Admin: clear(id, evidenceHash)
    else Нарушено правило
        Recv->>Admin: veto(id, ruleHash) и attachReason(id, evidenceHash)
    end
    Anyone->>Admin: execute(id)
    Note over Admin: Внутренняя дверь: ожидание истекло, допуск есть, вето нет, codehash не изменился
    Admin->>Admin: ProxyAdmin.upgradeAndCall(proxy, newImpl, data)
  1. Предложение. AirlockAdmin является владельцем ProxyAdmin из OpenZeppelin и Ownable токена. Ключ владельца может только предлагать: он никогда не бывает стражем (guardian), утверждающим (approver) или оракулом допуска. Каждое предложение (обновление или изменение самого шлюза: стражи, задержка, цель break-glass) получает readyBlock = block.number + delayBlocks. Целью обновления или break-glass может быть только обычный код контракта: делегированный аккаунт EIP-7702, ключ которого мог бы перенаправить его после вердикта, отклоняется (DelegatedAccount).
  2. Оценка. Log trigger CRE срабатывает на событие Proposed. Изменение самого шлюза оценивается по данным, прочитанным из блокчейна (R7). Обновление сначала проходит статические правила (R5, R8), затем симуляцию атаки, привязанную к блоку предложения: слот реализации ERC-1967 прокси подменяется новым кодом, а новый посторонний адрес (выводится из хеша блока предложения) получает средства, как настоящий пользователь: токены от второго держателя и ETH. Держатель делает перевод, посторонний вызывает transferFrom без allowance, а затем — каждый селектор, найденный в новом байткоде, с пятью вариантами аргументов. Каждый селектор вне стандартного интерфейса токена дополнительно вызывается с подстановкой держателя: держатель с балансом — в каждом из первых четырёх слотов аргументов, тестовая сумма — в другом слоте, посторонний адрес или прокси — в остальных. Эмиссия, владелец и каждый отслеживаемый баланс считываются до и после, баланс держателя — сразу после каждого вызова с подстановкой держателя; кроме того, проверяются собственные события каждого пробного вызова (с R1 по R4, R3B). Наконец, новый байткод проверяется на опкоды, которые в реальном блоке ведут себя иначе, чем в симуляции (R9).
  3. Согласование. Каждый узел (в развёртывании DON — каждый узел DON; сегодня — симулятор CLI) запускает симуляцию у четырёх провайдеров. Ответить должны минимум два, и все ответы должны содержать один и тот же вердикт; в любом другом случае — вето (R6). Затем шаг консенсуса согласует идентичный {verdict, rule}.
  4. Отчёт. Воркфлоу хеширует канонический JSON доказательств, подписывает вердикт EIP-712 своим ключом аттестатора (секрет CRE) и записывает отчёт через форвардер. AirlockReceiver проверяет отправителя, актуальность, подпись и то, что вердикт привязан именно к этому предложению (proposalDigest), после чего вызывает в AirlockAdmin clear либо veto и attachReason.
  5. Выполнение. Кто угодно может вызвать execute(id) после окончания периода ожидания. Вызов откатывается с ошибкой TooEarly во время ожидания, с NotCleared без допуска, с ProposalVetoed после вето и с CodehashChanged, если по целевому адресу больше нет кода, который был предложен и проверен. Вето всегда имеет приоритет над допуском.
  6. Обход. Cron trigger каждые 60 секунд перепроверяет последние предложения и выносит вердикт по каждому ожидающему предложению, на которое CRE ещё не ответил (oracleJudged): лог, пропущенный триггером, событие Proposed, слишком большое для доставки, или обновление, которому утверждающий выдал допуск вручную. Его вето имеет приоритет над таким допуском.

Все сбои ведут в одну сторону. Пропущенный лог, сбой RPC, расхождение провайдеров или неудачный прогон оставляют предложение без допуска или накладывают на него вето, то есть задерживают обновление и никогда его не пропускают. Если CRE не может ответить вовсе, ключ утверждающего может вручную выдать обновлению допуск с зафиксированными доказательствами; обходчик всё равно оценит это обновление, как только CRE снова начнёт отвечать, а вето CRE или стража по-прежнему имеет приоритет. breakGlass() позволяет владельцу или любому стражу мгновенно переключить прокси на заранее зарегистрированную реализацию в режиме паузы. Выход из режима паузы — обновление через обе двери; допуск предложения не истекает, поэтому завершить паузу может и ранее допущенное обновление (известные ограничения).

Подробнее: docs/design.md (модель угроз, конечный автомат допуска, привязка отчёта, бюджеты) и cre/README.md (воркфлоу).

Правила

ruleHash = keccak256("GUARD/<ID>"), записывается в блокчейн при каждом вето.

Правило Вето, если Чем определяется
R1 TOTAL_SUPPLY вызов постороннего изменяет totalSupply симуляция атаки
R2 OWNER вызов постороннего изменяет owner() симуляция атаки
R3 TRANSFER_CONSERVATION перевод держателем суммы X перемещает не ровно X или затрагивает чей-то ещё баланс симуляция атаки
R3B THIRD_PARTY_FUNDS посторонний перемещает токены любого держателя: transferFrom пополненного постороннего адреса без allowance проходит успешно, либо один из его пробных вызовов перемещает чужие токены, выпускает или сжигает токены, передаёт владение или оставляет баланс держателя ниже прежнего — даже без события и даже если последующий вызов это отменяет симуляция атаки
R4 TRANSFER_LIVENESS обычный перевод держателя не проходит симуляция атаки
R5 STRICT_CODEHASH включён строгий режим, а утверждающий не одобрил codehash чтение из блокчейна
R6 SIM_FAILURE симуляция не может вынести решение: провайдеры расходятся, отвечает слишком мало провайдеров, код не совпадает, откатывается view-вызов или пополнение постороннего адреса, либо пробные вызовы не умещаются в заданное конфигурацией число запросов симуляция и кворум
R7 CONFIG_GUARD смена стража, сокращение задержки или новая цель break-glass без предварительного одобрения утверждающего чтение из блокчейна
R8 INIT_DATA обновление содержит init data, которые утверждающий не одобрил вместе с codehash чтение из блокчейна
R9 NEW_OPCODES новый код содержит опкод контекста блока или транзакции (BASEFEE, TIMESTAMP, NUMBER, ORIGIN, GAS и другие), внешний вызов, create или selfdestruct, не покрытые фиксированным allowlist конфигурации (по умолчанию пустым), если только утверждающий не одобрил именно этот codehash байткод

R9 закрывает обход симуляции. EvilBaseFeeImpl ведёт себя как v1, пока block.basefee == 0 — а именно это видит вызов eth_simulateV1, — и позволяет кому угодно выпускать токены в реальном блоке. Все инварианты в симуляции выполняются; вето ему приносит опкод BASEFEE. Allowlist никогда не зависит от кода, который прокси исполняет сейчас, поэтому одобрение распространяется на один codehash и ни на одно последующее обновление. Сканер пропускает хвост метаданных компилятора, только если это точная CBOR-карта solc, недостижимая ни для одной инструкции; всё остальное сканируется как код. Одобрение codehash освобождает его от R9 и допускает усечение пробных вызовов (R6) — и только: все остальные правила продолжают действовать.

R3B закрывает тихий вывод средств. EvilDrainImpl — это v1 плюс rescue(from, to, amount) без контроля доступа. Эмиссия, владелец, переводы и опкоды у неё такие же, как у v1, поэтому правила с R1 по R4 и R9 проходят. rescue — не стандартная функция токена, поэтому посторонний вызывает её ещё и с подстановкой держателя; вызов с держателем в качестве from перемещает токены держателя, и R3B накладывает вето. Вывод средств, спрятанный за условием «только для вызывающих, у которых есть токены или ETH», тоже обнаруживается, потому что у постороннего есть и то и другое.

Автоматический и строгий режимы

Автоматический режим (по умолчанию): CRE самостоятельно выдаёт допуск или вето каждому обновлению по итогам ограниченного поиска атак — внешний вызывающий с пополненным балансом, каждый селектор нового кода и держатель в каждом из первых четырёх слотов аргументов каждой новой функции. Строгий режим (R5) добавляет проверку человеком: каждый новый codehash реализации сначала должен одобрить ключ утверждающего, а симуляция атаки всё равно выполняется. Автоматический режим подходит токену, которому нужен допуск рутинных обновлений за считанные минуты; строгий режим делает код-ревью частью допуска (два режима).

CRE — оркестрирующий слой Upgrade Airlock: воркфлоу — единственный автоматический путь через внутреннюю дверь, и он ведёт каждый шаг от ончейн-триггера до ончейн-вердикта. Воркфлоу написан на CRE TypeScript SDK (@chainlink/cre-sdk 1.23.0) и скомпилирован в WASM с помощью CRE CLI v1.37.0.

Технология Chainlink Где используется Роль в проекте
Раннер воркфлоу CRE cre/veto/main.ts#L1-L9, cre/veto/workflow.yaml, cre/project.yaml Два обработчика, цели sepolia и sepolia-fork
EVM log trigger cre/veto/workflow.ts#L202-L211, #L193-L198, #L54-L65 AirlockAdmin.Proposed на LATEST: предложение оценивается сразу после появления, за целый период ожидания до того, как его можно будет выполнить
Cron trigger cre/veto/workflow.ts#L200, #L209, cre/veto/airlock.ts#L323-L389 Обходчик: 20 последних id, каждую минуту
Чтение EVM (callContract) cre/veto/workflow.ts#L114-L122, #L137-L142, cre/veto/chain.ts Предложение, допуск, oracleJudged, одобрения и балансы — пакетами через Multicall3 на блоке предложения или на LATEST
Чтение EVM (headerByNumber) cre/veto/workflow.ts#L125-L136 Последний блок и хеш блока предложения, из которого выводится посторонний адрес для симуляции
HTTP capability в режиме узла (node mode) cre/veto/workflow.ts#L167-L189, #L80-L94, cre/veto/judge.ts#L157-L220 Каждый узел отправляет каждому провайдеру eth_getCode, затем один или два запроса eth_simulateV1 (каждый — в пределах лимита запроса 120 kb)
consensusIdenticalAggregation cre/veto/workflow.ts#L184-L188, кворум в cre/veto/judge.ts#L244-L277 Идентичный консенсус по {verdict, rule} (не более 52 байт; при развёртывании — каждый узел DON)
Секреты (Secrets) cre/veto/workflow.ts#L160-L166, cre/secrets.yaml Ключ аттестатора EIP-712, загружается один раз за прогон
Отчёт + writeReport cre/veto/workflow.ts#L143-L149, cre/veto/airlock.ts#L229-L282, cre/veto/report.ts runtime.report (ECDSA, keccak256) и evm.writeReport, затем чтение предложения для подтверждения результата
Форвардер + IReceiver src/cre/AirlockReceiver.sol#L88-L120, #L156-L169, src/cre/IReceiver.sol, script/DeployEth.s.sol#L142 onReport принимает отчёты только от настроенного форвардера и, если они заданы, проверяет workflow id и владельца воркфлоу
Селекторы сетей (chain selectors) cre/veto/workflow.ts#L100-L104 ethereum-testnet-sepolia
CRE CLI cre/setup.sh, cre/scripts/fork-e2e.sh, scripts/sepolia/airlock.sh, scripts/sepolia/sweep.sh cre workflow build, cre workflow simulate --broadcast

Как воркфлоу работает сегодня. Воркфлоу запускается командой CRE CLI cre workflow simulate --broadcast в сети Ethereum Sepolia. Слушатель (scripts/sepolia/airlock.sh) держит log trigger в режиме прослушивания, обходчик (scripts/sepolia/sweep.sh) каждую минуту запускает cron-обработчик, а каждый Clear и Veto — реальная транзакция в Sepolia через MockKeystoneForwarder, развёрнутый Chainlink в Sepolia, 0x15fC6ae953E024d975e77382eEeC56A9101f9F88. AirlockReceiver в текущем виде готов к развёртыванию воркфлоу в DON: достаточно развернуть его с FORWARDER, указывающим на продакшн-KeystoneForwarder, и с заданными WORKFLOW_ID / WORKFLOW_OWNER — тогда onReport дополнительно проверяет подписанные DON метаданные воркфлоу. DeployEth отказывается использовать permissionless MockKeystoneForwarder, если только не заданы оба значения или явно не указан ALLOW_MOCK_FORWARDER=1 (script/DeployEth.s.sol#L96-L104).

Спроектирован под лимиты CRE на один прогон. Не более 15 чтений из блокчейна (пакеты Multicall3 укладываются в лимит payload 5 kb), 12 из 15 HTTP-вызовов при четырёх провайдерах, одна симуляция атаки за прогон и значение консенсуса не более 52 байт. Бюджеты описаны в docs/design.md.

В сети Sepolia

Ethereum Sepolia, chain id 11155111. Развёртывание выполнено скриптом script/DeployEth.s.sol в блоках 11950684–11950699 (13 транзакций; AirlockAdmin в блоке 11950697); все контракты верифицированы на Sourcify. Владелец, утверждающий, аварийный страж и аттестатор — четыре разных ключа. Период ожидания — 25 блоков.

Контракт Адрес
AirlockAdmin 0x28e7aAC40560804F480e454bF9C12F50C83b700B
AirlockReceiver 0xE40D463116F4Aa24d5E79776a65ED0f1b088c670
Прокси DemoBTC (dBTC) 0x88Cc569265faDAEAc2a16B69f35E10B2158e4b71
ProxyAdmin 0x51a3fE6AD22BF7753574B7b7FD56aE9a65bd593E
DemoBTC v1 0x8f7d6453d77B3f3b008CBE4A144d82c5742eB108
DemoBTC v2 (честное обновление) 0x4D959f981C01E3B81ac6C9A20E84816Fe5f6Bce3
EvilMintImpl 0x01502c4af90c27f53B2E1293941670C6B761CAAA
EvilOwnerImpl 0xBe849DE8e0a4D9b0010ba1468f94Eeb24A42d488
EvilFeeImpl 0xB95Ffa7b970B1a73488e2fC32B787fA536F12AF5
EvilBaseFeeImpl 0xE1AAa4E88DB9cc7ceCca14722eE2874D834e0310
EvilDrainImpl 0x1d98D64BC5204AE139B5A0747855841Fa5333b6d
PausedImpl (цель break-glass) 0xb6573006fB607E96966d0F578B96AAB25413F5BE
Chainlink MockKeystoneForwarder 0x15fC6ae953E024d975e77382eEeC56A9101f9F88

Первый прогон: семь предложений — семь вердиктов в блокчейне (последующие #8–#12 перечислены в docs/evidence/sepolia-live.md). Блок в каждой ссылке — это блок, в который попала транзакция; (+n) показывает, через сколько блоков после предложения пришёл вердикт CRE.

Карточка Предложение Вердикт CRE Tx предложения Tx отчёта Затем
v2: честное обновление #4 Clear 11950745 11950747 (+2) execute 11950772, version() = "2"
evil-mint: выпускать токены может кто угодно #1 Veto R1 11950735 11950736 (+1) execute откатывается с ProposalVetoed
evil-owner: кто угодно может забрать владение #6 Veto R2 11950788 11950790 (+2) execute откатывается с ProposalVetoed
evil-fee: с каждого перевода удерживается 10% #7 Veto R3 11950792 11950794 (+2) execute откатывается с ProposalVetoed
evil-basefee: выпуск токенов открывается в реальном блоке #3 Veto R9 11950742 11950743 (+1) execute откатывается с ProposalVetoed
evil-drain: кто угодно перемещает токены держателя #2 Veto R3B 11950737 11950740 (+3) execute откатывается с ProposalVetoed
remove-receiver: убрать CRE из стражей #5 Veto R7 11950749 11950750 (+1) execute откатывается с ProposalVetoed

Все вердикты выше вынес официальный CRE CLI, выполнявший этот воркфлоу на сервере (слушатель log trigger плюс 60-секундный обходчик, scripts/sepolia/systemd/); собственные строки лога воркфлоу приведены в docs/evidence/sepolia-live.md.

Быстрый старт

Требования

  • Foundry (forge, cast, anvil), jq и Node 22.
  • Аккаунт CRE для симулятора (cre login); он нужен для сквозного прогона на форке и для операторского набора.
  • cre/setup.sh устанавливает CRE CLI v1.37.0 (с зафиксированным SHA-256) и bun 1.4.2 в cre/.tools.

Сборка и тесты

git clone https://gitlab.com/jiapunk/upgrade-airlock.git   # или https://upgrade-airlock.pages.dev/source/upgrade-airlock.git
cd upgrade-airlock

# контракты (OpenZeppelin и forge-std уже включены в lib/)
forge build                     # однократно загружает зафиксированную версию solc 0.8.37
forge test --offline            # 117 тестов

# воркфлоу CRE
cre/setup.sh
export PATH="$PWD/cre/.tools/bin:$PWD/cre/.tools/node_modules/.bin:$PATH"
(cd cre/veto && bun test && bun run typecheck)    # 118 тестов: правила, пробные вызовы, байткод, отчёты, бюджеты, обработчики

# ситуационный центр
(cd web && npm ci && npm run selftest)            # 394 проверки: режим реального времени и выбор атаки, офлайн-воспроизведение, API оператора

Сквозной прогон на форке Sepolia

cre login                       # один раз; симулятору нужен аккаунт CRE
cre/scripts/fork-e2e.sh         # локальный anvil-форк Sepolia: в реальную сеть ничего не отправляется

cre/scripts/fork-e2e.sh разворачивает стек на форке, запускает воркфлоу в симуляторе CRE с --broadcast и проверяет каждый результат в блокчейне:

  • Log trigger: EvilMint получает вето R1, EvilOwner — R2, EvilFee — R3, EvilBaseFee — R9 (с указанием BASEFEE), EvilDrain — R3B (с указанием rescue, при том что все остальные правила проходят); ещё семь вариантов вывода средств (test/fixtures/DrainVariants.sol) получают вето только по R3B: переставленные аргументы, проверка адреса токена, вывод, доступный лишь вызывающим с токенами или ETH, вариант без события Transfer и transferFrom, который для держателей токенов пропускает проверку allowance; честный v2 получает допуск и выполняется после периода ожидания; удаление получателя из стражей получает вето R7; сокращение задержки, заранее одобренное утверждающим, получает допуск.
  • Обходчик: обновление, по которому никто не вынес вердикт, после периода ожидания откатывается с NotCleared, один прогон обходчика выдаёт ему допуск, и оно выполняется; два предложения, которым утверждающий выдал допуск вручную, оцениваются следующим прогоном: EvilDrain получает вето R3B поверх допуска, а увеличение задержки CRE тоже допускает; обновление с 6 KB init data получает вето R8 на последующем прогоне.
  • Доказательства: каждое предложение под вето откатывается с ProposalVetoed, а хеш доказательств каждого вердикта пересчитывается по данным блокчейна (cre/veto/tools/check-evidence.ts).

Операторский набор для Sepolia

scripts/sepolia/ — скрипты, на которых работает онлайн-демо. Ключи хранятся вне репозитория, по одному hex-ключу на файл в KEY_DIR (owner.key, attester.key, sender.key, approver.key, emergency.key, а также необязательный sweeper.key); скрипты никогда их не выводят, а deploy.sh отказывается работать, если владелец, утверждающий, аварийный страж и аттестатор — не четыре разных ключа. Каждый скрипт можно запустить и на локальном форке с FORK=<port>, так что демо можно отрепетировать без каких-либо затрат.

scripts/sepolia/deploy.sh                    # 13 транзакций, записывает deployments/sepolia.json (FORCE=1 заменяет существующий)
bun scripts/sepolia/make-config.mjs          # конфигурация воркфлоу, 4 провайдера, кворум 2
scripts/sepolia/fund-sender.sh 0.05          # ETH для транзакций с отчётами
scripts/sepolia/airlock.sh                   # терминал A: слушатель CRE (log trigger)
scripts/sepolia/propose.sh v2                # терминал B: затем evil-mint, evil-owner, evil-fee, evil-basefee, evil-drain, remove-receiver
scripts/sepolia/execute.sh <id>              # выполняет допущенное предложение после периода ожидания, иначе показывает откат
scripts/sepolia/status.sh                    # все предложения: вердикт, оценено ли CRE, правило, доказательства, транзакции

sweep.sh однократно запускает cron-обходчик, clear.sh — ручной допуск утверждающего, fork.sh запускает, сохраняет и восстанавливает форк Sepolia, probe-rpcs.ts проверяет каждого провайдера собственным кодом оценки воркфлоу, а systemd/ запускает слушатель и обходчик на Linux-сервере.

Ситуационный центр

cd web
npm ci
npm run dev                     # http://127.0.0.1:5173/
npm run build:airlock           # синхронизирует deployments/sepolia.json в страницу, собирает web/dist-airlock

Страница читает Sepolia напрямую, не хранит ключей и никогда ничего не подписывает и не отправляет. Она показывает каждое предложение в виде шлюза: период ожидания, допуск CRE, вето с правилом и доказательствами. Посетитель выбирает одну из семи карточек (шесть атак и честное обновление v2) и смотрит помеченный повтор самого свежего реального предложения этого вида в Sepolia (или живое предложение, пока оно в периоде ожидания) — см. выбор атаки. Для докладчика есть режим оператора (web/operator/, web/functions/): на dev-сервере он принимает только loopback-подключения, на Cloudflare Pages требует токен и выключен, пока не заданы оба секрета OWNER_KEY и OPERATOR_TOKEN. Он отправляет одно из тех же семи фиксированных предложений через POST /api/operator/propose, подписывая его ключом владельца на стороне сервера (дизайн и так исходит из того, что этот ключ утечёт: он может только предлагать); страница обычного посетителя его никогда не вызывает — см. режим оператора.

Параметр URL Действие
?mode=airlock По умолчанию. Просмотр Ethereum Sepolia в реальном времени, только чтение
?mode=offline Воспроизводит записанный прогон вообще без сетевого подключения
?lang=en, ?lang=zh-TW, ?lang=zh-CN Язык интерфейса (по умолчанию zh-CN; клавиша L переключает их по кругу)
?rpc=http://127.0.0.1:<port> Читает вместо этого локальный форк Sepolia — для репетиций
?op=1 Режим оператора: запрашивает токен оператора (только для докладчика)

В режиме реального времени нажмите P, чтобы воспроизвести последние предложения по их событиям в блокчейне, и 1–7, чтобы воспроизвести карточку из выбора атаки.

Структура репозитория

src/AirlockAdmin.sol            две двери: предложения, период ожидания, вето стража, допуск, execute, breakGlass
src/cre/AirlockReceiver.sol     получатель отчётов CRE: форвардер, метаданные воркфлоу, аттестация EIP-712, привязка digest
src/cre/IReceiver.sol           интерфейс получателя Chainlink CRE
src/DemoBTC.sol, DemoBTCV2.sol  демо-токен (dBTC) v1 и честный v2
src/PausedImpl.sol              цель break-glass: любое изменение баланса откатывается
src/attack/                     EvilMintImpl, EvilOwnerImpl, EvilFeeImpl, EvilBaseFeeImpl, EvilDrainImpl
script/DeployEth.s.sol          развёртывание (13 транзакций) с проверками ролей, форвардера и связей после деплоя
test/                           тесты Foundry; fixtures/DrainVariants.sol содержит семь вариантов вывода средств
cre/veto/                       воркфлоу CRE: workflow.ts (capabilities), airlock.ts (обработчики), judge.ts (правила,
                                кворум), simulate.ts (eth_simulateV1), bytecode.ts (селекторы, R9), report.ts
                                (отчёт, EIP-712, доказательства), chain.ts (бюджет чтений), keys.ts, rules.ts, config.ts
cre/veto/test/                  юнит-тесты Bun
cre/veto/tools/                 check-evidence.ts пересчитывает каждый ончейн-хеш доказательств
cre/scripts/fork-e2e.sh         сквозной прогон на anvil-форке Sepolia
cre/setup.sh                    зафиксированные версии CRE CLI и bun
scripts/sepolia/                операторский набор для онлайн-демо, с unit-файлами systemd для сервера
web/                            ситуационный центр (Vite, TypeScript, viem)
web/operator/, web/functions/   режим оператора: семь фиксированных предложений, подписанных на сервере (dev-сервер, Pages Functions)
deployments/sepolia.json        действующее развёртывание
docs/design.md                  модель угроз, конечный автомат допуска, привязка отчёта, бюджеты
docs/evidence/                  запись реального прогона
.github/workflows/ci.yml        проверки контрактов, воркфлоу и ситуационного центра

Модель безопасности

Upgrade Airlock исходит из того, что ключ владельца утечёт, и построен так, чтобы одного ключа владельца никогда не хватало для выполнения обновления.

Роль Кто Что может
Владелец (owner) один ключ предлагать обновления и изменения самого шлюза (стражи, задержка, цель break-glass); breakGlass
Утверждающий (approver) отдельный ключ вручную выдавать допуск, когда CRE не может ответить; одобрять codehash, init data и изменения шлюза (approveCodehash)
Аварийный страж (emergency guardian) отдельный ключ налагать veto на ожидающее предложение; breakGlass
Аттестатор (attester) секрет CRE, отдельный ключ подписывать вердикты EIP-712, которые принимает AirlockReceiver
AirlockReceiver получатель отчётов CRE оракул допуска (clear) и страж (veto, attachReason)
Кто угодно вызывать execute для предложения, прошедшего обе двери

Роли никогда не пересекаются. Конструктор AirlockAdmin отклоняет владельца, который одновременно является утверждающим, стражем или оракулом допуска (RoleOverlap), а DeployEth выполняет развёртывание только тогда, когда владелец, утверждающий, аварийный страж и аттестатор — четыре разных адреса.

Утёкший ключ владельца может предлагать. Каждое предложение у всех на виду проходит период ожидания, пока CRE его оценивает, и требует допуска, который владелец не может выдать себе сам. Владелец также может задействовать аварийный тормоз (breakGlass), который лишь переключает прокси на заранее зарегистрированную реализацию в режиме паузы; выход из этого режима — обновление через обе двери, и завершить его может также ранее допущенное, но так и не выполненное обновление.

Тот же ключ не может:

  • выполнить обновление без допуска: у AirlockAdmin нет универсального пути вызова, и единственный способ сменить реализацию — execute допущенного обновления (или breakGlass на реализацию в режиме паузы);
  • сначала ослабить шлюз: удаление получателя CRE, сокращение задержки или замена цели break-glass — это тоже предложения, и без предварительного одобрения утверждающего R7 накладывает на них вето;
  • подменить код после вердикта: execute перепроверяет codehash цели (CodehashChanged), а делегированные аккаунты EIP-7702 отклоняются (DelegatedAccount);
  • подделать или повторно использовать вердикт: AirlockReceiver принимает отчёт только от настроенного форвардера, только подписанный аттестатором, не позднее его validUntilBlock и привязанный к proposalDigest предложения в этой сети и для этого получателя.

Ручной допуск утверждающего — это ручное переопределение на случай, когда CRE не может ответить: обходчик всё равно оценивает такое предложение, пока оно входит в 20 последних id, а вето CRE или стража имеет приоритет. Полная модель угроз — в docs/design.md.

Это демонстрационные контракты для тестовой сети с известными ограничениями, каждое из которых закреплено проходящим тестом: допуски не истекают, ключ владельца нельзя сменить (украденный ключ может снова ставить токен на паузу после каждого снятия паузы), а один недобросовестный страж может держать токен на паузе. Список — в docs/design.md.

Технологии

  • Solidity 0.8.37 и Foundry, с OpenZeppelin Contracts 5.7.0 (ProxyAdmin, TransparentUpgradeableProxy, EIP712, ECDSA)
  • Chainlink CRE: TypeScript SDK (@chainlink/cre-sdk 1.23.0) и CRE CLI v1.37.0 на Bun 1.4.2
  • viem для ABI-кодирования и чтения блокчейна — в воркфлоу и в ситуационном центре
  • Vite и TypeScript для ситуационного центра

Инструменты ИИ

Upgrade Airlock создан под руководством автора с помощью Claude Code: с ним написаны и проверены контракты, воркфлоу CRE, тесты, скрипты, ситуационный центр и эта документация.

Лицензия

MIT © 2026 jiapunk

Source
1<div align="center">
2
3# Upgrade Airlock
4
5**Каждое обновление прокси проходит через две двери: период ожидания и допуск Chainlink CRE, который может получить только код, выдержавший симуляцию атаки.**
6
7[English](README.md) · [简体中文](README.zh-CN.md) · [繁體中文](README.zh-TW.md) · [日本語](README.ja.md) · [한국어](README.ko.md) · [Español](README.es.md) · [Français](README.fr.md) · [Deutsch](README.de.md) · [Português (Brasil)](README.pt-BR.md) · **Русский** · [Tiếng Việt](README.vi.md)
8
9[![Source: GitLab](https://img.shields.io/badge/source-GitLab-FC6D26?logo=gitlab&logoColor=white)](https://gitlab.com/jiapunk/upgrade-airlock)
10[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
11[![Live demo](https://img.shields.io/badge/live%20demo-upgrade--airlock.pages.dev-2ea44f)](https://upgrade-airlock.pages.dev)
12[![Demo video](https://img.shields.io/badge/demo%20video-watch-red)](https://upgrade-airlock.pages.dev/video/)
13[![Ethereum Sepolia](https://img.shields.io/badge/Ethereum-Sepolia-627EEA?logo=ethereum&logoColor=white)](https://sepolia.etherscan.io/address/0x28e7aAC40560804F480e454bF9C12F50C83b700B)
14[![Chainlink CRE](https://img.shields.io/badge/Chainlink-CRE-375BD2?logo=chainlink&logoColor=white)](#chainlink-cre)
15
16[Ключевые особенности](#highlights) · [Как это работает](#how-it-works) · [Правила](#rules) · [Chainlink CRE](#chainlink-cre) · [В сети Sepolia](#live-on-sepolia) · [Быстрый старт](#quick-start) · [Модель безопасности](#security-model)
17
18</div>
19
20## Обзор
21
22Upgrade Airlock ставит перед каждым обновлением контракта две двери. **Внешняя дверь** — период ожидания, отсчитываемый в блоках. **Внутренняя дверь** открывается только по допуску, записанному в блокчейне. Автоматически это значит, что воркфлоу Chainlink CRE атаковал новую реализацию в симуляторе, опросив четыре независимых RPC-провайдера, ответили как минимум два и все ответы совпали, шаг консенсуса воркфлоу (`consensusIdenticalAggregation`) согласовал вердикт, а подписанный отчёт **Clear** оказался в блокчейне. Если CRE вообще не может ответить, ключ утверждающего (approver) может выдать допуск вручную с записанными доказательствами; обходчик всё равно оценивает такое предложение, пока оно входит в 20 последних id, а вето CRE или стража имеет приоритет. Вредоносная реализация, до которой доходит поиск атак, вместо этого получает подписанное **Veto** и не выполняется, даже если у атакующего есть ключ обновления; то, до чего ограниченный поиск не дотягивается, закрывает строгий режим с ручным ревью кода. Сегодня вердикты выносит официальный CRE CLI (`cre workflow simulate --broadcast`), который запускает воркфлоу на сервере; контракты и воркфлоу написаны в расчёте на DON, а для перехода туда потребуется новое развёртывание с продакшн-форвардером Chainlink. Проект создан для ETH Shenzhen 2026 на базе Ethereum Sepolia.
23
24> [!TIP]
25> **Онлайн-демо: [upgrade-airlock.pages.dev](https://upgrade-airlock.pages.dev)** · **Демо-видео: [upgrade-airlock.pages.dev/video](https://upgrade-airlock.pages.dev/video/)** (на упрощённом китайском и английском)
26>
27> Ситуационный центр (war room) читает Ethereum Sepolia прямо из браузера, в режиме только для чтения. Каждое предложение изображено в виде шлюза: **внешняя дверь** фиксирует предложение, **шлюзовая камера** отсчитывает период ожидания, а **внутренняя дверь** остаётся запертой, пока в блокчейне не окажется допуск (Clear от Chainlink CRE или ручной допуск утверждающего, который ситуационный центр так и помечает). Veto выбрасывает предложение из шлюза — с указанием правила, хеша доказательств и транзакции. При первом посещении открывается выбор атаки: каждая карточка (шесть атак и честное обновление v2) воспроизводит самое свежее реальное предложение своего вида в Sepolia и помечена как повтор. Страница открывается на упрощённом китайском: [`?lang=en`](https://upgrade-airlock.pages.dev/?lang=en) переключает её на английский, а [`?lang=zh-TW`](https://upgrade-airlock.pages.dev/?lang=zh-TW) — на традиционный китайский. Транзакции, которые она показывает (предложения #1–#12), перечислены в [доказательствах реального прогона](docs/evidence/sepolia-live.md).
28
29## <a id="highlights"></a>Ключевые особенности
30
31- **Fail-closed по построению.** Обновление выполняется только при наличии допуска в блокчейне. Пропущенный лог, сбой RPC, расхождение провайдеров или неудачный прогон оставляют предложение без допуска или под вето: обновление может задержаться, но никогда не пройдёт без проверки. Вето всегда имеет приоритет над допуском.
32- **Атака, а не чек-лист.** Воркфлоу (при развёртывании в DON — на каждом узле) прогоняет обновление на реальном состоянии прокси через `eth_simulateV1` у четырёх RPC-провайдеров. Посторонний адрес с пополненным балансом вызывает каждый селектор из нового байткода, аргументы с подстановкой держателя подаются в каждую новую функцию, а эмиссия, владение и балансы сравниваются до и после. Ответить должны как минимум два провайдера, и все ответы должны совпасть.
33- **Вердикты привязаны к предложению.** Шаг идентичного консенсуса воркфлоу согласует `{verdict, rule}`. Отчёт содержит аттестацию EIP-712, привязанную к точному содержимому предложения (`proposalDigest`), к сети, к получателю и к предельному блоку, а также хеш доказательств, который любой может пересчитать по данным блокчейна.
34- **Пропущенные логи подбираются.** Cron-обходчик (sweeper) каждые 60 секунд проверяет 20 последних предложений и выносит вердикт по всем, которые пропустил log trigger, включая обновления, которым утверждающий (approver) выдал допуск вручную; его вето имеет приоритет. Предложение, вытесненное за пределы 20 последних id, больше не оценивается: оно остаётся без допуска, если только утверждающий не выдал его вручную ([окно обхода](docs/design.md#the-sweep-window)).
35- **Работает в Ethereum Sepolia.** Официальный CRE CLI выполнял воркфлоу на сервере и ответил в блокчейне на каждое предложение (двенадцать на 2026-10-25): одно честное обновление получило допуск и было выполнено, одиннадцать атак получили вето (R1, R2, R3, R3B, R7, R9). Все контракты верифицированы на Sourcify.
36- **Проверено сквозными тестами.** В [CI](.github/workflows/ci.yml) запускаются 117 тестов Foundry, 118 юнит-тестов воркфлоу и 387 из 394 самопроверок ситуационного центра (остальным 7 нужны результаты сборки forge и cast, которых в CI-задаче нет); e2e-скрипт на форке прогоняет весь цикл через симулятор CRE.
37
38## Проблема
39
40Обновляемый прокси ставит весь протокол в зависимость от ключа обновления. Как только этот ключ утекает, атака занимает всего несколько шагов: развернуть вредоносную реализацию, перенаправить на неё прокси, выпустить токены или вывести средства. Такое происходило уже не раз:
41
42- **PAID Network, март 2021.** С помощью ключей владельца прокси проверенный аудиторами код токена был заменён версией с функциями burn и mint, и атакующий выпустил около 59,5 млн PAID. ([разбор инцидента CertiK](https://www.certik.com/blog/paid-network-post-mortem))
43- **Ankr, декабрь 2022.** С помощью скомпрометированного ключа деплоера атакующий заменил реализацию aBNBc на версию, которая выпускала токены без проверки авторизации: 60 трлн aBNBc. ([отчёт Ankr](https://www.ankr.com/blog/the-abnbc-token-report/))
44- **Radiant Capital, октябрь 2024.** Атакующий, контролировавший 3 из 11 подписантов мультисига, захватил провайдер пулов кредитования и обновил реализацию пулов, выведя более $53 млн. ([rekt.news](https://rekt.news/radiant-capital-rekt2))
45
46Таймлок лишь заставляет атакующего подождать, и никто не обязан что-либо проверять, пока идёт ожидание. К тому же долгие задержки блокируют экстренные исправления. Upgrade Airlock заставляет время ожидания работать: обновление не может выполниться, пока его не пропустит независимая проверка (децентрализованная после развёртывания в CRE DON), и эта проверка автоматически запускается для каждого предложения.
47
48## <a id="how-it-works"></a>Как это работает
49
50```mermaid
51sequenceDiagram
52 actor Owner as Ключ владельца (считаем, что он утёк)
53 participant Admin as AirlockAdmin
54 participant CRE as Воркфлоу Chainlink CRE
55 participant RPC as 4 RPC-провайдера
56 participant Fwd as KeystoneForwarder
57 participant Recv as AirlockReceiver
58 actor Anyone as Кто угодно
59
60 Owner->>Admin: proposeUpgrade(newImpl, data)
61 Note over Admin: Внешняя дверь: период ожидания 25 блоков, около 5 минут в Sepolia
62 Admin-->>CRE: Лог Proposed (EVM log trigger) или cron-обходчик каждые 60 с
63 CRE->>Admin: Чтения EVM на блоке предложения
64 loop На каждом узле (в симуляторе один, при развёртывании каждый узел DON), для каждого провайдера
65 CRE->>RPC: eth_getCode, затем симуляция атаки через eth_simulateV1
66 RPC-->>CRE: вердикт и правило
67 end
68 Note over CRE: Кворум: не менее 2 ответов, все совпадают, иначе Veto R6<br/>идентичный консенсус по вердикту и правилу<br/>хеш доказательств и аттестация EIP-712
69 CRE->>Fwd: writeReport
70 Fwd->>Recv: onReport(metadata, report)
71 alt Все проверки пройдены
72 Recv->>Admin: clear(id, evidenceHash)
73 else Нарушено правило
74 Recv->>Admin: veto(id, ruleHash) и attachReason(id, evidenceHash)
75 end
76 Anyone->>Admin: execute(id)
77 Note over Admin: Внутренняя дверь: ожидание истекло, допуск есть, вето нет, codehash не изменился
78 Admin->>Admin: ProxyAdmin.upgradeAndCall(proxy, newImpl, data)
79```
80
811. **Предложение.** `AirlockAdmin` является владельцем `ProxyAdmin` из OpenZeppelin и `Ownable` токена. Ключ владельца может только предлагать: он никогда не бывает стражем (guardian), утверждающим (approver) или оракулом допуска. Каждое предложение (обновление или изменение самого шлюза: стражи, задержка, цель break-glass) получает `readyBlock = block.number + delayBlocks`. Целью обновления или break-glass может быть только обычный код контракта: делегированный аккаунт EIP-7702, ключ которого мог бы перенаправить его после вердикта, отклоняется (`DelegatedAccount`).
822. **Оценка.** Log trigger CRE срабатывает на событие `Proposed`. Изменение самого шлюза оценивается по данным, прочитанным из блокчейна (R7). Обновление сначала проходит статические правила (R5, R8), затем симуляцию атаки, привязанную к блоку предложения: слот реализации ERC-1967 прокси подменяется новым кодом, а новый посторонний адрес (выводится из хеша блока предложения) получает средства, как настоящий пользователь: токены от второго держателя и ETH. Держатель делает перевод, посторонний вызывает `transferFrom` без allowance, а затем — каждый селектор, найденный в новом байткоде, с пятью вариантами аргументов. Каждый селектор вне стандартного интерфейса токена дополнительно вызывается с подстановкой держателя: держатель с балансом — в каждом из первых четырёх слотов аргументов, тестовая сумма — в другом слоте, посторонний адрес или прокси — в остальных. Эмиссия, владелец и каждый отслеживаемый баланс считываются до и после, баланс держателя — сразу после каждого вызова с подстановкой держателя; кроме того, проверяются собственные события каждого пробного вызова (с R1 по R4, R3B). Наконец, новый байткод проверяется на опкоды, которые в реальном блоке ведут себя иначе, чем в симуляции (R9).
833. **Согласование.** Каждый узел (в развёртывании DON — каждый узел DON; сегодня — симулятор CLI) запускает симуляцию у четырёх провайдеров. Ответить должны минимум два, и все ответы должны содержать один и тот же вердикт; в любом другом случае — вето (R6). Затем шаг консенсуса согласует идентичный `{verdict, rule}`.
844. **Отчёт.** Воркфлоу хеширует канонический JSON доказательств, подписывает вердикт EIP-712 своим ключом аттестатора (секрет CRE) и записывает отчёт через форвардер. `AirlockReceiver` проверяет отправителя, актуальность, подпись и то, что вердикт привязан именно к этому предложению (`proposalDigest`), после чего вызывает в `AirlockAdmin` `clear` либо `veto` и `attachReason`.
855. **Выполнение.** Кто угодно может вызвать `execute(id)` после окончания периода ожидания. Вызов откатывается с ошибкой `TooEarly` во время ожидания, с `NotCleared` без допуска, с `ProposalVetoed` после вето и с `CodehashChanged`, если по целевому адресу больше нет кода, который был предложен и проверен. Вето всегда имеет приоритет над допуском.
866. **Обход.** Cron trigger каждые 60 секунд перепроверяет последние предложения и выносит вердикт по каждому ожидающему предложению, на которое CRE ещё не ответил (`oracleJudged`): лог, пропущенный триггером, событие `Proposed`, слишком большое для доставки, или обновление, которому утверждающий выдал допуск вручную. Его вето имеет приоритет над таким допуском.
87
88**Все сбои ведут в одну сторону.** Пропущенный лог, сбой RPC, расхождение провайдеров или неудачный прогон оставляют предложение без допуска или накладывают на него вето, то есть задерживают обновление и никогда его не пропускают. Если CRE не может ответить вовсе, ключ утверждающего может вручную выдать обновлению допуск с зафиксированными доказательствами; обходчик всё равно оценит это обновление, как только CRE снова начнёт отвечать, а вето CRE или стража по-прежнему имеет приоритет. `breakGlass()` позволяет владельцу или любому стражу мгновенно переключить прокси на заранее зарегистрированную реализацию в режиме паузы. Выход из режима паузы — обновление через обе двери; допуск предложения не истекает, поэтому завершить паузу может и ранее допущенное обновление ([известные ограничения](docs/design.md#known-limitations)).
89
90Подробнее: [docs/design.md](docs/design.md) (модель угроз, конечный автомат допуска, привязка отчёта, бюджеты) и [cre/README.md](cre/README.md) (воркфлоу).
91
92## <a id="rules"></a>Правила
93
94`ruleHash = keccak256("GUARD/<ID>")`, записывается в блокчейн при каждом вето.
95
96| Правило | Вето, если | Чем определяется |
97|---|---|---|
98| R1 `TOTAL_SUPPLY` | вызов постороннего изменяет `totalSupply` | симуляция атаки |
99| R2 `OWNER` | вызов постороннего изменяет `owner()` | симуляция атаки |
100| R3 `TRANSFER_CONSERVATION` | перевод держателем суммы X перемещает не ровно X или затрагивает чей-то ещё баланс | симуляция атаки |
101| R3B `THIRD_PARTY_FUNDS` | посторонний перемещает токены любого держателя: `transferFrom` пополненного постороннего адреса без allowance проходит успешно, либо один из его пробных вызовов перемещает чужие токены, выпускает или сжигает токены, передаёт владение или оставляет баланс держателя ниже прежнего — даже без события и даже если последующий вызов это отменяет | симуляция атаки |
102| R4 `TRANSFER_LIVENESS` | обычный перевод держателя не проходит | симуляция атаки |
103| R5 `STRICT_CODEHASH` | включён строгий режим, а утверждающий не одобрил codehash | чтение из блокчейна |
104| R6 `SIM_FAILURE` | симуляция не может вынести решение: провайдеры расходятся, отвечает слишком мало провайдеров, код не совпадает, откатывается view-вызов или пополнение постороннего адреса, либо пробные вызовы не умещаются в заданное конфигурацией число запросов | симуляция и кворум |
105| R7 `CONFIG_GUARD` | смена стража, сокращение задержки или новая цель break-glass без предварительного одобрения утверждающего | чтение из блокчейна |
106| R8 `INIT_DATA` | обновление содержит init data, которые утверждающий не одобрил вместе с codehash | чтение из блокчейна |
107| R9 `NEW_OPCODES` | новый код содержит опкод контекста блока или транзакции (BASEFEE, TIMESTAMP, NUMBER, ORIGIN, GAS и другие), внешний вызов, create или selfdestruct, не покрытые фиксированным allowlist конфигурации (по умолчанию пустым), если только утверждающий не одобрил именно этот codehash | байткод |
108
109**R9 закрывает обход симуляции.** `EvilBaseFeeImpl` ведёт себя как v1, пока `block.basefee == 0` — а именно это видит вызов `eth_simulateV1`, — и позволяет кому угодно выпускать токены в реальном блоке. Все инварианты в симуляции выполняются; вето ему приносит опкод `BASEFEE`. Allowlist никогда не зависит от кода, который прокси исполняет сейчас, поэтому одобрение распространяется на один codehash и ни на одно последующее обновление. Сканер пропускает хвост метаданных компилятора, только если это точная CBOR-карта solc, недостижимая ни для одной инструкции; всё остальное сканируется как код. Одобрение codehash освобождает его от R9 и допускает усечение пробных вызовов (R6) — и только: все остальные правила продолжают действовать.
110
111**R3B закрывает тихий вывод средств.** `EvilDrainImpl` — это v1 плюс `rescue(from, to, amount)` без контроля доступа. Эмиссия, владелец, переводы и опкоды у неё такие же, как у v1, поэтому правила с R1 по R4 и R9 проходят. `rescue` — не стандартная функция токена, поэтому посторонний вызывает её ещё и с подстановкой держателя; вызов с держателем в качестве `from` перемещает токены держателя, и R3B накладывает вето. Вывод средств, спрятанный за условием «только для вызывающих, у которых есть токены или ETH», тоже обнаруживается, потому что у постороннего есть и то и другое.
112
113### Автоматический и строгий режимы
114
115**Автоматический режим** (по умолчанию): CRE самостоятельно выдаёт допуск или вето каждому обновлению по итогам ограниченного поиска атак — внешний вызывающий с пополненным балансом, каждый селектор нового кода и держатель в каждом из первых четырёх слотов аргументов каждой новой функции. **Строгий режим** (R5) добавляет проверку человеком: каждый новый codehash реализации сначала должен одобрить ключ утверждающего, а симуляция атаки всё равно выполняется. Автоматический режим подходит токену, которому нужен допуск рутинных обновлений за считанные минуты; строгий режим делает код-ревью частью допуска ([два режима](docs/design.md#two-modes-automatic-and-strict)).
116
117## Chainlink CRE
118
119CRE — оркестрирующий слой Upgrade Airlock: воркфлоу — единственный автоматический путь через внутреннюю дверь, и он ведёт каждый шаг от ончейн-триггера до ончейн-вердикта. Воркфлоу написан на CRE TypeScript SDK (`@chainlink/cre-sdk` 1.23.0) и скомпилирован в WASM с помощью CRE CLI v1.37.0.
120
121| Технология Chainlink | Где используется | Роль в проекте |
122|---|---|---|
123| Раннер воркфлоу CRE | [`cre/veto/main.ts#L1-L9`](cre/veto/main.ts#L1-L9), [`cre/veto/workflow.yaml`](cre/veto/workflow.yaml), [`cre/project.yaml`](cre/project.yaml) | Два обработчика, цели `sepolia` и `sepolia-fork` |
124| EVM log trigger | [`cre/veto/workflow.ts#L202-L211`](cre/veto/workflow.ts#L202-L211), [`#L193-L198`](cre/veto/workflow.ts#L193-L198), [`#L54-L65`](cre/veto/workflow.ts#L54-L65) | `AirlockAdmin.Proposed` на `LATEST`: предложение оценивается сразу после появления, за целый период ожидания до того, как его можно будет выполнить |
125| Cron trigger | [`cre/veto/workflow.ts#L200`](cre/veto/workflow.ts#L200), [`#L209`](cre/veto/workflow.ts#L209), [`cre/veto/airlock.ts#L323-L389`](cre/veto/airlock.ts#L323-L389) | Обходчик: 20 последних id, каждую минуту |
126| Чтение EVM (`callContract`) | [`cre/veto/workflow.ts#L114-L122`](cre/veto/workflow.ts#L114-L122), [`#L137-L142`](cre/veto/workflow.ts#L137-L142), [`cre/veto/chain.ts`](cre/veto/chain.ts) | Предложение, допуск, `oracleJudged`, одобрения и балансы — пакетами через Multicall3 на блоке предложения или на `LATEST` |
127| Чтение EVM (`headerByNumber`) | [`cre/veto/workflow.ts#L125-L136`](cre/veto/workflow.ts#L125-L136) | Последний блок и хеш блока предложения, из которого выводится посторонний адрес для симуляции |
128| HTTP capability в режиме узла (node mode) | [`cre/veto/workflow.ts#L167-L189`](cre/veto/workflow.ts#L167-L189), [`#L80-L94`](cre/veto/workflow.ts#L80-L94), [`cre/veto/judge.ts#L157-L220`](cre/veto/judge.ts#L157-L220) | Каждый узел отправляет каждому провайдеру `eth_getCode`, затем один или два запроса `eth_simulateV1` (каждый — в пределах лимита запроса 120 kb) |
129| `consensusIdenticalAggregation` | [`cre/veto/workflow.ts#L184-L188`](cre/veto/workflow.ts#L184-L188), кворум в [`cre/veto/judge.ts#L244-L277`](cre/veto/judge.ts#L244-L277) | Идентичный консенсус по `{verdict, rule}` (не более 52 байт; при развёртывании — каждый узел DON) |
130| Секреты (Secrets) | [`cre/veto/workflow.ts#L160-L166`](cre/veto/workflow.ts#L160-L166), [`cre/secrets.yaml`](cre/secrets.yaml) | Ключ аттестатора EIP-712, загружается один раз за прогон |
131| Отчёт + `writeReport` | [`cre/veto/workflow.ts#L143-L149`](cre/veto/workflow.ts#L143-L149), [`cre/veto/airlock.ts#L229-L282`](cre/veto/airlock.ts#L229-L282), [`cre/veto/report.ts`](cre/veto/report.ts) | `runtime.report` (ECDSA, keccak256) и `evm.writeReport`, затем чтение предложения для подтверждения результата |
132| Форвардер + `IReceiver` | [`src/cre/AirlockReceiver.sol#L88-L120`](src/cre/AirlockReceiver.sol#L88-L120), [`#L156-L169`](src/cre/AirlockReceiver.sol#L156-L169), [`src/cre/IReceiver.sol`](src/cre/IReceiver.sol), [`script/DeployEth.s.sol#L142`](script/DeployEth.s.sol#L142) | `onReport` принимает отчёты только от настроенного форвардера и, если они заданы, проверяет workflow id и владельца воркфлоу |
133| Селекторы сетей (chain selectors) | [`cre/veto/workflow.ts#L100-L104`](cre/veto/workflow.ts#L100-L104) | `ethereum-testnet-sepolia` |
134| CRE CLI | [`cre/setup.sh`](cre/setup.sh), [`cre/scripts/fork-e2e.sh`](cre/scripts/fork-e2e.sh), [`scripts/sepolia/airlock.sh`](scripts/sepolia/airlock.sh), [`scripts/sepolia/sweep.sh`](scripts/sepolia/sweep.sh) | `cre workflow build`, `cre workflow simulate --broadcast` |
135
136**Как воркфлоу работает сегодня.** Воркфлоу запускается командой CRE CLI `cre workflow simulate --broadcast` в сети Ethereum Sepolia. Слушатель ([`scripts/sepolia/airlock.sh`](scripts/sepolia/airlock.sh)) держит log trigger в режиме прослушивания, обходчик ([`scripts/sepolia/sweep.sh`](scripts/sepolia/sweep.sh)) каждую минуту запускает cron-обработчик, а каждый Clear и Veto — реальная транзакция в Sepolia через MockKeystoneForwarder, развёрнутый Chainlink в Sepolia, [`0x15fC6ae953E024d975e77382eEeC56A9101f9F88`](https://docs.chain.link/cre/guides/workflow/using-evm-client/forwarder-directory-ts). `AirlockReceiver` в текущем виде готов к развёртыванию воркфлоу в DON: достаточно развернуть его с `FORWARDER`, указывающим на продакшн-KeystoneForwarder, и с заданными `WORKFLOW_ID` / `WORKFLOW_OWNER` — тогда `onReport` дополнительно проверяет подписанные DON метаданные воркфлоу. `DeployEth` отказывается использовать permissionless MockKeystoneForwarder, если только не заданы оба значения или явно не указан `ALLOW_MOCK_FORWARDER=1` ([`script/DeployEth.s.sol#L96-L104`](script/DeployEth.s.sol#L96-L104)).
137
138**Спроектирован под лимиты CRE на один прогон.** Не более 15 чтений из блокчейна (пакеты Multicall3 укладываются в лимит payload 5 kb), 12 из 15 HTTP-вызовов при четырёх провайдерах, одна симуляция атаки за прогон и значение консенсуса не более 52 байт. Бюджеты описаны в [docs/design.md](docs/design.md#budgets).
139
140## <a id="live-on-sepolia"></a>В сети Sepolia
141
142Ethereum Sepolia, chain id 11155111. Развёртывание выполнено скриптом [`script/DeployEth.s.sol`](script/DeployEth.s.sol) в блоках 11950684–11950699 (13 транзакций; AirlockAdmin в блоке 11950697); все контракты верифицированы на Sourcify. Владелец, утверждающий, аварийный страж и аттестатор — четыре разных ключа. Период ожидания — 25 блоков.
143
144| Контракт | Адрес |
145|---|---|
146| AirlockAdmin | [`0x28e7aAC40560804F480e454bF9C12F50C83b700B`](https://sepolia.etherscan.io/address/0x28e7aAC40560804F480e454bF9C12F50C83b700B) |
147| AirlockReceiver | [`0xE40D463116F4Aa24d5E79776a65ED0f1b088c670`](https://sepolia.etherscan.io/address/0xE40D463116F4Aa24d5E79776a65ED0f1b088c670) |
148| Прокси DemoBTC (dBTC) | [`0x88Cc569265faDAEAc2a16B69f35E10B2158e4b71`](https://sepolia.etherscan.io/address/0x88Cc569265faDAEAc2a16B69f35E10B2158e4b71) |
149| ProxyAdmin | [`0x51a3fE6AD22BF7753574B7b7FD56aE9a65bd593E`](https://sepolia.etherscan.io/address/0x51a3fE6AD22BF7753574B7b7FD56aE9a65bd593E) |
150| DemoBTC v1 | [`0x8f7d6453d77B3f3b008CBE4A144d82c5742eB108`](https://sepolia.etherscan.io/address/0x8f7d6453d77B3f3b008CBE4A144d82c5742eB108) |
151| DemoBTC v2 (честное обновление) | [`0x4D959f981C01E3B81ac6C9A20E84816Fe5f6Bce3`](https://sepolia.etherscan.io/address/0x4D959f981C01E3B81ac6C9A20E84816Fe5f6Bce3) |
152| EvilMintImpl | [`0x01502c4af90c27f53B2E1293941670C6B761CAAA`](https://sepolia.etherscan.io/address/0x01502c4af90c27f53B2E1293941670C6B761CAAA) |
153| EvilOwnerImpl | [`0xBe849DE8e0a4D9b0010ba1468f94Eeb24A42d488`](https://sepolia.etherscan.io/address/0xBe849DE8e0a4D9b0010ba1468f94Eeb24A42d488) |
154| EvilFeeImpl | [`0xB95Ffa7b970B1a73488e2fC32B787fA536F12AF5`](https://sepolia.etherscan.io/address/0xB95Ffa7b970B1a73488e2fC32B787fA536F12AF5) |
155| EvilBaseFeeImpl | [`0xE1AAa4E88DB9cc7ceCca14722eE2874D834e0310`](https://sepolia.etherscan.io/address/0xE1AAa4E88DB9cc7ceCca14722eE2874D834e0310) |
156| EvilDrainImpl | [`0x1d98D64BC5204AE139B5A0747855841Fa5333b6d`](https://sepolia.etherscan.io/address/0x1d98D64BC5204AE139B5A0747855841Fa5333b6d) |
157| PausedImpl (цель break-glass) | [`0xb6573006fB607E96966d0F578B96AAB25413F5BE`](https://sepolia.etherscan.io/address/0xb6573006fB607E96966d0F578B96AAB25413F5BE) |
158| Chainlink MockKeystoneForwarder | [`0x15fC6ae953E024d975e77382eEeC56A9101f9F88`](https://sepolia.etherscan.io/address/0x15fC6ae953E024d975e77382eEeC56A9101f9F88) |
159
160Первый прогон: семь предложений — семь вердиктов в блокчейне (последующие #8–#12 перечислены в [docs/evidence/sepolia-live.md](docs/evidence/sepolia-live.md)). Блок в каждой ссылке — это блок, в который попала транзакция; `(+n)` показывает, через сколько блоков после предложения пришёл вердикт CRE.
161
162| Карточка | Предложение | Вердикт CRE | Tx предложения | Tx отчёта | Затем |
163|---|---|---|---|---|---|
164| `v2`: честное обновление | #4 | Clear | [11950745](https://sepolia.etherscan.io/tx/0x3cd96ac220391e2b404d35591d1a96808b75256e7c685a414e2ca5a63bc54145) | [11950747 (+2)](https://sepolia.etherscan.io/tx/0x2c02d2a23016a72509900eb7ce92dabc06b0dd93a2394d076a62d687ce467efb) | `execute` [11950772](https://sepolia.etherscan.io/tx/0x01913c1854b9862763af74ddacd7be23a7f55232af7334714aa8213aa7021890), `version()` = "2" |
165| `evil-mint`: выпускать токены может кто угодно | #1 | Veto R1 | [11950735](https://sepolia.etherscan.io/tx/0x862c5fdf826a8fef09fe2ff278d3e58191f93c267dbdd38ad9aaa270f39937b0) | [11950736 (+1)](https://sepolia.etherscan.io/tx/0xb0d74e25e2528fb9aad7d21de202dac288cd256048f2986b5214280fda4b84f6) | `execute` откатывается с `ProposalVetoed` |
166| `evil-owner`: кто угодно может забрать владение | #6 | Veto R2 | [11950788](https://sepolia.etherscan.io/tx/0x32eb22039bd43602e0406b1a8f9a02c5cbcdb5cf2ef1c389919a45d05820da25) | [11950790 (+2)](https://sepolia.etherscan.io/tx/0x49451bee7a2f3351b18992d94b5f89b9ff89cf53be65a937400ab95fb79be551) | `execute` откатывается с `ProposalVetoed` |
167| `evil-fee`: с каждого перевода удерживается 10% | #7 | Veto R3 | [11950792](https://sepolia.etherscan.io/tx/0xe06924c2e3961ab27d2803c0654b783fc06e52107345bda9bfeff391053fcad3) | [11950794 (+2)](https://sepolia.etherscan.io/tx/0x01f6055a683e0dabc8f34ae094e5daa5a81ddb11c687aa9c5ac6e30c7a7f3d3b) | `execute` откатывается с `ProposalVetoed` |
168| `evil-basefee`: выпуск токенов открывается в реальном блоке | #3 | Veto R9 | [11950742](https://sepolia.etherscan.io/tx/0x3c29c118ca395ca92500abcaae6126244924fbd8e14f846af57b70f113cad6e1) | [11950743 (+1)](https://sepolia.etherscan.io/tx/0xb6db2d832112384287e5a7ccdd086127931360900d09c80645c21eac2bc165db) | `execute` откатывается с `ProposalVetoed` |
169| `evil-drain`: кто угодно перемещает токены держателя | #2 | Veto R3B | [11950737](https://sepolia.etherscan.io/tx/0x0e52369921040eb0c901ac3a077aff762d370b4efd9503a52c91360d56366b16) | [11950740 (+3)](https://sepolia.etherscan.io/tx/0x40bbe374a8cee47a94da977f973cf66a67573c12bc84117483fcbd11430f90cb) | `execute` откатывается с `ProposalVetoed` |
170| `remove-receiver`: убрать CRE из стражей | #5 | Veto R7 | [11950749](https://sepolia.etherscan.io/tx/0xda2bd1c9dc5f7770da3926358b8da3ffe5a1cd3154b499cafae440387eebb934) | [11950750 (+1)](https://sepolia.etherscan.io/tx/0xaa2f598bdcc7f46727e5456528521e50ac5c1d8e423805042cbf2d10a4c4cdac) | `execute` откатывается с `ProposalVetoed` |
171
172Все вердикты выше вынес официальный CRE CLI, выполнявший этот воркфлоу на сервере (слушатель log trigger плюс 60-секундный обходчик, [`scripts/sepolia/systemd/`](scripts/sepolia/systemd)); собственные строки лога воркфлоу приведены в [`docs/evidence/sepolia-live.md`](docs/evidence/sepolia-live.md).
173
174## <a id="quick-start"></a>Быстрый старт
175
176### Требования
177
178- [Foundry](https://getfoundry.sh) (`forge`, `cast`, `anvil`), `jq` и Node 22.
179- Аккаунт CRE для симулятора (`cre login`); он нужен для сквозного прогона на форке и для операторского набора.
180- [`cre/setup.sh`](cre/setup.sh) устанавливает CRE CLI v1.37.0 (с зафиксированным SHA-256) и bun 1.4.2 в `cre/.tools`.
181
182### Сборка и тесты
183
184```bash
185git clone https://gitlab.com/jiapunk/upgrade-airlock.git # или https://upgrade-airlock.pages.dev/source/upgrade-airlock.git
186cd upgrade-airlock
187
188# контракты (OpenZeppelin и forge-std уже включены в lib/)
189forge build # однократно загружает зафиксированную версию solc 0.8.37
190forge test --offline # 117 тестов
191
192# воркфлоу CRE
193cre/setup.sh
194export PATH="$PWD/cre/.tools/bin:$PWD/cre/.tools/node_modules/.bin:$PATH"
195(cd cre/veto && bun test && bun run typecheck) # 118 тестов: правила, пробные вызовы, байткод, отчёты, бюджеты, обработчики
196
197# ситуационный центр
198(cd web && npm ci && npm run selftest) # 394 проверки: режим реального времени и выбор атаки, офлайн-воспроизведение, API оператора
199```
200
201### Сквозной прогон на форке Sepolia
202
203```bash
204cre login # один раз; симулятору нужен аккаунт CRE
205cre/scripts/fork-e2e.sh # локальный anvil-форк Sepolia: в реальную сеть ничего не отправляется
206```
207
208[`cre/scripts/fork-e2e.sh`](cre/scripts/fork-e2e.sh) разворачивает стек на форке, запускает воркфлоу в симуляторе CRE с `--broadcast` и проверяет каждый результат в блокчейне:
209
210- **Log trigger:** EvilMint получает вето R1, EvilOwner — R2, EvilFee — R3, EvilBaseFee — R9 (с указанием `BASEFEE`), EvilDrain — R3B (с указанием `rescue`, при том что все остальные правила проходят); ещё семь вариантов вывода средств ([`test/fixtures/DrainVariants.sol`](test/fixtures/DrainVariants.sol)) получают вето только по R3B: переставленные аргументы, проверка адреса токена, вывод, доступный лишь вызывающим с токенами или ETH, вариант без события `Transfer` и `transferFrom`, который для держателей токенов пропускает проверку allowance; честный v2 получает допуск и выполняется после периода ожидания; удаление получателя из стражей получает вето R7; сокращение задержки, заранее одобренное утверждающим, получает допуск.
211- **Обходчик:** обновление, по которому никто не вынес вердикт, после периода ожидания откатывается с `NotCleared`, один прогон обходчика выдаёт ему допуск, и оно выполняется; два предложения, которым утверждающий выдал допуск вручную, оцениваются следующим прогоном: EvilDrain получает вето R3B поверх допуска, а увеличение задержки CRE тоже допускает; обновление с 6 KB init data получает вето R8 на последующем прогоне.
212- **Доказательства:** каждое предложение под вето откатывается с `ProposalVetoed`, а хеш доказательств каждого вердикта пересчитывается по данным блокчейна ([`cre/veto/tools/check-evidence.ts`](cre/veto/tools/check-evidence.ts)).
213
214### Операторский набор для Sepolia
215
216[`scripts/sepolia/`](scripts/sepolia/README.md) — скрипты, на которых работает онлайн-демо. Ключи хранятся вне репозитория, по одному hex-ключу на файл в `KEY_DIR` (`owner.key`, `attester.key`, `sender.key`, `approver.key`, `emergency.key`, а также необязательный `sweeper.key`); скрипты никогда их не выводят, а `deploy.sh` отказывается работать, если владелец, утверждающий, аварийный страж и аттестатор — не четыре разных ключа. Каждый скрипт можно запустить и на локальном форке с `FORK=<port>`, так что демо можно отрепетировать без каких-либо затрат.
217
218```bash
219scripts/sepolia/deploy.sh # 13 транзакций, записывает deployments/sepolia.json (FORCE=1 заменяет существующий)
220bun scripts/sepolia/make-config.mjs # конфигурация воркфлоу, 4 провайдера, кворум 2
221scripts/sepolia/fund-sender.sh 0.05 # ETH для транзакций с отчётами
222scripts/sepolia/airlock.sh # терминал A: слушатель CRE (log trigger)
223scripts/sepolia/propose.sh v2 # терминал B: затем evil-mint, evil-owner, evil-fee, evil-basefee, evil-drain, remove-receiver
224scripts/sepolia/execute.sh <id> # выполняет допущенное предложение после периода ожидания, иначе показывает откат
225scripts/sepolia/status.sh # все предложения: вердикт, оценено ли CRE, правило, доказательства, транзакции
226```
227
228`sweep.sh` однократно запускает cron-обходчик, `clear.sh` — ручной допуск утверждающего, `fork.sh` запускает, сохраняет и восстанавливает форк Sepolia, `probe-rpcs.ts` проверяет каждого провайдера собственным кодом оценки воркфлоу, а `systemd/` запускает слушатель и обходчик на Linux-сервере.
229
230### Ситуационный центр
231
232```bash
233cd web
234npm ci
235npm run dev # http://127.0.0.1:5173/
236npm run build:airlock # синхронизирует deployments/sepolia.json в страницу, собирает web/dist-airlock
237```
238
239Страница читает Sepolia напрямую, не хранит ключей и никогда ничего не подписывает и не отправляет. Она показывает каждое предложение в виде шлюза: период ожидания, допуск CRE, вето с правилом и доказательствами. Посетитель выбирает одну из семи карточек (шесть атак и честное обновление v2) и смотрит помеченный повтор самого свежего реального предложения этого вида в Sepolia (или живое предложение, пока оно в периоде ожидания) — см. [выбор атаки](web/README.md#attack-picker-what-a-visitor-sees). Для докладчика есть режим оператора (`web/operator/`, `web/functions/`): на dev-сервере он принимает только loopback-подключения, на Cloudflare Pages требует токен и выключен, пока не заданы оба секрета `OWNER_KEY` и `OPERATOR_TOKEN`. Он отправляет одно из тех же семи фиксированных предложений через `POST /api/operator/propose`, подписывая его ключом владельца на стороне сервера (дизайн и так исходит из того, что этот ключ утечёт: он может только предлагать); страница обычного посетителя его никогда не вызывает — см. [режим оператора](web/README.md#operator-mode-real-attacks-on-sepolia).
240
241| Параметр URL | Действие |
242|---|---|
243| `?mode=airlock` | По умолчанию. Просмотр Ethereum Sepolia в реальном времени, только чтение |
244| `?mode=offline` | Воспроизводит записанный прогон вообще без сетевого подключения |
245| `?lang=en`, `?lang=zh-TW`, `?lang=zh-CN` | Язык интерфейса (по умолчанию `zh-CN`; клавиша `L` переключает их по кругу) |
246| `?rpc=http://127.0.0.1:<port>` | Читает вместо этого локальный форк Sepolia — для репетиций |
247| `?op=1` | Режим оператора: запрашивает токен оператора (только для докладчика) |
248
249В режиме реального времени нажмите `P`, чтобы воспроизвести последние предложения по их событиям в блокчейне, и `1`–`7`, чтобы воспроизвести карточку из выбора атаки.
250
251## Структура репозитория
252
253```
254src/AirlockAdmin.sol две двери: предложения, период ожидания, вето стража, допуск, execute, breakGlass
255src/cre/AirlockReceiver.sol получатель отчётов CRE: форвардер, метаданные воркфлоу, аттестация EIP-712, привязка digest
256src/cre/IReceiver.sol интерфейс получателя Chainlink CRE
257src/DemoBTC.sol, DemoBTCV2.sol демо-токен (dBTC) v1 и честный v2
258src/PausedImpl.sol цель break-glass: любое изменение баланса откатывается
259src/attack/ EvilMintImpl, EvilOwnerImpl, EvilFeeImpl, EvilBaseFeeImpl, EvilDrainImpl
260script/DeployEth.s.sol развёртывание (13 транзакций) с проверками ролей, форвардера и связей после деплоя
261test/ тесты Foundry; fixtures/DrainVariants.sol содержит семь вариантов вывода средств
262cre/veto/ воркфлоу CRE: workflow.ts (capabilities), airlock.ts (обработчики), judge.ts (правила,
263 кворум), simulate.ts (eth_simulateV1), bytecode.ts (селекторы, R9), report.ts
264 (отчёт, EIP-712, доказательства), chain.ts (бюджет чтений), keys.ts, rules.ts, config.ts
265cre/veto/test/ юнит-тесты Bun
266cre/veto/tools/ check-evidence.ts пересчитывает каждый ончейн-хеш доказательств
267cre/scripts/fork-e2e.sh сквозной прогон на anvil-форке Sepolia
268cre/setup.sh зафиксированные версии CRE CLI и bun
269scripts/sepolia/ операторский набор для онлайн-демо, с unit-файлами systemd для сервера
270web/ ситуационный центр (Vite, TypeScript, viem)
271web/operator/, web/functions/ режим оператора: семь фиксированных предложений, подписанных на сервере (dev-сервер, Pages Functions)
272deployments/sepolia.json действующее развёртывание
273docs/design.md модель угроз, конечный автомат допуска, привязка отчёта, бюджеты
274docs/evidence/ запись реального прогона
275.github/workflows/ci.yml проверки контрактов, воркфлоу и ситуационного центра
276```
277
278## <a id="security-model"></a>Модель безопасности
279
280Upgrade Airlock исходит из того, что ключ владельца утечёт, и построен так, чтобы одного ключа владельца никогда не хватало для выполнения обновления.
281
282| Роль | Кто | Что может |
283|---|---|---|
284| Владелец (owner) | один ключ | предлагать обновления и изменения самого шлюза (стражи, задержка, цель break-glass); `breakGlass` |
285| Утверждающий (approver) | отдельный ключ | вручную выдавать допуск, когда CRE не может ответить; одобрять codehash, init data и изменения шлюза (`approveCodehash`) |
286| Аварийный страж (emergency guardian) | отдельный ключ | налагать `veto` на ожидающее предложение; `breakGlass` |
287| Аттестатор (attester) | секрет CRE, отдельный ключ | подписывать вердикты EIP-712, которые принимает `AirlockReceiver` |
288| AirlockReceiver | получатель отчётов CRE | оракул допуска (`clear`) и страж (`veto`, `attachReason`) |
289| Кто угодно | | вызывать `execute` для предложения, прошедшего обе двери |
290
291Роли никогда не пересекаются. Конструктор `AirlockAdmin` отклоняет владельца, который одновременно является утверждающим, стражем или оракулом допуска (`RoleOverlap`), а `DeployEth` выполняет развёртывание только тогда, когда владелец, утверждающий, аварийный страж и аттестатор — четыре разных адреса.
292
293**Утёкший ключ владельца может предлагать.** Каждое предложение у всех на виду проходит период ожидания, пока CRE его оценивает, и требует допуска, который владелец не может выдать себе сам. Владелец также может задействовать аварийный тормоз (`breakGlass`), который лишь переключает прокси на заранее зарегистрированную реализацию в режиме паузы; выход из этого режима — обновление через обе двери, и завершить его может также ранее допущенное, но так и не выполненное обновление.
294
295**Тот же ключ не может:**
296
297- выполнить обновление без допуска: у `AirlockAdmin` нет универсального пути вызова, и единственный способ сменить реализацию — `execute` допущенного обновления (или `breakGlass` на реализацию в режиме паузы);
298- сначала ослабить шлюз: удаление получателя CRE, сокращение задержки или замена цели break-glass — это тоже предложения, и без предварительного одобрения утверждающего R7 накладывает на них вето;
299- подменить код после вердикта: `execute` перепроверяет codehash цели (`CodehashChanged`), а делегированные аккаунты EIP-7702 отклоняются (`DelegatedAccount`);
300- подделать или повторно использовать вердикт: `AirlockReceiver` принимает отчёт только от настроенного форвардера, только подписанный аттестатором, не позднее его `validUntilBlock` и привязанный к `proposalDigest` предложения в этой сети и для этого получателя.
301
302Ручной допуск утверждающего — это ручное переопределение на случай, когда CRE не может ответить: обходчик всё равно оценивает такое предложение, пока оно входит в 20 последних id, а вето CRE или стража имеет приоритет. Полная модель угроз — в [docs/design.md](docs/design.md#threat-model).
303
304Это демонстрационные контракты для тестовой сети с известными ограничениями, каждое из которых закреплено проходящим тестом: допуски не истекают, ключ владельца нельзя сменить (украденный ключ может снова ставить токен на паузу после каждого снятия паузы), а один недобросовестный страж может держать токен на паузе. Список — в [docs/design.md](docs/design.md#known-limitations).
305
306## Технологии
307
308- **Solidity 0.8.37** и **Foundry**, с OpenZeppelin Contracts 5.7.0 (`ProxyAdmin`, `TransparentUpgradeableProxy`, `EIP712`, `ECDSA`)
309- **Chainlink CRE**: TypeScript SDK (`@chainlink/cre-sdk` 1.23.0) и CRE CLI v1.37.0 на Bun 1.4.2
310- **viem** для ABI-кодирования и чтения блокчейна — в воркфлоу и в ситуационном центре
311- **Vite** и TypeScript для ситуационного центра
312
313## Инструменты ИИ
314
315Upgrade Airlock создан под руководством автора с помощью [Claude Code](https://claude.com/claude-code): с ним написаны и проверены контракты, воркфлоу CRE, тесты, скрипты, ситуационный центр и эта документация.
316
317## Лицензия
318
319[MIT](LICENSE) © 2026 jiapunk