Upgrade Airlock
Todo upgrade de proxy passa por duas portas: um período de espera e uma liberação do Chainlink CRE que só o código que sobrevive a uma simulação de ataque consegue obter.
English · 简体中文 · 繁體中文 · 日本語 · 한국어 · Español · Français · Deutsch · Português (Brasil) · Русский · Tiếng Việt
Destaques · Como funciona · Regras · Chainlink CRE · Ao vivo na Sepolia · Início rápido · Modelo de segurança
Visão geral
O Upgrade Airlock coloca duas portas diante de cada upgrade de contrato. A porta externa é um período de espera
(cooldown) medido em blocos. A porta interna só se abre com uma liberação registrada on-chain. Automaticamente,
isso significa que um workflow do Chainlink CRE atacou a nova implementação em um simulador consultando quatro
provedores RPC independentes, pelo menos dois responderam e todas as respostas coincidiram, a etapa de consenso do
workflow (consensusIdenticalAggregation) chegou a um acordo sobre o veredito e um relatório Clear (liberação)
assinado foi registrado on-chain. Quando o CRE não consegue responder de forma alguma, a chave do aprovador pode
liberar manualmente, com evidência registrada; o varredor continua julgando essa proposta enquanto ela estiver entre
os 20 ids mais recentes, e um veto do CRE ou de um guardião prevalece. Uma implementação maliciosa que a busca de
ataques alcança recebe, em vez disso, um Veto assinado e não é executada, mesmo que o atacante detenha a chave de
upgrade; o modo estrito acrescenta uma revisão humana do código para o que uma busca limitada não alcança. Hoje os
vereditos vêm da CRE CLI oficial (cre workflow simulate --broadcast) executando o workflow em um servidor; os
contratos e o workflow foram escritos para uma DON, e migrar para lá exige um novo deploy no forwarder de produção da
Chainlink. Desenvolvido para o ETH Shenzhen 2026, na Ethereum Sepolia.
[!TIP] Demo ao vivo: upgrade-airlock.pages.dev · Vídeo da demo: upgrade-airlock.pages.dev/video (em chinês simplificado e inglês)
A war room lê a Ethereum Sepolia direto do seu navegador, em modo somente leitura. Cada proposta é representada como uma câmara de descompressão: a porta externa registra a proposta, a câmara faz a contagem regressiva do período de espera e a porta interna continua trancada até que uma liberação chegue on-chain (o Clear do Chainlink CRE, ou uma liberação manual do aprovador, que a war room identifica como tal). Um Veto ejeta a proposta, com a respectiva regra, o hash de evidência e a transação. Na primeira visita abre-se um seletor de ataques: cada cartão (seis ataques e o upgrade honesto v2) reproduz a proposta real mais recente do seu tipo na Sepolia, identificada como reprodução. A página abre em chinês simplificado:
?lang=enmuda para inglês e?lang=zh-TWpara chinês tradicional. As transações por trás dela (propostas #1–#12) estão listadas na evidência da execução ao vivo.
Destaques
- Fail-closed por construção. Um upgrade só é executado com uma liberação on-chain. Um log perdido, uma queda de RPC, provedores em desacordo ou uma execução com falha deixam a proposta sem liberação ou vetada: isso pode atrasar um upgrade, nunca deixá-lo passar. Um veto sempre prevalece sobre uma liberação.
- Um ataque, não um checklist. O workflow (em cada nó, em um deploy em DON) executa o upgrade sobre o estado real
do proxy com
eth_simulateV1em quatro provedores RPC. Um desconhecido com fundos chama todos os seletores do novo bytecode, argumentos no formato de detentor atingem cada função nova, e supply, propriedade e saldos são comparados antes e depois. Pelo menos dois provedores precisam responder, e todas as respostas precisam coincidir. - Vereditos vinculados à proposta. A etapa de consenso idêntico do workflow chega a um acordo sobre
{verdict, rule}. O relatório carrega uma atestação EIP-712 vinculada ao conteúdo exato da proposta (proposalDigest), à chain, ao receiver e a um prazo em blocos, além de um hash de evidência que qualquer pessoa pode recalcular a partir dos dados da chain. - Logs perdidos são recuperados. Um varredor cron verifica as 20 propostas mais recentes a cada 60 segundos e julga qualquer uma que o log trigger tenha deixado passar, inclusive upgrades que o aprovador liberou manualmente; o veto do varredor prevalece. Uma proposta empurrada para baixo dos 20 ids mais recentes não é mais julgada: fica sem liberação, a menos que o aprovador a tenha liberado manualmente (a janela de varredura).
- Ao vivo na Ethereum Sepolia. A CRE CLI oficial executou o workflow em um servidor e respondeu a todas as propostas on-chain (doze em 2026-10-25): um upgrade honesto liberado e executado, onze ataques vetados (R1, R2, R3, R3B, R7, R9). Todos os contratos estão verificados no Sourcify.
- Testado de ponta a ponta. A CI roda 117 testes Foundry, 118 testes unitários do workflow e 387 das 394 verificações de autoteste da war room (as outras 7 precisam da saída de build do forge e do cast, que o job de CI não tem); um script de ponta a ponta em fork percorre o ciclo completo no simulador do CRE.
O problema
Um proxy atualizável deixa o protocolo inteiro à mercê da sua chave de upgrade. Quando essa chave vaza, o ataque é curto: fazer o deploy de uma implementação maliciosa, apontar o proxy para ela, cunhar ou drenar. Isso já aconteceu muitas vezes:
- PAID Network, março de 2021. As chaves do owner do proxy trocaram o código auditado do token por uma versão com funções de burn e mint, e o atacante cunhou cerca de 59,5 milhões de PAID. (post-mortem da CertiK)
- Ankr, dezembro de 2022. Com a chave de deployer comprometida, o atacante substituiu a implementação do aBNBc por uma que cunhava sem verificação de autorização: 60 trilhões de aBNBc. (relatório da Ankr)
- Radiant Capital, outubro de 2024. Um atacante que controlava 3 de 11 signatários do multisig assumiu o controle do provider dos pools de empréstimo e fez upgrade da implementação desses pools, drenando mais de US$ 53 milhões. (rekt.news)
Um timelock só faz o atacante esperar, e ninguém é obrigado a olhar durante a espera. Atrasos longos também bloqueiam correções emergenciais. O Upgrade Airlock coloca a espera para trabalhar: um upgrade não pode ser executado até que uma verificação independente (descentralizada depois de implantada em uma DON do CRE) o aprove, e essa verificação roda automaticamente em toda proposta.
Como funciona
sequenceDiagram
actor Owner as Chave do owner (presuma que vaze)
participant Admin as AirlockAdmin
participant CRE as Workflow do Chainlink CRE
participant RPC as 4 provedores RPC
participant Fwd as KeystoneForwarder
participant Recv as AirlockReceiver
actor Anyone as Qualquer pessoa
Owner->>Admin: proposeUpgrade(newImpl, data)
Note over Admin: Porta externa: espera de 25 blocos, cerca de 5 minutos na Sepolia
Admin-->>CRE: log Proposed (EVM log trigger), ou o varredor cron a cada 60 s
CRE->>Admin: leituras EVM no bloco da proposta
loop Em cada nó (um no simulador, cada nó da DON quando implantado), para cada provedor
CRE->>RPC: eth_getCode e depois a simulação de ataque eth_simulateV1
RPC-->>CRE: veredito e regra
end
Note over CRE: Quórum: ao menos 2 respostas, todas iguais, senão Veto R6<br/>consenso idêntico sobre veredito e regra<br/>hash de evidência e atestação EIP-712
CRE->>Fwd: writeReport
Fwd->>Recv: onReport(metadata, report)
alt Todas as verificações passaram
Recv->>Admin: clear(id, evidenceHash)
else Uma regra falhou
Recv->>Admin: veto(id, ruleHash) e attachReason(id, evidenceHash)
end
Anyone->>Admin: execute(id)
Note over Admin: Porta interna: espera cumprida, liberada, sem veto, codehash inalterado
Admin->>Admin: ProxyAdmin.upgradeAndCall(proxy, newImpl, data)
- Propor. O
AirlockAdminé dono doProxyAdminda OpenZeppelin e doOwnabledo token. A chave do owner só pode propor: nunca é guardião, aprovador nem oráculo de liberação. Toda proposta (um upgrade ou uma mudança no próprio airlock: guardiões, atraso, alvo do break-glass) recebereadyBlock = block.number + delayBlocks. O alvo de um upgrade ou do break-glass precisa ser código de contrato comum: uma conta delegada via EIP-7702, cuja chave poderia redirecioná-la depois do veredito, é recusada (DelegatedAccount). - Julgar. O log trigger do CRE dispara no evento
Proposed. Uma mudança no airlock é julgada a partir de leituras on-chain (R7). Um upgrade passa primeiro pelas regras estáticas (R5, R8) e depois pela simulação de ataque, fixada no bloco da proposta: o slot de implementação ERC-1967 do proxy é sobrescrito com o novo código, e um endereço desconhecido recém-gerado (derivado do hash do bloco da proposta) recebe fundos como um usuário real, com tokens de um segundo detentor e ETH. Um detentor faz uma transferência, o desconhecido chamatransferFromsem allowance e, em seguida, todos os seletores encontrados no novo bytecode com cinco formatos de argumentos. Todo seletor fora da interface padrão de token também é chamado com argumentos no formato de detentor: o detentor com fundos em cada um dos quatro primeiros slots de argumento, o valor de teste em outro, o desconhecido ou o proxy nos demais. Supply, owner e cada saldo observado são lidos antes e depois, o saldo do detentor logo após cada chamada no formato de detentor, e os eventos emitidos por cada sonda são verificados (R1 a R4, R3B). Por fim, o novo bytecode é varrido em busca de opcodes que se comportam de forma diferente em um bloco real e em uma simulação (R9). - Concordar. Cada nó (cada nó da DON em um deploy em DON; hoje, o simulador da CLI) executa a simulação em quatro
provedores. Pelo menos dois precisam responder e todas as respostas precisam trazer o mesmo veredito; qualquer outro
resultado é um veto (R6). Em seguida, a etapa de consenso chega a um acordo sobre um
{verdict, rule}idêntico. - Reportar. O workflow calcula o hash de um JSON canônico de evidência, assina um veredito EIP-712 com sua chave
de attester (um secret do CRE) e grava o relatório por meio do forwarder. O
AirlockReceiververifica o remetente, se o relatório ainda está dentro do prazo, a assinatura e se o veredito está vinculado exatamente a esta proposta (proposalDigest), e então chamaclear, ouvetoeattachReason, noAirlockAdmin. - Executar. Qualquer pessoa pode chamar
execute(id)assim que o período de espera terminar. A chamada reverte comTooEarlydurante o período de espera,NotClearedsem liberação,ProposalVetoedapós um veto eCodehashChangedquando o alvo já não contém o código que foi proposto e julgado. Um veto sempre prevalece sobre uma liberação. - Varrer. Um cron trigger reverifica as propostas mais recentes a cada 60 segundos e julga toda proposta
pendente a que o CRE ainda não respondeu (
oracleJudged): um log que o trigger deixou passar, um eventoProposedgrande demais para ser entregue ou um upgrade que o aprovador liberou manualmente. O veto do varredor prevalece sobre essa liberação.
Toda falha aponta na mesma direção. Um log perdido, uma queda de RPC, provedores em desacordo ou uma execução com
falha deixam a proposta sem liberação ou a vetam; assim, atrasam um upgrade e nunca deixam um passar. Quando o CRE
não consegue responder de forma alguma, a chave do aprovador pode liberar um upgrade manualmente, com evidência
registrada; o varredor ainda julga esse upgrade assim que o CRE voltar a responder, e um veto do CRE ou de um guardião
continua prevalecendo. breakGlass() permite que o owner ou qualquer guardião troque o proxy imediatamente para uma
implementação pausada pré-registrada. Sair do estado pausado é um upgrade pelas duas portas; uma proposta liberada nunca expira, então um upgrade liberado
antes também pode encerrar a pausa (limitações conhecidas).
Mais detalhes: docs/design.md (modelo de ameaças, máquina de estados da liberação, vinculação do relatório, orçamentos) e cre/README.md (o workflow).
Regras
ruleHash = keccak256("GUARD/<ID>"), registrado on-chain a cada veto.
| Regra | Veta quando | Decidido por |
|---|---|---|
R1 TOTAL_SUPPLY |
uma chamada de um desconhecido altera totalSupply |
simulação de ataque |
R2 OWNER |
uma chamada de um desconhecido altera owner() |
simulação de ataque |
R3 TRANSFER_CONSERVATION |
a transferência de X feita por um detentor não move exatamente X, ou altera o saldo de qualquer outra pessoa | simulação de ataque |
R3B THIRD_PARTY_FUNDS |
um terceiro movimenta tokens de qualquer detentor: o transferFrom do desconhecido com fundos, sem allowance, é bem-sucedido, ou uma de suas sondas move tokens de outra pessoa, cunha, queima ou transfere a propriedade, ou deixa o saldo do detentor menor, mesmo sem evento ou quando uma chamada posterior desfaz a alteração |
simulação de ataque |
R4 TRANSFER_LIVENESS |
uma transferência simples de um detentor falha | simulação de ataque |
R5 STRICT_CODEHASH |
o modo estrito está ativado e o aprovador não aprovou o codehash | leituras on-chain |
R6 SIM_FAILURE |
a simulação não consegue decidir: provedores discordam, poucos respondem, o código não confere, uma view ou o financiamento do desconhecido reverte, ou as sondas não cabem nas requisições configuradas | simulação e quórum |
R7 CONFIG_GUARD |
uma mudança de guardião, um atraso menor ou um novo alvo de break-glass sem a pré-aprovação do aprovador | leituras on-chain |
R8 INIT_DATA |
um upgrade traz dados de inicialização que o aprovador não aprovou junto com o codehash | leituras on-chain |
R9 NEW_OPCODES |
o novo código contém um opcode de contexto de bloco ou de transação (BASEFEE, TIMESTAMP, NUMBER, ORIGIN, GAS e outros), uma chamada externa, create ou selfdestruct que a allowlist fixa da configuração (vazia por padrão) não cobre, a menos que o aprovador tenha aprovado exatamente esse codehash | bytecode |
A R9 bloqueia a evasão da simulação. EvilBaseFeeImpl se comporta como a v1 enquanto block.basefee == 0, que
é o que uma chamada eth_simulateV1 enxerga, e permite que qualquer um cunhe em um bloco real. Todos os invariantes
simulados passam; o opcode BASEFEE faz com que ela seja vetada. A allowlist nunca depende do código que o proxy
executa hoje, então uma aprovação cobre um único codehash e nenhum upgrade posterior. A varredura ignora a cauda de
metadados do compilador apenas quando ela é um mapa CBOR exato do solc que nenhuma instrução consegue alcançar;
qualquer outra coisa é varrida como código. Aprovar um codehash o isenta da R9 e tolera o truncamento das sondas (R6),
nada além disso: todas as outras regras continuam rodando.
A R3B bloqueia o dreno silencioso. EvilDrainImpl é a v1 mais rescue(from, to, amount) sem controle de acesso.
Supply, owner, transferências e opcodes são os da v1, então R1 a R4 e R9 passam todas. rescue não é uma função
padrão de token, então o desconhecido também a chama com argumentos no formato de detentor; a chamada com o detentor
como from move os tokens do detentor, e a R3B a veta. Um dreno que se esconde atrás de "apenas para chamadores que possuem
tokens ou ETH" também é alcançado, porque o desconhecido possui os dois.
Modo automático e modo estrito
O modo automático, o padrão, faz o CRE liberar ou vetar cada upgrade por conta própria, por meio de uma busca de ataque limitada: um chamador externo com fundos, todos os seletores do novo código e o detentor em cada um dos quatro primeiros slots de argumento de toda função nova. O modo estrito (R5) acrescenta uma revisão humana: o codehash de cada nova implementação precisa antes ser aprovado pela chave do aprovador, e a simulação de ataque continua rodando. O modo automático é indicado para um token que quer upgrades de rotina liberados em minutos; o modo estrito torna uma revisão de código parte da liberação (os dois modos).
Chainlink CRE
O CRE é a camada de orquestração do Upgrade Airlock: o workflow é o único caminho automatizado pela porta interna e
conduz cada etapa, do trigger on-chain ao veredito on-chain. Ele foi escrito com o SDK TypeScript do CRE
(@chainlink/cre-sdk 1.23.0) e compilado para WASM com a CRE CLI v1.37.0.
| Tecnologia Chainlink | Onde | O que faz aqui |
|---|---|---|
| CRE workflow runner | cre/veto/main.ts#L1-L9, cre/veto/workflow.yaml, cre/project.yaml |
Dois handlers, targets sepolia e sepolia-fork |
| EVM log trigger | cre/veto/workflow.ts#L202-L211, #L193-L198, #L54-L65 |
AirlockAdmin.Proposed em LATEST: uma proposta é julgada assim que aparece, um período de espera inteiro antes de poder ser executada |
| Cron trigger | cre/veto/workflow.ts#L200, #L209, cre/veto/airlock.ts#L323-L389 |
O varredor: os 20 ids mais recentes, a cada minuto |
EVM read (callContract) |
cre/veto/workflow.ts#L114-L122, #L137-L142, cre/veto/chain.ts |
Proposta, liberação, oracleJudged, aprovações e saldos, agrupados via Multicall3 no bloco da proposta ou em LATEST |
EVM read (headerByNumber) |
cre/veto/workflow.ts#L125-L136 |
O bloco mais recente e o hash do bloco da proposta, que serve de semente para o desconhecido simulado |
| HTTP capability em node mode | cre/veto/workflow.ts#L167-L189, #L80-L94, cre/veto/judge.ts#L157-L220 |
Cada nó envia a cada provedor eth_getCode e, em seguida, uma ou duas requisições eth_simulateV1 (cada uma abaixo do limite de 120 kb por requisição) |
consensusIdenticalAggregation |
cre/veto/workflow.ts#L184-L188, quórum em cre/veto/judge.ts#L244-L277 |
Consenso idêntico sobre {verdict, rule} (no máximo 52 bytes; cada nó da DON quando implantado) |
| Secrets | cre/veto/workflow.ts#L160-L166, cre/secrets.yaml |
A chave de attester EIP-712, buscada uma vez por execução |
Report + writeReport |
cre/veto/workflow.ts#L143-L149, cre/veto/airlock.ts#L229-L282, cre/veto/report.ts |
runtime.report (ECDSA, keccak256) e evm.writeReport, seguidos de uma leitura da proposta para confirmar o resultado |
Forwarder + IReceiver |
src/cre/AirlockReceiver.sol#L88-L120, #L156-L169, src/cre/IReceiver.sol, script/DeployEth.s.sol#L142 |
onReport aceita apenas o forwarder configurado e verifica o workflow id e o owner quando configurados |
| 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 |
Como o workflow roda hoje. O workflow roda com o cre workflow simulate --broadcast da CRE CLI na Ethereum
Sepolia. O listener (scripts/sepolia/airlock.sh) mantém o log trigger em modo de
escuta, o varredor (scripts/sepolia/sweep.sh) executa o handler cron a cada minuto, e
cada Clear e Veto é uma transação real na Sepolia através do MockKeystoneForwarder da Chainlink na Sepolia
0x15fC6ae953E024d975e77382eEeC56A9101f9F88.
O AirlockReceiver está pronto para um deploy em DON tal como está: faça o deploy com FORWARDER apontando para o
KeystoneForwarder de produção e com WORKFLOW_ID / WORKFLOW_OWNER definidos, e onReport passa a verificar também
os metadados do workflow assinados pela DON. O DeployEth recusa o MockKeystoneForwarder permissionless, a menos que
ambos estejam definidos ou que ALLOW_MOCK_FORWARDER=1 o autorize explicitamente
(script/DeployEth.s.sol#L96-L104).
Feito para os limites por execução do CRE. No máximo 15 leituras on-chain (lotes Multicall3 abaixo do limite de payload de 5 kb), 12 de 15 chamadas HTTP com quatro provedores, uma simulação de ataque por execução e um valor de consenso de no máximo 52 bytes. Os orçamentos estão em docs/design.md.
Ao vivo na Sepolia
Ethereum Sepolia, chain id 11155111. Deploy feito com script/DeployEth.s.sol nos
blocos 11950684–11950699 (13 transações; AirlockAdmin no bloco 11950697); todos os contratos estão verificados no Sourcify. Owner, aprovador, guardião de emergência
e attester são quatro chaves diferentes. O período de espera é de 25 blocos.
| Contrato | Endereço |
|---|---|
| AirlockAdmin | 0x28e7aAC40560804F480e454bF9C12F50C83b700B |
| AirlockReceiver | 0xE40D463116F4Aa24d5E79776a65ED0f1b088c670 |
| Proxy DemoBTC (dBTC) | 0x88Cc569265faDAEAc2a16B69f35E10B2158e4b71 |
| ProxyAdmin | 0x51a3fE6AD22BF7753574B7b7FD56aE9a65bd593E |
| DemoBTC v1 | 0x8f7d6453d77B3f3b008CBE4A144d82c5742eB108 |
| DemoBTC v2 (upgrade honesto) | 0x4D959f981C01E3B81ac6C9A20E84816Fe5f6Bce3 |
| EvilMintImpl | 0x01502c4af90c27f53B2E1293941670C6B761CAAA |
| EvilOwnerImpl | 0xBe849DE8e0a4D9b0010ba1468f94Eeb24A42d488 |
| EvilFeeImpl | 0xB95Ffa7b970B1a73488e2fC32B787fA536F12AF5 |
| EvilBaseFeeImpl | 0xE1AAa4E88DB9cc7ceCca14722eE2874D834e0310 |
| EvilDrainImpl | 0x1d98D64BC5204AE139B5A0747855841Fa5333b6d |
| PausedImpl (alvo do break-glass) | 0xb6573006fB607E96966d0F578B96AAB25413F5BE |
| Chainlink MockKeystoneForwarder | 0x15fC6ae953E024d975e77382eEeC56A9101f9F88 |
A primeira execução: sete propostas, sete vereditos on-chain (as execuções seguintes, #8–#12, estão listadas em
docs/evidence/sepolia-live.md). O bloco em cada link é o bloco em que a transação foi incluída; (+n) indica
quantos blocos após a proposta o veredito do CRE chegou.
| Cartão | Proposta | Veredito do CRE | Tx da proposta | Tx do relatório | Depois |
|---|---|---|---|---|---|
v2: upgrade honesto |
#4 | Clear | 11950745 | 11950747 (+2) | execute 11950772, version() = "2" |
evil-mint: qualquer um pode cunhar |
#1 | Veto R1 | 11950735 | 11950736 (+1) | execute reverte com ProposalVetoed |
evil-owner: qualquer um pode tomar a propriedade |
#6 | Veto R2 | 11950788 | 11950790 (+2) | execute reverte com ProposalVetoed |
evil-fee: transferências desviam 10% |
#7 | Veto R3 | 11950792 | 11950794 (+2) | execute reverte com ProposalVetoed |
evil-basefee: o mint é liberado em um bloco real |
#3 | Veto R9 | 11950742 | 11950743 (+1) | execute reverte com ProposalVetoed |
evil-drain: qualquer um move os tokens de um detentor |
#2 | Veto R3B | 11950737 | 11950740 (+3) | execute reverte com ProposalVetoed |
remove-receiver: remove o CRE como guardião |
#5 | Veto R7 | 11950749 | 11950750 (+1) | execute reverte com ProposalVetoed |
Todos os vereditos acima vieram da CRE CLI oficial executando este workflow em um servidor (o listener do log trigger
mais o varredor de 60 segundos, scripts/sepolia/systemd/); as linhas de log do próprio
workflow estão em docs/evidence/sepolia-live.md.
Início rápido
Pré-requisitos
- Foundry (
forge,cast,anvil),jqe Node 22. - Uma conta CRE para o simulador (
cre login), usada pela execução de ponta a ponta em fork e pelo kit do operador. cre/setup.shinstala a CRE CLI v1.37.0 (com SHA-256 fixado) e o bun 1.4.2 emcre/.tools.
Build e testes
git clone https://gitlab.com/jiapunk/upgrade-airlock.git # ou https://upgrade-airlock.pages.dev/source/upgrade-airlock.git
cd upgrade-airlock
# contratos (OpenZeppelin e forge-std já vêm incluídos em lib/)
forge build # baixa o solc 0.8.37 fixado uma única vez
forge test --offline # 117 testes
# o workflow do 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 testes: regras, sondas, bytecode, relatórios, orçamentos, handlers
# a war room
(cd web && npm ci && npm run selftest) # 394 verificações: visão ao vivo e seletor de ataques, reprodução offline, API do operador
Ponta a ponta em um fork da Sepolia
cre login # uma vez; o simulador precisa de uma conta CRE
cre/scripts/fork-e2e.sh # um fork local da Sepolia no anvil: nada é enviado a uma rede real
cre/scripts/fork-e2e.sh faz o deploy da stack no fork, executa o workflow no simulador do
CRE com --broadcast e verifica cada resultado on-chain:
- Log trigger: EvilMint vetado por R1, EvilOwner por R2, EvilFee por R3, EvilBaseFee por R9 (que cita
BASEFEE), EvilDrain por R3B (que citarescue, com todas as outras regras passando); mais sete drenos (test/fixtures/DrainVariants.sol) vetados apenas por R3B: argumentos reordenados, uma proteção por endereço de token, um dreno aberto só a chamadores que possuem tokens ou ETH, um sem eventoTransfere umtransferFromque ignora a allowance para detentores de tokens; a v2 honesta liberada e executada após o período de espera; a remoção do receiver como guardião vetada por R7; um atraso menor, pré-aprovado pelo aprovador, liberado. - Varredor: um upgrade que ninguém julgou reverte com
NotClearedapós o período de espera, uma execução do varredor o libera e ele é executado; duas propostas que o aprovador liberou manualmente são julgadas na execução seguinte: EvilDrain vetado por R3B, prevalecendo sobre a liberação, e um atraso maior liberado também pelo CRE; um upgrade com 6 KB de dados de inicialização é vetado por R8 na execução seguinte. - Evidência: toda proposta vetada reverte com
ProposalVetoed, e o hash de evidência de cada veredito é recalculado a partir dos dados on-chain (cre/veto/tools/check-evidence.ts).
Kit do operador da Sepolia
scripts/sepolia/ opera a demo ao vivo. As chaves ficam fora do repositório, uma chave
hex por arquivo em KEY_DIR (owner.key, attester.key, sender.key, approver.key,
emergency.key, mais um sweeper.key opcional); os scripts nunca as imprimem,
e deploy.sh se recusa a rodar a menos que owner, aprovador, guardião de emergência e attester sejam quatro chaves
diferentes. Todo script também roda contra um fork local com FORK=<port>, então a demo pode ser ensaiada sem gastar
nada.
scripts/sepolia/deploy.sh # 13 transações, grava deployments/sepolia.json (FORCE=1 substitui um existente)
bun scripts/sepolia/make-config.mjs # a configuração do workflow, 4 provedores, quórum 2
scripts/sepolia/fund-sender.sh 0.05 # ETH para as transações de relatório
scripts/sepolia/airlock.sh # terminal A: o listener do CRE (log trigger)
scripts/sepolia/propose.sh v2 # terminal B: depois evil-mint, evil-owner, evil-fee, evil-basefee, evil-drain, remove-receiver
scripts/sepolia/execute.sh <id> # executa uma proposta liberada após o período de espera; caso contrário, mostra o revert
scripts/sepolia/status.sh # todas as propostas: veredito, julgada ou não pelo CRE, regra, evidência, txs
sweep.sh executa o varredor cron uma vez, clear.sh é a liberação manual do aprovador, fork.sh inicia, salva e
restaura um fork da Sepolia, probe-rpcs.ts verifica cada provedor com o próprio código de julgamento do workflow, e
systemd/ roda o listener e o varredor em um servidor Linux.
War room
cd web
npm ci
npm run dev # http://127.0.0.1:5173/
npm run build:airlock # sincroniza deployments/sepolia.json com a página e gera web/dist-airlock
A página lê a Sepolia diretamente; não guarda nenhuma chave e nunca assina nem envia nada. Ela mostra cada proposta
como uma câmara de descompressão: o período de espera, a liberação do CRE, o veto com sua regra e evidência. Os
visitantes escolhem um de sete cartões (seis ataques e o upgrade honesto v2) e assistem a uma reprodução identificada
da proposta real mais recente desse tipo na Sepolia (ou à proposta ao vivo enquanto ela estiver no período de espera):
veja o seletor de ataques. Para quem apresenta há um modo operador
(web/operator/, web/functions/): só via loopback no servidor de desenvolvimento e com token no Cloudflare Pages,
onde fica desligado a menos que existam os dois secrets OWNER_KEY e OPERATOR_TOKEN. Ele envia uma das mesmas sete
propostas fixas por POST /api/operator/propose, assinada no servidor com a chave do owner (o design já assume que
essa chave vaza: ela só pode propor), e a página de um visitante nunca o chama: veja o modo
operador.
| Parâmetro de URL | Efeito |
|---|---|
?mode=airlock |
Padrão. Visão ao vivo da Ethereum Sepolia, somente leitura |
?mode=offline |
Reproduz uma execução gravada, sem nenhuma conexão de rede |
?lang=en, ?lang=zh-TW, ?lang=zh-CN |
Idioma da interface (padrão zh-CN; L alterna entre eles) |
?rpc=http://127.0.0.1:<port> |
Lê um fork local da Sepolia no lugar da rede, para ensaios |
?op=1 |
Modo operador: pede o token do operador (só para quem apresenta) |
Pressione P no modo ao vivo para reproduzir as propostas mais recentes a partir de seus eventos on-chain, e 1 a
7 para reproduzir um cartão do seletor.
Estrutura do repositório
src/AirlockAdmin.sol as duas portas: propostas, período de espera, veto de guardião, liberação, execute, breakGlass
src/cre/AirlockReceiver.sol receiver de relatórios do CRE: forwarder, metadados do workflow, atestação EIP-712, vínculo ao digest
src/cre/IReceiver.sol interface de receiver do Chainlink CRE
src/DemoBTC.sol, DemoBTCV2.sol token de demonstração (dBTC) v1 e a v2 honesta
src/PausedImpl.sol alvo do break-glass: toda alteração de saldo reverte
src/attack/ EvilMintImpl, EvilOwnerImpl, EvilFeeImpl, EvilBaseFeeImpl, EvilDrainImpl
script/DeployEth.s.sol deploy (13 transações) com verificações de papéis, do forwarder e da configuração pós-deploy
test/ testes Foundry; fixtures/DrainVariants.sol contém as sete variantes de dreno
cre/veto/ o workflow do CRE: workflow.ts (capabilities), airlock.ts (handlers), judge.ts (regras,
quórum), simulate.ts (eth_simulateV1), bytecode.ts (seletores, R9), report.ts
(relatório, EIP-712, evidência), chain.ts (orçamento de leituras), keys.ts, rules.ts, config.ts
cre/veto/test/ testes unitários em Bun
cre/veto/tools/ check-evidence.ts recalcula cada hash de evidência on-chain
cre/scripts/fork-e2e.sh ponta a ponta em um fork da Sepolia no anvil
cre/setup.sh CRE CLI e bun com versões fixadas
scripts/sepolia/ kit do operador da demo ao vivo, com units systemd para um servidor
web/ war room (Vite, TypeScript, viem)
web/operator/, web/functions/ modo operador: sete propostas fixas assinadas no servidor (servidor de dev, Pages Functions)
deployments/sepolia.json o deploy ao vivo
docs/design.md modelo de ameaças, máquina de estados da liberação, vinculação do relatório, orçamentos
docs/evidence/ a execução ao vivo registrada
.github/workflows/ci.yml verificações dos contratos, do workflow e da war room
Modelo de segurança
O Upgrade Airlock parte do princípio de que a chave do owner vaza, e foi construído para que a chave do owner sozinha nunca consiga executar um upgrade.
| Papel | Mantido por | Pode |
|---|---|---|
| Owner | uma chave | propor upgrades e mudanças no airlock (guardiões, atraso, alvo do break-glass); breakGlass |
| Aprovador | uma chave separada | liberar manualmente quando o CRE não consegue responder; aprovar codehashes, dados de inicialização e mudanças no airlock (approveCodehash) |
| Guardião de emergência | uma chave separada | veto de uma proposta pendente; breakGlass |
| Attester | um secret do CRE, com chave própria | assinar os vereditos EIP-712 que o AirlockReceiver aceita |
| AirlockReceiver | o receiver de relatórios do CRE | oráculo de liberação (clear) e guardião (veto, attachReason) |
| Qualquer pessoa | execute de uma proposta que passou pelas duas portas |
Os papéis nunca se sobrepõem. O construtor do AirlockAdmin recusa um owner que também seja o aprovador, um guardião
ou o oráculo de liberação (RoleOverlap), e o DeployEth só faz o deploy quando owner, aprovador, guardião de
emergência e attester são quatro endereços diferentes.
Uma chave de owner vazada pode propor. Toda proposta cumpre o período de espera à vista de todos enquanto o CRE a
julga, e precisa de uma liberação que o owner não pode conceder a si mesmo. O owner também pode puxar o freio de
emergência (breakGlass), que apenas troca o proxy para a implementação pausada pré-registrada; sair dela é um
upgrade pelas duas portas, e um upgrade liberado antes e nunca executado também pode encerrá-la.
A mesma chave não pode:
- executar sem liberação: o
AirlockAdminnão tem um caminho de chamada genérico, e a única forma de trocar a implementação é oexecutede um upgrade liberado (ou obreakGlasspara a implementação pausada); - enfraquecer o airlock antes: remover o receiver do CRE, encurtar o atraso ou trocar o alvo do break-glass também são propostas, e a R7 as veta sem a pré-aprovação do aprovador;
- trocar o código depois do veredito:
executereverifica o codehash do alvo (CodehashChanged), e contas delegadas via EIP-7702 são recusadas (DelegatedAccount); - forjar ou reutilizar um veredito: o
AirlockReceiversó aceita um relatório vindo do forwarder configurado, assinado pelo attester, até o seuvalidUntilBlocke vinculado aoproposalDigestda proposta nesta chain e neste receiver.
A liberação manual do aprovador é um override para quando o CRE não consegue responder: o varredor ainda julga essa proposta enquanto ela estiver entre os 20 ids mais recentes, e um veto do CRE ou de um guardião prevalece. O modelo de ameaças completo está em docs/design.md.
Estes são contratos de demonstração para testnet com limitações conhecidas, cada uma fixada por um teste que passa: as liberações nunca expiram, a chave de owner não pode ser trocada (uma chave roubada pode pausar o token de novo após cada retomada) e um único guardião desonesto pode mantê-lo pausado. Elas estão listadas em docs/design.md.
Tecnologias
- Solidity 0.8.37 e Foundry, com OpenZeppelin Contracts 5.7.0 (
ProxyAdmin,TransparentUpgradeableProxy,EIP712,ECDSA) - Chainlink CRE: o SDK TypeScript (
@chainlink/cre-sdk1.23.0) e a CRE CLI v1.37.0, sobre Bun 1.4.2 - viem para codificação ABI e leituras on-chain, no workflow e na war room
- Vite e TypeScript para a war room
Ferramentas de IA
O Upgrade Airlock foi construído com o Claude Code sob a direção do autor: os contratos, o workflow do CRE, os testes, os scripts, a war room e estes documentos foram escritos e verificados com ele.
Licença
MIT © 2026 jiapunk
Source
| 1 | <div align="center"> |
| 2 | |
| 3 | # Upgrade Airlock |
| 4 | |
| 5 | **Todo upgrade de proxy passa por duas portas: um período de espera e uma liberação do Chainlink CRE que só o código que sobrevive a uma simulação de ataque consegue obter.** |
| 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.ru.md) · [Tiếng Việt](README.vi.md) |
| 8 | |
| 9 | [](https://gitlab.com/jiapunk/upgrade-airlock) |
| 10 | [](LICENSE) |
| 11 | [](https://upgrade-airlock.pages.dev) |
| 12 | [](https://upgrade-airlock.pages.dev/video/) |
| 13 | [](https://sepolia.etherscan.io/address/0x28e7aAC40560804F480e454bF9C12F50C83b700B) |
| 14 | [](#chainlink-cre) |
| 15 | |
| 16 | [Destaques](#highlights) · [Como funciona](#how-it-works) · [Regras](#rules) · [Chainlink CRE](#chainlink-cre) · [Ao vivo na Sepolia](#live-on-sepolia) · [Início rápido](#quick-start) · [Modelo de segurança](#security-model) |
| 17 | |
| 18 | </div> |
| 19 | |
| 20 | ## Visão geral |
| 21 | |
| 22 | O Upgrade Airlock coloca duas portas diante de cada upgrade de contrato. A **porta externa** é um período de espera |
| 23 | (cooldown) medido em blocos. A **porta interna** só se abre com uma liberação registrada on-chain. Automaticamente, |
| 24 | isso significa que um workflow do Chainlink CRE atacou a nova implementação em um simulador consultando quatro |
| 25 | provedores RPC independentes, pelo menos dois responderam e todas as respostas coincidiram, a etapa de consenso do |
| 26 | workflow (`consensusIdenticalAggregation`) chegou a um acordo sobre o veredito e um relatório **Clear** (liberação) |
| 27 | assinado foi registrado on-chain. Quando o CRE não consegue responder de forma alguma, a chave do aprovador pode |
| 28 | liberar manualmente, com evidência registrada; o varredor continua julgando essa proposta enquanto ela estiver entre |
| 29 | os 20 ids mais recentes, e um veto do CRE ou de um guardião prevalece. Uma implementação maliciosa que a busca de |
| 30 | ataques alcança recebe, em vez disso, um **Veto** assinado e não é executada, mesmo que o atacante detenha a chave de |
| 31 | upgrade; o modo estrito acrescenta uma revisão humana do código para o que uma busca limitada não alcança. Hoje os |
| 32 | vereditos vêm da CRE CLI oficial (`cre workflow simulate --broadcast`) executando o workflow em um servidor; os |
| 33 | contratos e o workflow foram escritos para uma DON, e migrar para lá exige um novo deploy no forwarder de produção da |
| 34 | Chainlink. Desenvolvido para o ETH Shenzhen 2026, na Ethereum Sepolia. |
| 35 | |
| 36 | > [!TIP] |
| 37 | > **Demo ao vivo: [upgrade-airlock.pages.dev](https://upgrade-airlock.pages.dev)** · **Vídeo da demo: |
| 38 | > [upgrade-airlock.pages.dev/video](https://upgrade-airlock.pages.dev/video/)** (em chinês simplificado e inglês) |
| 39 | > |
| 40 | > A war room lê a Ethereum Sepolia direto do seu navegador, em modo somente leitura. Cada proposta é representada como |
| 41 | > uma câmara de descompressão: a **porta externa** registra a proposta, a **câmara** faz a contagem regressiva do |
| 42 | > período de espera e a **porta interna** continua trancada até que uma liberação chegue on-chain (o Clear do |
| 43 | > Chainlink CRE, ou uma liberação manual do aprovador, que a war room identifica como tal). Um Veto ejeta a proposta, |
| 44 | > com a respectiva regra, o hash de evidência e a transação. Na primeira visita abre-se um seletor de ataques: cada |
| 45 | > cartão (seis ataques e o upgrade honesto v2) reproduz a proposta real mais recente do seu tipo na Sepolia, |
| 46 | > identificada como reprodução. A página abre em chinês simplificado: |
| 47 | > [`?lang=en`](https://upgrade-airlock.pages.dev/?lang=en) muda para inglês e |
| 48 | > [`?lang=zh-TW`](https://upgrade-airlock.pages.dev/?lang=zh-TW) para chinês tradicional. As transações por trás dela |
| 49 | > (propostas #1–#12) estão listadas na [evidência da execução ao vivo](docs/evidence/sepolia-live.md). |
| 50 | |
| 51 | ## <a id="highlights"></a>Destaques |
| 52 | |
| 53 | - **Fail-closed por construção.** Um upgrade só é executado com uma liberação on-chain. Um log perdido, uma queda de |
| 54 | RPC, provedores em desacordo ou uma execução com falha deixam a proposta sem liberação ou vetada: isso pode atrasar |
| 55 | um upgrade, nunca deixá-lo passar. Um veto sempre prevalece sobre uma liberação. |
| 56 | - **Um ataque, não um checklist.** O workflow (em cada nó, em um deploy em DON) executa o upgrade sobre o estado real |
| 57 | do proxy com |
| 58 | `eth_simulateV1` em quatro provedores RPC. Um desconhecido com fundos chama todos os seletores do novo bytecode, |
| 59 | argumentos no formato de detentor atingem cada função nova, e supply, propriedade e saldos são comparados antes e |
| 60 | depois. Pelo menos dois provedores precisam responder, e todas as respostas precisam coincidir. |
| 61 | - **Vereditos vinculados à proposta.** A etapa de consenso idêntico do workflow chega a um acordo sobre `{verdict, rule}`. O relatório carrega uma |
| 62 | atestação EIP-712 vinculada ao conteúdo exato da proposta (`proposalDigest`), à chain, ao receiver e a um prazo em |
| 63 | blocos, além de um hash de evidência que qualquer pessoa pode recalcular a partir dos dados da chain. |
| 64 | - **Logs perdidos são recuperados.** Um varredor cron verifica as 20 propostas mais recentes a cada 60 segundos e |
| 65 | julga qualquer uma que o log trigger tenha deixado passar, inclusive upgrades que o aprovador liberou manualmente; o |
| 66 | veto do varredor prevalece. Uma proposta empurrada para baixo dos 20 ids mais recentes não é mais julgada: fica sem |
| 67 | liberação, a menos que o aprovador a tenha liberado manualmente ([a janela de varredura](docs/design.md#the-sweep-window)). |
| 68 | - **Ao vivo na Ethereum Sepolia.** A CRE CLI oficial executou o workflow em um servidor e respondeu a todas as |
| 69 | propostas on-chain (doze em 2026-10-25): um upgrade honesto liberado e executado, onze ataques vetados (R1, R2, R3, |
| 70 | R3B, R7, R9). Todos os |
| 71 | contratos estão verificados no Sourcify. |
| 72 | - **Testado de ponta a ponta.** A [CI](.github/workflows/ci.yml) roda 117 testes Foundry, 118 testes unitários do |
| 73 | workflow e 387 das 394 verificações de autoteste da war room (as outras 7 precisam da saída de build do forge e do |
| 74 | cast, que o job de CI não tem); um script de ponta a ponta em fork percorre o ciclo completo no simulador do CRE. |
| 75 | |
| 76 | ## O problema |
| 77 | |
| 78 | Um proxy atualizável deixa o protocolo inteiro à mercê da sua chave de upgrade. Quando essa chave vaza, o ataque é |
| 79 | curto: fazer o deploy de uma implementação maliciosa, apontar o proxy para ela, cunhar ou drenar. Isso já aconteceu |
| 80 | muitas vezes: |
| 81 | |
| 82 | - **PAID Network, março de 2021.** As chaves do owner do proxy trocaram o código auditado do token por uma versão com |
| 83 | funções de burn e mint, e o atacante cunhou cerca de 59,5 milhões de PAID. |
| 84 | ([post-mortem da CertiK](https://www.certik.com/blog/paid-network-post-mortem)) |
| 85 | - **Ankr, dezembro de 2022.** Com a chave de deployer comprometida, o atacante substituiu a implementação do aBNBc |
| 86 | por uma que cunhava sem verificação de autorização: 60 trilhões de aBNBc. |
| 87 | ([relatório da Ankr](https://www.ankr.com/blog/the-abnbc-token-report/)) |
| 88 | - **Radiant Capital, outubro de 2024.** Um atacante que controlava 3 de 11 signatários do multisig assumiu o |
| 89 | controle do provider dos pools de empréstimo e fez upgrade da implementação desses pools, drenando mais de |
| 90 | US$ 53 milhões. |
| 91 | ([rekt.news](https://rekt.news/radiant-capital-rekt2)) |
| 92 | |
| 93 | Um timelock só faz o atacante esperar, e ninguém é obrigado a olhar durante a espera. Atrasos longos também bloqueiam |
| 94 | correções emergenciais. O Upgrade Airlock coloca a espera para trabalhar: um upgrade não pode ser executado até que |
| 95 | uma verificação independente (descentralizada depois de implantada em uma DON do CRE) o aprove, e essa verificação roda automaticamente em toda proposta. |
| 96 | |
| 97 | ## <a id="how-it-works"></a>Como funciona |
| 98 | |
| 99 | ```mermaid |
| 100 | sequenceDiagram |
| 101 | actor Owner as Chave do owner (presuma que vaze) |
| 102 | participant Admin as AirlockAdmin |
| 103 | participant CRE as Workflow do Chainlink CRE |
| 104 | participant RPC as 4 provedores RPC |
| 105 | participant Fwd as KeystoneForwarder |
| 106 | participant Recv as AirlockReceiver |
| 107 | actor Anyone as Qualquer pessoa |
| 108 | |
| 109 | Owner->>Admin: proposeUpgrade(newImpl, data) |
| 110 | Note over Admin: Porta externa: espera de 25 blocos, cerca de 5 minutos na Sepolia |
| 111 | Admin-->>CRE: log Proposed (EVM log trigger), ou o varredor cron a cada 60 s |
| 112 | CRE->>Admin: leituras EVM no bloco da proposta |
| 113 | loop Em cada nó (um no simulador, cada nó da DON quando implantado), para cada provedor |
| 114 | CRE->>RPC: eth_getCode e depois a simulação de ataque eth_simulateV1 |
| 115 | RPC-->>CRE: veredito e regra |
| 116 | end |
| 117 | Note over CRE: Quórum: ao menos 2 respostas, todas iguais, senão Veto R6<br/>consenso idêntico sobre veredito e regra<br/>hash de evidência e atestação EIP-712 |
| 118 | CRE->>Fwd: writeReport |
| 119 | Fwd->>Recv: onReport(metadata, report) |
| 120 | alt Todas as verificações passaram |
| 121 | Recv->>Admin: clear(id, evidenceHash) |
| 122 | else Uma regra falhou |
| 123 | Recv->>Admin: veto(id, ruleHash) e attachReason(id, evidenceHash) |
| 124 | end |
| 125 | Anyone->>Admin: execute(id) |
| 126 | Note over Admin: Porta interna: espera cumprida, liberada, sem veto, codehash inalterado |
| 127 | Admin->>Admin: ProxyAdmin.upgradeAndCall(proxy, newImpl, data) |
| 128 | ``` |
| 129 | |
| 130 | 1. **Propor.** O `AirlockAdmin` é dono do `ProxyAdmin` da OpenZeppelin e do `Ownable` do token. A chave do owner só |
| 131 | pode propor: nunca é guardião, aprovador nem oráculo de liberação. Toda proposta (um upgrade ou uma mudança no |
| 132 | próprio airlock: guardiões, atraso, alvo do break-glass) recebe `readyBlock = block.number + delayBlocks`. O alvo |
| 133 | de um upgrade ou do break-glass precisa ser código de contrato comum: uma conta delegada via EIP-7702, cuja chave |
| 134 | poderia redirecioná-la depois do veredito, é recusada (`DelegatedAccount`). |
| 135 | 2. **Julgar.** O log trigger do CRE dispara no evento `Proposed`. Uma mudança no airlock é julgada a partir de |
| 136 | leituras on-chain (R7). Um upgrade passa primeiro pelas regras estáticas (R5, R8) e depois pela simulação de |
| 137 | ataque, fixada no bloco da proposta: o slot de implementação ERC-1967 do proxy é sobrescrito com o novo código, e um |
| 138 | endereço desconhecido recém-gerado (derivado do hash do bloco da proposta) recebe fundos como um usuário real, com |
| 139 | tokens de um segundo detentor e ETH. Um detentor faz uma transferência, o desconhecido chama `transferFrom` sem |
| 140 | allowance e, em seguida, todos os seletores encontrados no novo bytecode com cinco formatos de argumentos. Todo |
| 141 | seletor fora da interface padrão de token também é chamado com argumentos no formato de detentor: o detentor com |
| 142 | fundos em cada um dos quatro primeiros slots de argumento, o valor de teste em outro, o desconhecido ou o proxy nos |
| 143 | demais. Supply, owner e cada saldo observado são lidos antes e depois, o saldo do detentor logo após cada chamada no |
| 144 | formato de detentor, e os eventos emitidos por cada sonda são verificados (R1 a R4, R3B). Por fim, o novo bytecode |
| 145 | é varrido em busca de opcodes que se comportam de forma diferente em um bloco real e em uma simulação (R9). |
| 146 | 3. **Concordar.** Cada nó (cada nó da DON em um deploy em DON; hoje, o simulador da CLI) executa a simulação em quatro |
| 147 | provedores. Pelo menos dois precisam responder e todas as respostas precisam trazer o mesmo veredito; qualquer outro |
| 148 | resultado é um veto (R6). Em seguida, a etapa de consenso chega a um acordo sobre um `{verdict, rule}` idêntico. |
| 149 | 4. **Reportar.** O workflow calcula o hash de um JSON canônico de evidência, assina um veredito EIP-712 com sua chave |
| 150 | de attester (um secret do CRE) e grava o relatório por meio do forwarder. O `AirlockReceiver` verifica o |
| 151 | remetente, se o relatório ainda está dentro do prazo, a assinatura e se o veredito está vinculado exatamente a esta |
| 152 | proposta (`proposalDigest`), e então chama `clear`, ou `veto` e `attachReason`, no `AirlockAdmin`. |
| 153 | 5. **Executar.** Qualquer pessoa pode chamar `execute(id)` assim que o período de espera terminar. A chamada reverte |
| 154 | com `TooEarly` durante o período de espera, `NotCleared` sem liberação, `ProposalVetoed` após um veto e |
| 155 | `CodehashChanged` quando o alvo já não contém o código que foi proposto e julgado. Um veto sempre prevalece sobre |
| 156 | uma liberação. |
| 157 | 6. **Varrer.** Um cron trigger reverifica as propostas mais recentes a cada 60 segundos e julga toda proposta |
| 158 | pendente a que o CRE ainda não respondeu (`oracleJudged`): um log que o trigger deixou passar, um evento `Proposed` |
| 159 | grande demais para ser entregue ou um upgrade que o aprovador liberou manualmente. O veto do varredor prevalece |
| 160 | sobre essa liberação. |
| 161 | |
| 162 | **Toda falha aponta na mesma direção.** Um log perdido, uma queda de RPC, provedores em desacordo ou uma execução com |
| 163 | falha deixam a proposta sem liberação ou a vetam; assim, atrasam um upgrade e nunca deixam um passar. Quando o CRE |
| 164 | não consegue responder de forma alguma, a chave do aprovador pode liberar um upgrade manualmente, com evidência |
| 165 | registrada; o varredor ainda julga esse upgrade assim que o CRE voltar a responder, e um veto do CRE ou de um guardião |
| 166 | continua prevalecendo. `breakGlass()` permite que o owner ou qualquer guardião troque o proxy imediatamente para uma |
| 167 | implementação pausada pré-registrada. Sair do estado pausado é um upgrade pelas duas portas; uma proposta liberada nunca expira, então um upgrade liberado |
| 168 | antes também pode encerrar a pausa ([limitações conhecidas](docs/design.md#known-limitations)). |
| 169 | |
| 170 | Mais detalhes: [docs/design.md](docs/design.md) (modelo de ameaças, máquina de estados da liberação, vinculação do |
| 171 | relatório, orçamentos) e [cre/README.md](cre/README.md) (o workflow). |
| 172 | |
| 173 | ## <a id="rules"></a>Regras |
| 174 | |
| 175 | `ruleHash = keccak256("GUARD/<ID>")`, registrado on-chain a cada veto. |
| 176 | |
| 177 | | Regra | Veta quando | Decidido por | |
| 178 | |---|---|---| |
| 179 | | R1 `TOTAL_SUPPLY` | uma chamada de um desconhecido altera `totalSupply` | simulação de ataque | |
| 180 | | R2 `OWNER` | uma chamada de um desconhecido altera `owner()` | simulação de ataque | |
| 181 | | R3 `TRANSFER_CONSERVATION` | a transferência de X feita por um detentor não move exatamente X, ou altera o saldo de qualquer outra pessoa | simulação de ataque | |
| 182 | | R3B `THIRD_PARTY_FUNDS` | um terceiro movimenta tokens de qualquer detentor: o `transferFrom` do desconhecido com fundos, sem allowance, é bem-sucedido, ou uma de suas sondas move tokens de outra pessoa, cunha, queima ou transfere a propriedade, ou deixa o saldo do detentor menor, mesmo sem evento ou quando uma chamada posterior desfaz a alteração | simulação de ataque | |
| 183 | | R4 `TRANSFER_LIVENESS` | uma transferência simples de um detentor falha | simulação de ataque | |
| 184 | | R5 `STRICT_CODEHASH` | o modo estrito está ativado e o aprovador não aprovou o codehash | leituras on-chain | |
| 185 | | R6 `SIM_FAILURE` | a simulação não consegue decidir: provedores discordam, poucos respondem, o código não confere, uma view ou o financiamento do desconhecido reverte, ou as sondas não cabem nas requisições configuradas | simulação e quórum | |
| 186 | | R7 `CONFIG_GUARD` | uma mudança de guardião, um atraso menor ou um novo alvo de break-glass sem a pré-aprovação do aprovador | leituras on-chain | |
| 187 | | R8 `INIT_DATA` | um upgrade traz dados de inicialização que o aprovador não aprovou junto com o codehash | leituras on-chain | |
| 188 | | R9 `NEW_OPCODES` | o novo código contém um opcode de contexto de bloco ou de transação (BASEFEE, TIMESTAMP, NUMBER, ORIGIN, GAS e outros), uma chamada externa, create ou selfdestruct que a allowlist fixa da configuração (vazia por padrão) não cobre, a menos que o aprovador tenha aprovado exatamente esse codehash | bytecode | |
| 189 | |
| 190 | **A R9 bloqueia a evasão da simulação.** `EvilBaseFeeImpl` se comporta como a v1 enquanto `block.basefee == 0`, que |
| 191 | é o que uma chamada `eth_simulateV1` enxerga, e permite que qualquer um cunhe em um bloco real. Todos os invariantes |
| 192 | simulados passam; o opcode `BASEFEE` faz com que ela seja vetada. A allowlist nunca depende do código que o proxy |
| 193 | executa hoje, então uma aprovação cobre um único codehash e nenhum upgrade posterior. A varredura ignora a cauda de |
| 194 | metadados do compilador apenas quando ela é um mapa CBOR exato do solc que nenhuma instrução consegue alcançar; |
| 195 | qualquer outra coisa é varrida como código. Aprovar um codehash o isenta da R9 e tolera o truncamento das sondas (R6), |
| 196 | nada além disso: todas as outras regras continuam rodando. |
| 197 | |
| 198 | **A R3B bloqueia o dreno silencioso.** `EvilDrainImpl` é a v1 mais `rescue(from, to, amount)` sem controle de acesso. |
| 199 | Supply, owner, transferências e opcodes são os da v1, então R1 a R4 e R9 passam todas. `rescue` não é uma função |
| 200 | padrão de token, então o desconhecido também a chama com argumentos no formato de detentor; a chamada com o detentor |
| 201 | como `from` move os tokens do detentor, e a R3B a veta. Um dreno que se esconde atrás de "apenas para chamadores que possuem |
| 202 | tokens ou ETH" também é alcançado, porque o desconhecido possui os dois. |
| 203 | |
| 204 | ### Modo automático e modo estrito |
| 205 | |
| 206 | **O modo automático**, o padrão, faz o CRE liberar ou vetar cada upgrade por conta própria, por meio de uma busca de |
| 207 | ataque limitada: um chamador externo com fundos, todos os seletores do novo código e o detentor em cada um dos quatro |
| 208 | primeiros slots de argumento de toda função nova. **O modo estrito** (R5) acrescenta uma revisão humana: o codehash |
| 209 | de cada nova implementação precisa antes ser aprovado pela chave do aprovador, e a simulação de ataque continua |
| 210 | rodando. O modo automático é indicado para um token que quer upgrades de rotina liberados em minutos; o modo estrito |
| 211 | torna uma revisão de código parte da liberação ([os dois modos](docs/design.md#two-modes-automatic-and-strict)). |
| 212 | |
| 213 | ## Chainlink CRE |
| 214 | |
| 215 | O CRE é a camada de orquestração do Upgrade Airlock: o workflow é o único caminho automatizado pela porta interna e |
| 216 | conduz cada etapa, do trigger on-chain ao veredito on-chain. Ele foi escrito com o SDK TypeScript do CRE |
| 217 | (`@chainlink/cre-sdk` 1.23.0) e compilado para WASM com a CRE CLI v1.37.0. |
| 218 | |
| 219 | | Tecnologia Chainlink | Onde | O que faz aqui | |
| 220 | |---|---|---| |
| 221 | | CRE workflow runner | [`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) | Dois handlers, targets `sepolia` e `sepolia-fork` | |
| 222 | | 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` em `LATEST`: uma proposta é julgada assim que aparece, um período de espera inteiro antes de poder ser executada | |
| 223 | | 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) | O varredor: os 20 ids mais recentes, a cada minuto | |
| 224 | | EVM read (`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) | Proposta, liberação, `oracleJudged`, aprovações e saldos, agrupados via Multicall3 no bloco da proposta ou em `LATEST` | |
| 225 | | EVM read (`headerByNumber`) | [`cre/veto/workflow.ts#L125-L136`](cre/veto/workflow.ts#L125-L136) | O bloco mais recente e o hash do bloco da proposta, que serve de semente para o desconhecido simulado | |
| 226 | | HTTP capability em 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) | Cada nó envia a cada provedor `eth_getCode` e, em seguida, uma ou duas requisições `eth_simulateV1` (cada uma abaixo do limite de 120 kb por requisição) | |
| 227 | | `consensusIdenticalAggregation` | [`cre/veto/workflow.ts#L184-L188`](cre/veto/workflow.ts#L184-L188), quórum em [`cre/veto/judge.ts#L244-L277`](cre/veto/judge.ts#L244-L277) | Consenso idêntico sobre `{verdict, rule}` (no máximo 52 bytes; cada nó da DON quando implantado) | |
| 228 | | Secrets | [`cre/veto/workflow.ts#L160-L166`](cre/veto/workflow.ts#L160-L166), [`cre/secrets.yaml`](cre/secrets.yaml) | A chave de attester EIP-712, buscada uma vez por execução | |
| 229 | | Report + `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) e `evm.writeReport`, seguidos de uma leitura da proposta para confirmar o resultado | |
| 230 | | Forwarder + `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` aceita apenas o forwarder configurado e verifica o workflow id e o owner quando configurados | |
| 231 | | Chain selectors | [`cre/veto/workflow.ts#L100-L104`](cre/veto/workflow.ts#L100-L104) | `ethereum-testnet-sepolia` | |
| 232 | | 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` | |
| 233 | |
| 234 | **Como o workflow roda hoje.** O workflow roda com o `cre workflow simulate --broadcast` da CRE CLI na Ethereum |
| 235 | Sepolia. O listener ([`scripts/sepolia/airlock.sh`](scripts/sepolia/airlock.sh)) mantém o log trigger em modo de |
| 236 | escuta, o varredor ([`scripts/sepolia/sweep.sh`](scripts/sepolia/sweep.sh)) executa o handler cron a cada minuto, e |
| 237 | cada Clear e Veto é uma transação real na Sepolia através do MockKeystoneForwarder da Chainlink na Sepolia |
| 238 | [`0x15fC6ae953E024d975e77382eEeC56A9101f9F88`](https://docs.chain.link/cre/guides/workflow/using-evm-client/forwarder-directory-ts). |
| 239 | O `AirlockReceiver` está pronto para um deploy em DON tal como está: faça o deploy com `FORWARDER` apontando para o |
| 240 | KeystoneForwarder de produção e com `WORKFLOW_ID` / `WORKFLOW_OWNER` definidos, e `onReport` passa a verificar também |
| 241 | os metadados do workflow assinados pela DON. O `DeployEth` recusa o MockKeystoneForwarder permissionless, a menos que |
| 242 | ambos estejam definidos ou que `ALLOW_MOCK_FORWARDER=1` o autorize explicitamente |
| 243 | ([`script/DeployEth.s.sol#L96-L104`](script/DeployEth.s.sol#L96-L104)). |
| 244 | |
| 245 | **Feito para os limites por execução do CRE.** No máximo 15 leituras on-chain (lotes Multicall3 abaixo do limite de |
| 246 | payload de 5 kb), 12 de 15 chamadas HTTP com quatro provedores, uma simulação de ataque por execução e um valor de |
| 247 | consenso de no máximo 52 bytes. Os orçamentos estão em [docs/design.md](docs/design.md#budgets). |
| 248 | |
| 249 | ## <a id="live-on-sepolia"></a>Ao vivo na Sepolia |
| 250 | |
| 251 | Ethereum Sepolia, chain id 11155111. Deploy feito com [`script/DeployEth.s.sol`](script/DeployEth.s.sol) nos |
| 252 | blocos 11950684–11950699 (13 transações; AirlockAdmin no bloco 11950697); todos os contratos estão verificados no Sourcify. Owner, aprovador, guardião de emergência |
| 253 | e attester são quatro chaves diferentes. O período de espera é de 25 blocos. |
| 254 | |
| 255 | | Contrato | Endereço | |
| 256 | |---|---| |
| 257 | | AirlockAdmin | [`0x28e7aAC40560804F480e454bF9C12F50C83b700B`](https://sepolia.etherscan.io/address/0x28e7aAC40560804F480e454bF9C12F50C83b700B) | |
| 258 | | AirlockReceiver | [`0xE40D463116F4Aa24d5E79776a65ED0f1b088c670`](https://sepolia.etherscan.io/address/0xE40D463116F4Aa24d5E79776a65ED0f1b088c670) | |
| 259 | | Proxy DemoBTC (dBTC) | [`0x88Cc569265faDAEAc2a16B69f35E10B2158e4b71`](https://sepolia.etherscan.io/address/0x88Cc569265faDAEAc2a16B69f35E10B2158e4b71) | |
| 260 | | ProxyAdmin | [`0x51a3fE6AD22BF7753574B7b7FD56aE9a65bd593E`](https://sepolia.etherscan.io/address/0x51a3fE6AD22BF7753574B7b7FD56aE9a65bd593E) | |
| 261 | | DemoBTC v1 | [`0x8f7d6453d77B3f3b008CBE4A144d82c5742eB108`](https://sepolia.etherscan.io/address/0x8f7d6453d77B3f3b008CBE4A144d82c5742eB108) | |
| 262 | | DemoBTC v2 (upgrade honesto) | [`0x4D959f981C01E3B81ac6C9A20E84816Fe5f6Bce3`](https://sepolia.etherscan.io/address/0x4D959f981C01E3B81ac6C9A20E84816Fe5f6Bce3) | |
| 263 | | EvilMintImpl | [`0x01502c4af90c27f53B2E1293941670C6B761CAAA`](https://sepolia.etherscan.io/address/0x01502c4af90c27f53B2E1293941670C6B761CAAA) | |
| 264 | | EvilOwnerImpl | [`0xBe849DE8e0a4D9b0010ba1468f94Eeb24A42d488`](https://sepolia.etherscan.io/address/0xBe849DE8e0a4D9b0010ba1468f94Eeb24A42d488) | |
| 265 | | EvilFeeImpl | [`0xB95Ffa7b970B1a73488e2fC32B787fA536F12AF5`](https://sepolia.etherscan.io/address/0xB95Ffa7b970B1a73488e2fC32B787fA536F12AF5) | |
| 266 | | EvilBaseFeeImpl | [`0xE1AAa4E88DB9cc7ceCca14722eE2874D834e0310`](https://sepolia.etherscan.io/address/0xE1AAa4E88DB9cc7ceCca14722eE2874D834e0310) | |
| 267 | | EvilDrainImpl | [`0x1d98D64BC5204AE139B5A0747855841Fa5333b6d`](https://sepolia.etherscan.io/address/0x1d98D64BC5204AE139B5A0747855841Fa5333b6d) | |
| 268 | | PausedImpl (alvo do break-glass) | [`0xb6573006fB607E96966d0F578B96AAB25413F5BE`](https://sepolia.etherscan.io/address/0xb6573006fB607E96966d0F578B96AAB25413F5BE) | |
| 269 | | Chainlink MockKeystoneForwarder | [`0x15fC6ae953E024d975e77382eEeC56A9101f9F88`](https://sepolia.etherscan.io/address/0x15fC6ae953E024d975e77382eEeC56A9101f9F88) | |
| 270 | |
| 271 | A primeira execução: sete propostas, sete vereditos on-chain (as execuções seguintes, #8–#12, estão listadas em |
| 272 | [docs/evidence/sepolia-live.md](docs/evidence/sepolia-live.md)). O bloco em cada link é o bloco em que a transação foi incluída; `(+n)` indica |
| 273 | quantos blocos após a proposta o veredito do CRE chegou. |
| 274 | |
| 275 | | Cartão | Proposta | Veredito do CRE | Tx da proposta | Tx do relatório | Depois | |
| 276 | |---|---|---|---|---|---| |
| 277 | | `v2`: upgrade honesto | #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" | |
| 278 | | `evil-mint`: qualquer um pode cunhar | #1 | Veto R1 | [11950735](https://sepolia.etherscan.io/tx/0x862c5fdf826a8fef09fe2ff278d3e58191f93c267dbdd38ad9aaa270f39937b0) | [11950736 (+1)](https://sepolia.etherscan.io/tx/0xb0d74e25e2528fb9aad7d21de202dac288cd256048f2986b5214280fda4b84f6) | `execute` reverte com `ProposalVetoed` | |
| 279 | | `evil-owner`: qualquer um pode tomar a propriedade | #6 | Veto R2 | [11950788](https://sepolia.etherscan.io/tx/0x32eb22039bd43602e0406b1a8f9a02c5cbcdb5cf2ef1c389919a45d05820da25) | [11950790 (+2)](https://sepolia.etherscan.io/tx/0x49451bee7a2f3351b18992d94b5f89b9ff89cf53be65a937400ab95fb79be551) | `execute` reverte com `ProposalVetoed` | |
| 280 | | `evil-fee`: transferências desviam 10% | #7 | Veto R3 | [11950792](https://sepolia.etherscan.io/tx/0xe06924c2e3961ab27d2803c0654b783fc06e52107345bda9bfeff391053fcad3) | [11950794 (+2)](https://sepolia.etherscan.io/tx/0x01f6055a683e0dabc8f34ae094e5daa5a81ddb11c687aa9c5ac6e30c7a7f3d3b) | `execute` reverte com `ProposalVetoed` | |
| 281 | | `evil-basefee`: o mint é liberado em um bloco real | #3 | Veto R9 | [11950742](https://sepolia.etherscan.io/tx/0x3c29c118ca395ca92500abcaae6126244924fbd8e14f846af57b70f113cad6e1) | [11950743 (+1)](https://sepolia.etherscan.io/tx/0xb6db2d832112384287e5a7ccdd086127931360900d09c80645c21eac2bc165db) | `execute` reverte com `ProposalVetoed` | |
| 282 | | `evil-drain`: qualquer um move os tokens de um detentor | #2 | Veto R3B | [11950737](https://sepolia.etherscan.io/tx/0x0e52369921040eb0c901ac3a077aff762d370b4efd9503a52c91360d56366b16) | [11950740 (+3)](https://sepolia.etherscan.io/tx/0x40bbe374a8cee47a94da977f973cf66a67573c12bc84117483fcbd11430f90cb) | `execute` reverte com `ProposalVetoed` | |
| 283 | | `remove-receiver`: remove o CRE como guardião | #5 | Veto R7 | [11950749](https://sepolia.etherscan.io/tx/0xda2bd1c9dc5f7770da3926358b8da3ffe5a1cd3154b499cafae440387eebb934) | [11950750 (+1)](https://sepolia.etherscan.io/tx/0xaa2f598bdcc7f46727e5456528521e50ac5c1d8e423805042cbf2d10a4c4cdac) | `execute` reverte com `ProposalVetoed` | |
| 284 | |
| 285 | Todos os vereditos acima vieram da CRE CLI oficial executando este workflow em um servidor (o listener do log trigger |
| 286 | mais o varredor de 60 segundos, [`scripts/sepolia/systemd/`](scripts/sepolia/systemd)); as linhas de log do próprio |
| 287 | workflow estão em [`docs/evidence/sepolia-live.md`](docs/evidence/sepolia-live.md). |
| 288 | |
| 289 | ## <a id="quick-start"></a>Início rápido |
| 290 | |
| 291 | ### Pré-requisitos |
| 292 | |
| 293 | - [Foundry](https://getfoundry.sh) (`forge`, `cast`, `anvil`), `jq` e Node 22. |
| 294 | - Uma conta CRE para o simulador (`cre login`), usada pela execução de ponta a ponta em fork e pelo kit do operador. |
| 295 | - [`cre/setup.sh`](cre/setup.sh) instala a CRE CLI v1.37.0 (com SHA-256 fixado) e o bun 1.4.2 em `cre/.tools`. |
| 296 | |
| 297 | ### Build e testes |
| 298 | |
| 299 | ```bash |
| 300 | git clone https://gitlab.com/jiapunk/upgrade-airlock.git # ou https://upgrade-airlock.pages.dev/source/upgrade-airlock.git |
| 301 | cd upgrade-airlock |
| 302 | |
| 303 | # contratos (OpenZeppelin e forge-std já vêm incluídos em lib/) |
| 304 | forge build # baixa o solc 0.8.37 fixado uma única vez |
| 305 | forge test --offline # 117 testes |
| 306 | |
| 307 | # o workflow do CRE |
| 308 | cre/setup.sh |
| 309 | export PATH="$PWD/cre/.tools/bin:$PWD/cre/.tools/node_modules/.bin:$PATH" |
| 310 | (cd cre/veto && bun test && bun run typecheck) # 118 testes: regras, sondas, bytecode, relatórios, orçamentos, handlers |
| 311 | |
| 312 | # a war room |
| 313 | (cd web && npm ci && npm run selftest) # 394 verificações: visão ao vivo e seletor de ataques, reprodução offline, API do operador |
| 314 | ``` |
| 315 | |
| 316 | ### Ponta a ponta em um fork da Sepolia |
| 317 | |
| 318 | ```bash |
| 319 | cre login # uma vez; o simulador precisa de uma conta CRE |
| 320 | cre/scripts/fork-e2e.sh # um fork local da Sepolia no anvil: nada é enviado a uma rede real |
| 321 | ``` |
| 322 | |
| 323 | [`cre/scripts/fork-e2e.sh`](cre/scripts/fork-e2e.sh) faz o deploy da stack no fork, executa o workflow no simulador do |
| 324 | CRE com `--broadcast` e verifica cada resultado on-chain: |
| 325 | |
| 326 | - **Log trigger:** EvilMint vetado por R1, EvilOwner por R2, EvilFee por R3, EvilBaseFee por R9 (que cita |
| 327 | `BASEFEE`), EvilDrain por R3B (que cita `rescue`, com todas as outras regras passando); mais sete drenos |
| 328 | ([`test/fixtures/DrainVariants.sol`](test/fixtures/DrainVariants.sol)) vetados apenas por R3B: argumentos |
| 329 | reordenados, uma proteção por endereço de token, um dreno aberto só a chamadores que possuem tokens ou ETH, um sem |
| 330 | evento `Transfer` e um `transferFrom` que ignora a allowance para detentores de tokens; a v2 honesta liberada e |
| 331 | executada após o período de espera; a remoção do receiver como guardião vetada por R7; um atraso menor, pré-aprovado |
| 332 | pelo aprovador, liberado. |
| 333 | - **Varredor:** um upgrade que ninguém julgou reverte com `NotCleared` após o período de espera, uma execução do |
| 334 | varredor o libera e ele é executado; duas propostas que o aprovador liberou manualmente são julgadas na execução |
| 335 | seguinte: EvilDrain vetado por R3B, prevalecendo sobre a liberação, e um atraso maior liberado também pelo CRE; um |
| 336 | upgrade com 6 KB de dados de inicialização é vetado por R8 na execução seguinte. |
| 337 | - **Evidência:** toda proposta vetada reverte com `ProposalVetoed`, e o hash de evidência de cada veredito é |
| 338 | recalculado a partir dos dados on-chain ([`cre/veto/tools/check-evidence.ts`](cre/veto/tools/check-evidence.ts)). |
| 339 | |
| 340 | ### Kit do operador da Sepolia |
| 341 | |
| 342 | [`scripts/sepolia/`](scripts/sepolia/README.md) opera a demo ao vivo. As chaves ficam fora do repositório, uma chave |
| 343 | hex por arquivo em `KEY_DIR` (`owner.key`, `attester.key`, `sender.key`, `approver.key`, |
| 344 | `emergency.key`, mais um `sweeper.key` opcional); os scripts nunca as imprimem, |
| 345 | e `deploy.sh` se recusa a rodar a menos que owner, aprovador, guardião de emergência e attester sejam quatro chaves |
| 346 | diferentes. Todo script também roda contra um fork local com `FORK=<port>`, então a demo pode ser ensaiada sem gastar |
| 347 | nada. |
| 348 | |
| 349 | ```bash |
| 350 | scripts/sepolia/deploy.sh # 13 transações, grava deployments/sepolia.json (FORCE=1 substitui um existente) |
| 351 | bun scripts/sepolia/make-config.mjs # a configuração do workflow, 4 provedores, quórum 2 |
| 352 | scripts/sepolia/fund-sender.sh 0.05 # ETH para as transações de relatório |
| 353 | scripts/sepolia/airlock.sh # terminal A: o listener do CRE (log trigger) |
| 354 | scripts/sepolia/propose.sh v2 # terminal B: depois evil-mint, evil-owner, evil-fee, evil-basefee, evil-drain, remove-receiver |
| 355 | scripts/sepolia/execute.sh <id> # executa uma proposta liberada após o período de espera; caso contrário, mostra o revert |
| 356 | scripts/sepolia/status.sh # todas as propostas: veredito, julgada ou não pelo CRE, regra, evidência, txs |
| 357 | ``` |
| 358 | |
| 359 | `sweep.sh` executa o varredor cron uma vez, `clear.sh` é a liberação manual do aprovador, `fork.sh` inicia, salva e |
| 360 | restaura um fork da Sepolia, `probe-rpcs.ts` verifica cada provedor com o próprio código de julgamento do workflow, e |
| 361 | `systemd/` roda o listener e o varredor em um servidor Linux. |
| 362 | |
| 363 | ### War room |
| 364 | |
| 365 | ```bash |
| 366 | cd web |
| 367 | npm ci |
| 368 | npm run dev # http://127.0.0.1:5173/ |
| 369 | npm run build:airlock # sincroniza deployments/sepolia.json com a página e gera web/dist-airlock |
| 370 | ``` |
| 371 | |
| 372 | A página lê a Sepolia diretamente; não guarda nenhuma chave e nunca assina nem envia nada. Ela mostra cada proposta |
| 373 | como uma câmara de descompressão: o período de espera, a liberação do CRE, o veto com sua regra e evidência. Os |
| 374 | visitantes escolhem um de sete cartões (seis ataques e o upgrade honesto v2) e assistem a uma reprodução identificada |
| 375 | da proposta real mais recente desse tipo na Sepolia (ou à proposta ao vivo enquanto ela estiver no período de espera): |
| 376 | veja [o seletor de ataques](web/README.md#attack-picker-what-a-visitor-sees). Para quem apresenta há um modo operador |
| 377 | (`web/operator/`, `web/functions/`): só via loopback no servidor de desenvolvimento e com token no Cloudflare Pages, |
| 378 | onde fica desligado a menos que existam os dois secrets `OWNER_KEY` e `OPERATOR_TOKEN`. Ele envia uma das mesmas sete |
| 379 | propostas fixas por `POST /api/operator/propose`, assinada no servidor com a chave do owner (o design já assume que |
| 380 | essa chave vaza: ela só pode propor), e a página de um visitante nunca o chama: veja [o modo |
| 381 | operador](web/README.md#operator-mode-real-attacks-on-sepolia). |
| 382 | |
| 383 | | Parâmetro de URL | Efeito | |
| 384 | |---|---| |
| 385 | | `?mode=airlock` | Padrão. Visão ao vivo da Ethereum Sepolia, somente leitura | |
| 386 | | `?mode=offline` | Reproduz uma execução gravada, sem nenhuma conexão de rede | |
| 387 | | `?lang=en`, `?lang=zh-TW`, `?lang=zh-CN` | Idioma da interface (padrão `zh-CN`; `L` alterna entre eles) | |
| 388 | | `?rpc=http://127.0.0.1:<port>` | Lê um fork local da Sepolia no lugar da rede, para ensaios | |
| 389 | | `?op=1` | Modo operador: pede o token do operador (só para quem apresenta) | |
| 390 | |
| 391 | Pressione `P` no modo ao vivo para reproduzir as propostas mais recentes a partir de seus eventos on-chain, e `1` a |
| 392 | `7` para reproduzir um cartão do seletor. |
| 393 | |
| 394 | ## Estrutura do repositório |
| 395 | |
| 396 | ``` |
| 397 | src/AirlockAdmin.sol as duas portas: propostas, período de espera, veto de guardião, liberação, execute, breakGlass |
| 398 | src/cre/AirlockReceiver.sol receiver de relatórios do CRE: forwarder, metadados do workflow, atestação EIP-712, vínculo ao digest |
| 399 | src/cre/IReceiver.sol interface de receiver do Chainlink CRE |
| 400 | src/DemoBTC.sol, DemoBTCV2.sol token de demonstração (dBTC) v1 e a v2 honesta |
| 401 | src/PausedImpl.sol alvo do break-glass: toda alteração de saldo reverte |
| 402 | src/attack/ EvilMintImpl, EvilOwnerImpl, EvilFeeImpl, EvilBaseFeeImpl, EvilDrainImpl |
| 403 | script/DeployEth.s.sol deploy (13 transações) com verificações de papéis, do forwarder e da configuração pós-deploy |
| 404 | test/ testes Foundry; fixtures/DrainVariants.sol contém as sete variantes de dreno |
| 405 | cre/veto/ o workflow do CRE: workflow.ts (capabilities), airlock.ts (handlers), judge.ts (regras, |
| 406 | quórum), simulate.ts (eth_simulateV1), bytecode.ts (seletores, R9), report.ts |
| 407 | (relatório, EIP-712, evidência), chain.ts (orçamento de leituras), keys.ts, rules.ts, config.ts |
| 408 | cre/veto/test/ testes unitários em Bun |
| 409 | cre/veto/tools/ check-evidence.ts recalcula cada hash de evidência on-chain |
| 410 | cre/scripts/fork-e2e.sh ponta a ponta em um fork da Sepolia no anvil |
| 411 | cre/setup.sh CRE CLI e bun com versões fixadas |
| 412 | scripts/sepolia/ kit do operador da demo ao vivo, com units systemd para um servidor |
| 413 | web/ war room (Vite, TypeScript, viem) |
| 414 | web/operator/, web/functions/ modo operador: sete propostas fixas assinadas no servidor (servidor de dev, Pages Functions) |
| 415 | deployments/sepolia.json o deploy ao vivo |
| 416 | docs/design.md modelo de ameaças, máquina de estados da liberação, vinculação do relatório, orçamentos |
| 417 | docs/evidence/ a execução ao vivo registrada |
| 418 | .github/workflows/ci.yml verificações dos contratos, do workflow e da war room |
| 419 | ``` |
| 420 | |
| 421 | ## <a id="security-model"></a>Modelo de segurança |
| 422 | |
| 423 | O Upgrade Airlock parte do princípio de que a chave do owner vaza, e foi construído para que a chave do owner sozinha |
| 424 | nunca consiga executar um upgrade. |
| 425 | |
| 426 | | Papel | Mantido por | Pode | |
| 427 | |---|---|---| |
| 428 | | Owner | uma chave | propor upgrades e mudanças no airlock (guardiões, atraso, alvo do break-glass); `breakGlass` | |
| 429 | | Aprovador | uma chave separada | liberar manualmente quando o CRE não consegue responder; aprovar codehashes, dados de inicialização e mudanças no airlock (`approveCodehash`) | |
| 430 | | Guardião de emergência | uma chave separada | `veto` de uma proposta pendente; `breakGlass` | |
| 431 | | Attester | um secret do CRE, com chave própria | assinar os vereditos EIP-712 que o `AirlockReceiver` aceita | |
| 432 | | AirlockReceiver | o receiver de relatórios do CRE | oráculo de liberação (`clear`) e guardião (`veto`, `attachReason`) | |
| 433 | | Qualquer pessoa | | `execute` de uma proposta que passou pelas duas portas | |
| 434 | |
| 435 | Os papéis nunca se sobrepõem. O construtor do `AirlockAdmin` recusa um owner que também seja o aprovador, um guardião |
| 436 | ou o oráculo de liberação (`RoleOverlap`), e o `DeployEth` só faz o deploy quando owner, aprovador, guardião de |
| 437 | emergência e attester são quatro endereços diferentes. |
| 438 | |
| 439 | **Uma chave de owner vazada pode propor.** Toda proposta cumpre o período de espera à vista de todos enquanto o CRE a |
| 440 | julga, e precisa de uma liberação que o owner não pode conceder a si mesmo. O owner também pode puxar o freio de |
| 441 | emergência (`breakGlass`), que apenas troca o proxy para a implementação pausada pré-registrada; sair dela é um |
| 442 | upgrade pelas duas portas, e um upgrade liberado antes e nunca executado também pode encerrá-la. |
| 443 | |
| 444 | **A mesma chave não pode:** |
| 445 | |
| 446 | - executar sem liberação: o `AirlockAdmin` não tem um caminho de chamada genérico, e a única forma de trocar a |
| 447 | implementação é o `execute` de um upgrade liberado (ou o `breakGlass` para a implementação pausada); |
| 448 | - enfraquecer o airlock antes: remover o receiver do CRE, encurtar o atraso ou trocar o alvo do break-glass também |
| 449 | são propostas, e a R7 as veta sem a pré-aprovação do aprovador; |
| 450 | - trocar o código depois do veredito: `execute` reverifica o codehash do alvo (`CodehashChanged`), e contas delegadas |
| 451 | via EIP-7702 são recusadas (`DelegatedAccount`); |
| 452 | - forjar ou reutilizar um veredito: o `AirlockReceiver` só aceita um relatório vindo do forwarder configurado, |
| 453 | assinado pelo attester, até o seu `validUntilBlock` e vinculado ao `proposalDigest` da proposta nesta chain e neste |
| 454 | receiver. |
| 455 | |
| 456 | A liberação manual do aprovador é um override para quando o CRE não consegue responder: o varredor ainda julga essa |
| 457 | proposta enquanto ela estiver entre os 20 ids mais recentes, e um veto do CRE ou de um guardião prevalece. O modelo de |
| 458 | ameaças completo está em [docs/design.md](docs/design.md#threat-model). |
| 459 | |
| 460 | Estes são contratos de demonstração para testnet com limitações conhecidas, cada uma fixada por um teste que passa: as |
| 461 | liberações nunca expiram, a chave de owner não pode ser trocada (uma chave roubada pode pausar o token de novo após |
| 462 | cada retomada) e um único guardião desonesto pode mantê-lo pausado. Elas estão listadas em |
| 463 | [docs/design.md](docs/design.md#known-limitations). |
| 464 | |
| 465 | ## Tecnologias |
| 466 | |
| 467 | - **Solidity 0.8.37** e **Foundry**, com OpenZeppelin Contracts 5.7.0 (`ProxyAdmin`, |
| 468 | `TransparentUpgradeableProxy`, `EIP712`, `ECDSA`) |
| 469 | - **Chainlink CRE**: o SDK TypeScript (`@chainlink/cre-sdk` 1.23.0) e a CRE CLI v1.37.0, sobre Bun 1.4.2 |
| 470 | - **viem** para codificação ABI e leituras on-chain, no workflow e na war room |
| 471 | - **Vite** e TypeScript para a war room |
| 472 | |
| 473 | ## Ferramentas de IA |
| 474 | |
| 475 | O Upgrade Airlock foi construído com o [Claude Code](https://claude.com/claude-code) sob a direção do autor: os |
| 476 | contratos, o workflow do CRE, os testes, os scripts, a war room e estes documentos foram escritos e verificados com |
| 477 | ele. |
| 478 | |
| 479 | ## Licença |
| 480 | |
| 481 | [MIT](LICENSE) © 2026 jiapunk |