From 486adaada671624d84f72deebb18472d630257de Mon Sep 17 00:00:00 2001 From: Yuri T Date: Tue, 4 Aug 2026 16:47:44 +0100 Subject: [PATCH 1/3] docs: add new audit reports and LIP-37 --- docs/lips.md | 6 + docs/security/audits.md | 428 ++++++++++++++++++++++++++++++++++++++-- scripts/fetch-audits.js | 20 +- 3 files changed, 442 insertions(+), 12 deletions(-) diff --git a/docs/lips.md b/docs/lips.md index 187bc4ad2..a68c62c2f 100644 --- a/docs/lips.md +++ b/docs/lips.md @@ -12,6 +12,12 @@ More details on the contribution process and LIPs statuses can be found [here](h ## Proposed +| LIP # | Title | Author | Discussions‑to | +|------------|-------|--------|----------------| +| [37](https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-37.md) | "Execution Delegation Framework" | Raman Siamionau, Matsvei Talstalutski | [Link](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746) | + +## Approved + | LIP # | Title | Author | Discussions‑to | |------------|-------|--------|----------------| | [36](https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-36.md) | NEST — Automated LDO Buyback and Liquidity Provisioning System | Vasiliy Shapovalov, Vitaly Galaichuk, Jen Kopytina, Alexander Belokon, adcv | [Link](https://research.lido.fi/t/liquid-buybacks-nest-execution-with-ldo-wsteth-liquidity/10894) | diff --git a/docs/security/audits.md b/docs/security/audits.md index a6cb0bbc0..a4200c7d5 100644 --- a/docs/security/audits.md +++ b/docs/security/audits.md @@ -1,9 +1,174 @@ # Lido Protocol Audits -## Lido on Ethereum (99 reports) +## Lido on Ethereum (112 reports) + +### 08-2026 MixBytes Lido TokenRateNotifier Security Audit + +Audited at commit [`dc9066b`](https://github.com/lidofinance/core/tree/dc9066b6f9b5c6c3b65f4a4aac338427198f0e98). + +- Total Issues: 1 (1 Fixed) +- Critical Issues: 0 +- High Issues: 0 +- Medium Issues: 0 +- Low Issues: 1 (1 Fixed) + +See [full report](https://github.com/lidofinance/audits/blob/main/MixBytes%20Lido%20TokenRateNotifier%20Security%20Audit%20Report%2008-2026.pdf) for more details. + +### 07-2026 Composable Security Lido Oracle v8.0.2 Security Consultation + +Audited at commit [`0fb275b7`](https://github.com/lidofinance/lido-oracle/tree/0fb275b7a3030ffed36a64182b3d91cd2de6bfc6). + +A security consultation for a hotfix to the Lido Oracle V8, correcting the calculation of deposits injected between the nearest/distant slot and a report's reference slot, and adding a check that these slots fall within the same frame. Previous [report for V8](#06-2026-composable-security-lido-oracle-v8-security-audit). + +See [full report](https://github.com/lidofinance/audits/blob/main/Composable%20Security%20Lido%20Oracle%20V8_0_2%20Security%20Consultation%20Report.pdf) for more details. + +### 07-2026 Composable Security Lido Oracle v8.0.3 Security Consultation + +Audited at commit [`1d0b9440`](https://github.com/lidofinance/lido-oracle/tree/1d0b9440fda2dd6d421a1756fa5604269f972a6d). + +A security consultation for a hotfix to the Lido Oracle V8, fixing an over-ejection on exact withdrawal coverage, an incorrect sweep calculation for validators to eject, invalid indexing of MetaRegistry groups, and non-deterministic tree construction. Previous [report for V8](#06-2026-composable-security-lido-oracle-v8-security-audit). + +See [full report](https://github.com/lidofinance/audits/blob/main/Composable%20Security%20Lido%20Oracle%20V8_0_3%20Security%20Consultation%20Report.pdf) for more details. + +### 07-2026 MixBytes Off-chain Audit of Lido Oracle v8.0.5 + +Audited at commit [`61231c3`](https://github.com/lidofinance/lido-oracle/tree/61231c3d21587a1d1cddf7cb7d3b0a29b1ebfd69). + +An interim security review of the Lido Oracle v8.0.5 change set, which adds reconciliation checks between the Keys API, the Consensus Layer and the Staking Router in the Accounting Oracle path, and fixes the exit ordering used by the Validator Exit Bus Oracle. Previous [report for V8](#06-2026-composable-security-lido-oracle-v8-security-audit). + +- Total Issues: 7 (1 Fixed, 6 Acknowledged) +- Critical Issues: 0 +- High Issues: 0 +- Medium Issues: 2 (2 Acknowledged) +- Low Issues: 5 (1 Fixed, 4 Acknowledged) + +See [full report](https://github.com/lidofinance/audits/blob/main/MixBytes%20Lido%20Oracle%20v8.0.5%20Security%20Audit%20Report%2007-2026.pdf) for more details. + +### 07-2026 Composable Security Lido Oracle v8.0.5 Security Consultation + +Audited at commit [`2b29cfdc`](https://github.com/lidofinance/lido-oracle/tree/2b29cfdc7e3c8678fda7fa63e3ec78f4aa370f70). + +A security consultation for the Lido Oracle V8.0.5, covering the handling of operator groups in which all internal Curated Module v2 node operators have zero weight, the additional Keys API consistency checks, the handling of critical key-count discrepancies and possible validator deposit front-running, and the diagnostic logging for pending deposits using Lido withdrawal credentials. Previous [report for V8](#06-2026-composable-security-lido-oracle-v8-security-audit). + +See [full report](https://github.com/lidofinance/audits/blob/main/Composable%20Security%20Lido%20Oracle%20V8_0_5%20Security%20Consultation%20Report.pdf) for more details. + +### 07-2026 Certora Staking Router v3 Audit + +Audited at commit [`af095e4`](https://github.com/lidofinance/core/tree/af095e48bbc1c3841c2c9936219c8461af01056b). + +An audit of the [Staking Router v3](https://research.lido.fi/t/staking-router-v3-design-implementation-proposal-lip-35/11621). + +- Total Issues: 23 (15 Fixed, 8 Acknowledged) +- Critical Issues: 1 (1 Fixed) +- High Issues: 0 +- Medium Issues: 5 (5 Fixed) +- Low Issues: 7 (2 Fixed, 5 Acknowledged) +- Info Issues: 10 (7 Fixed, 3 Acknowledged) + +See [full report](https://github.com/lidofinance/audits/blob/main/Certora%20Staking%20Router%20v3%20Audit%20Report%2007-2026.pdf) for more details. + +### 07-2026 Statemind Staking Router v3 Audit + +Audited at commit [`af095e4`](https://github.com/lidofinance/core/tree/af095e48bbc1c3841c2c9936219c8461af01056b). + +An audit of the [Staking Router v3](https://research.lido.fi/t/staking-router-v3-design-implementation-proposal-lip-35/11621). + +- Total Issues: 45 (33 Fixed, 12 Acknowledged) +- Critical Issues: 0 +- High Issues: 0 +- Medium Issues: 0 +- Info Issues: 45 (33 Fixed, 12 Acknowledged) + +See [full report](https://github.com/lidofinance/audits/blob/main/Statemind%20Staking%20Router%20v3%20Audit%20Report%2007-2026.pdf) for more details. + +### 07-2026 MixBytes Staking Router v3 Upgrade Audit + +Audited at commit [`4c889ca`](https://github.com/lidofinance/core/tree/4c889ca95f66a8d57fb8d1eb2b83d251e51f8718). + +An audit of the [Staking Router v3](https://research.lido.fi/t/staking-router-v3-design-implementation-proposal-lip-35/11621) upgrade (voting contracts). + +- Total Issues: 11 (4 Fixed, 7 Acknowledged) +- Critical Issues: 0 +- High Issues: 0 +- Medium Issues: 1 (1 Acknowledged) +- Low Issues: 10 (4 Fixed, 6 Acknowledged) + +See [full report](https://github.com/lidofinance/audits/blob/main/MixBytes%20Staking%20Router%20v3%20Upgrade%20Audit%20Report%2007-2026.pdf) for more details. + +### 07-2026 MixBytes Lido Easy Track Factories (SRv3, CSMv3, CMv2) Security Audit + +Audited at commit [`b5d7455`](https://github.com/lidofinance/easy-track/tree/b5d74557448a7f65ea74abdc09ff954bd113c8c3). + +An audit of Easy Track factories supporting the Staking Router v3, Community Staking Module v3, and Curated Module v2 releases. + +- Total Issues: 10 (6 Fixed, 4 Acknowledged) +- Critical Issues: 0 +- High Issues: 0 +- Medium Issues: 1 (1 Fixed) +- Low Issues: 9 (5 Fixed, 4 Acknowledged) + +See [full report](https://github.com/lidofinance/audits/blob/main/MixBytes%20Lido%20Easy%20Track%20Factories%20%28SRv3%20CSMv3%20CMv2%29%20Security%20Audit%20Report%2007-2026.pdf) for more details. + +### 07-2026 Ack3 Lido NEST Audit + +Audited at commit [`10a2499`](https://github.com/lidofinance/stonks/tree/10a249971099be1b36c27f43a3f0a520389fb62e) for the [stonks repository](https://github.com/lidofinance/stonks) and commit [`dc9066b`](https://github.com/lidofinance/core/tree/dc9066b6f9b5c6c3b65f4a4aac338427198f0e98) for the [core repository](https://github.com/lidofinance/core). + +- Total Issues: 16 (8 Fixed, 8 Acknowledged) +- Critical Issues: 0 +- High Issues: 0 +- Medium Issues: 0 +- Low Issues: 9 (5 Fixed, 4 Acknowledged) +- Warnings: 3 (3 Acknowledged) +- Info Issues: 4 (3 Fixed, 1 Acknowledged) + +See [full report](https://github.com/lidofinance/audits/blob/main/Ack3%20Lido%20NEST%20Audit%20Report%2007-2026.pdf) for more details. + +### 06-2026 Composable Security Lido Oracle v8 Security Audit + +Audited at commit [`cdd0767`](https://github.com/lidofinance/lido-oracle/tree/cdd076793cd371a07e9f95f7843f1e9f36574f18). + +- Total Issues: 21 (14 Fixed, 7 Acknowledged) +- Critical Issues: 0 +- High Issues: 1 (1 Fixed) +- Medium Issues: 9 (8 Fixed, 1 Acknowledged) +- Low Issues: 6 (2 Fixed, 4 Acknowledged) +- Info Issues: 5 (3 Fixed, 2 Acknowledged) + +See [full report](https://github.com/lidofinance/audits/blob/main/Composable%20Security%20Lido%20Oracle%20V8%20Audit%20Report.pdf) for more details. + +### 06-2026 Certora Lido CSM v3 and CM v2 Audit + +Audited at commit [`4d3de66`](https://github.com/lidofinance/community-staking-module/tree/4d3de6658499e1c1774951780a97d7ae25ca18b8). + +An audit of the Community Staking Module v3 and Curated Module v2. + +- Total Issues: 12 (6 Fixed, 6 Acknowledged) +- Critical Issues: 0 +- High Issues: 0 +- Medium Issues: 0 +- Low Issues: 5 (1 Fixed, 4 Acknowledged) +- Info Issues: 7 (5 Fixed, 2 Acknowledged) + +See [full report](https://github.com/lidofinance/audits/blob/main/Certora%20Lido%20CSM%20v3%20and%20CM%20v2%20Audit%20Report%20-%2006-2026.pdf) for more details. + +### 06-2026 Statemind Lido CSM v3 and CM v2 Audit + +Audited at commit [`4d3de66`](https://github.com/lidofinance/community-staking-module/tree/4d3de6658499e1c1774951780a97d7ae25ca18b8). + +An audit of the Community Staking Module v3 and Curated Module v2. + +- Total Issues: 20 (14 Fixed, 6 Acknowledged) +- Critical Issues: 0 +- High Issues: 0 +- Medium Issues: 0 +- Info Issues: 20 (14 Fixed, 6 Acknowledged) + +See [full report](https://github.com/lidofinance/audits/blob/main/Statemind%20Lido%20CSM%20v3%20and%20CM%20v2%20Audit%20Report%2006-2026.pdf) for more details. ### 04-2026 Cyfrin Lido CircuitBreaker Security Audit and Formal Verification +Audited at commit [`b4b2fbc`](https://github.com/lidofinance/circuit-breaker/tree/b4b2fbc921b3191560a3fc62d502d4bb98ad99e1). + An audit and Certora Prover formal verification of the CircuitBreaker emergency pause manager. - Total Issues: 5 (3 Resolved, 2 Acknowledged) @@ -22,6 +187,8 @@ See [formal verification report](https://github.com/lidofinance/audits/blob/main ### 04-2026 MixBytes Lido CircuitBreaker Security Audit +Audited at commit [`b4b2fbc`](https://github.com/lidofinance/circuit-breaker/tree/b4b2fbc921b3191560a3fc62d502d4bb98ad99e1). + An audit of the CircuitBreaker emergency pause manager. - Total Issues: 1 (1 Fixed) @@ -34,6 +201,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/MixBytes%20Cir ### 03-2026 Composable Security Lido Oracle v7.1 Security Audit +Audited at commit [`b2e9296`](https://github.com/lidofinance/lido-oracle/tree/b2e92969fd35b2a1838667a290ded649ca33fbf5). + - Total Issues: 1 (1 Fixed) - Info Issues: 1 (1 Fixed) @@ -41,6 +210,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Composable%20S ### 03-2026 MixBytes Lido DeFi Wrapper MellowStrategyAdapter Security Audit Report 03-2026 +Audited at commit [`37b0999`](https://github.com/lidofinance/vaults-wrapper/tree/37b09995b8695becbaa6bcdbcfcdbc3838b8c926). + - Total Issues: 9 (7 Fixed, 2 Acknowledged) - Critical Issues: 0 - High Issues: 0 @@ -51,6 +222,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/MixBytes%20Lid ### 03-2026 MixBytes Triggerable Withdrawals Easy Track Security Audit Report +Audited at commit [`bdd0483`](https://github.com/lidofinance/easy-track/tree/bdd048352fe6719b0e1f998bffdc05c6607688e0). + An updated report for [the previously audited Triggerable Withdrawals Easy Tracks](#09-2025-mixbytes-lido-triggerable-withdrawals-easy-track-security-audit). The update includes mitigations for a vulnerability that allowed unauthorized access to the withdrawal process by duplicating keys not owned by the Node Operator. @@ -60,6 +233,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/MixBytes%20Lid ### 03-2026 Certora Lido V3 Security Assessment Fix Review +Audited at commit [`702ef9d`](https://github.com/lidofinance/core/tree/702ef9da1bc067a996614de1e3c6a53b797fa4c2). + A fix review for [the previously audited Lido V3 contracts](#12-2025-certora-lido-v3-security-audit). The review covered fixes to VaultHub's partial withdrawal prohibition for unhealthy vaults and related components. @@ -74,6 +249,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Certora%20Lido ### 03-2026 MixBytes Lido V3 Security Audit +Audited at commit [`702ef9d`](https://github.com/lidofinance/core/tree/702ef9da1bc067a996614de1e3c6a53b797fa4c2). + An updated report for [the previously audited Lido V3 contracts](#12-2025-mixbytes-lido-v3-security-audit). The review covered fixes to LazyOracle's sanity checks and VaultHub's partial withdrawal handling for vaults with obligations shortfall. @@ -83,6 +260,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/MixBytes%20Lid ### 03-2026 MixBytes Lido EasyTrack stVaults Security Audit +Audited at commit [`1330bcf`](https://github.com/lidofinance/easy-track/tree/1330bcff9dec5d0c800af9b7f8347268e6743181). + An updated report for [the previously audited Lido V3 Easy Track contracts](#12-2025-mixbytes-lido-v3-easy-track-security-audit). The review covered changes to tier shareLimit validation in OperatorGrid EVMScript factories, decoupling it from the on-chain group shareLimit in favor of a hardcoded constant. @@ -92,6 +271,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/MixBytes%20Lid ### 01-2026 Sigma Prime Lido BLS Library Security Audit +Audited at commit [`33f2f59`](https://github.com/lidofinance/core/tree/33f2f59156697aae93cdea2d0984de7be347e3af). + - Total Issues: 6 (6 Fixed) - Critical Issues: 0 - High Issues: 0 @@ -103,6 +284,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Sigma%20Prime% ### 01-2026 MixBytes CSM Performance Oracle Security Audit +Audited at commit [`2981a28`](https://github.com/lidofinance/community-staking-module/tree/2981a28d5573266abe98097f44f9cdc086944c4b). + - Total Issues: 1 (1 Acknowledged) - Critical Issues: 0 - High Issues: 0 @@ -113,6 +296,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/MixBytes%20CSM ### 01-2026 MixBytes Lido DeFi Wrapper Security Audit Report +Audited at commit [`f35fe13`](https://github.com/lidofinance/vaults-wrapper/tree/f35fe13ddca2084d7b848172b2698feb4fa88025). + - Total Issues: 24 (14 Fixed, 10 Acknowledged) - Critical Issues: 0 - High Issues: 0 @@ -123,6 +308,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/MixBytes%20Lid ### 01-2026 Ackee Blockchain Vault Wrapper Report +Audited at commit [`f35fe13`](https://github.com/lidofinance/vaults-wrapper/tree/f35fe13ddca2084d7b848172b2698feb4fa88025). + - Total Issues: 14 (13 Fixed, 1 Acknowledged) - Critical Issues: 0 - High Issues: 0 @@ -135,6 +322,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Ackee%20Blockc ### 12-2025 Certora Lido V3 Security Audit +Audited at commit [`33f2f59`](https://github.com/lidofinance/core/tree/33f2f59156697aae93cdea2d0984de7be347e3af). + - Total Issues: 84 (70 Fixed, 14 Acknowledged) - Critical Issues: 7 (7 Fixed) - High Issues: 14 (14 Fixed) @@ -146,6 +335,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Certora%20Lido ### 12-2025 Certora Lido V3 Formal Verification +Audited at commit [`b983714`](https://github.com/lidofinance/core/tree/b98371488eb9479cf072bd6c2b682a59c5dd71d8). + - Total Issues: 10 (6 Fixed, 4 Acknowledged) - Critical Issues: 1 (1 Fixed) - High Issues: 0 @@ -157,6 +348,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Certora%20Lido ### 12-2025 Certora Lido V3 Oracle Off-chain Security Assessment +Audited at commit [`39a1c5b`](https://github.com/lidofinance/lido-oracle/tree/39a1c5b8a5e1a90debb2f9eebac6e6588767a473). + - Total Issues: 16 (7 Fixed, 9 Acknowledged) - Critical Issues: 0 - High Issues: 2 (2 Fixed) @@ -168,6 +361,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Certora%20Lido ### 12-2025 MixBytes Lido V3 Security Audit +Audited at commit [`33f2f59`](https://github.com/lidofinance/core/tree/33f2f59156697aae93cdea2d0984de7be347e3af). + - Total Issues: 19 (8 Fixed, 11 Acknowledged) - Critical Issues: 0 - High Issues: 1 (1 Acknowledged) @@ -178,6 +373,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/MixBytes%20Lid ### 12-2025 MixBytes Lido V3 Easy Track Security Audit +Audited at commit [`aa2f545`](https://github.com/lidofinance/easy-track/tree/aa2f5456962dbd8f8599d1450fd8a576b84ade34). + - Total Issues: 4 (2 Fixed, 2 Acknowledged) - Critical Issues: 0 - High Issues: 0 @@ -188,6 +385,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/MixBytes%20Lid ### 12-2025 Consensys Diligence Lido V3 Security Audit +Audited at commit [`33f2f59`](https://github.com/lidofinance/core/tree/33f2f59156697aae93cdea2d0984de7be347e3af). + - Total Issues: 43 (32 Fixed, 11 Acknowledged) - Critical Issues: 2 (2 Fixed) - Major Issues: 5 (5 Fixed) @@ -199,6 +398,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Consensys%20Di ### 12-2025 Ackee Blockchain Stonks 2.0 Audit +Audited at commit [`0669c4a`](https://github.com/lidofinance/stonks/tree/0669c4a50660912785e18861d9ef8108a86a3552). + - Total Issues: 17 (17 Fixed) - Critical Issues: 0 - High Issues: 0 @@ -211,6 +412,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Ackee%20Blockc ### 12-2025 MixBytes Lido LDO Revesting Security Audit Report +Audited at commit [`6d7e4cb`](https://github.com/lidofinance/ldo-revesting/tree/6d7e4cb9e971dc4006927ac9ba5447254f9db7a8). + - Total Issues: 1 (1 Acknowledged) - Critical Issues: 0 - High Issues: 0 @@ -221,6 +424,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Lido%20LDO%20R ### 12-2025 Composable Security Lido Oracle v7 Security Audit +Audited at commit [`39a1c5b`](https://github.com/lidofinance/lido-oracle/tree/39a1c5b8a5e1a90debb2f9eebac6e6588767a473). + - Total Issues: 6 (4 Fixed, 2 Acknowledged) - Critical Issues: 0 - High Issues: 1 (1 Fixed) @@ -232,6 +437,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Composable%20S ### 09-2025 MixBytes Lido Triggerable Withdrawals Easy Track Security Audit +Audited at commit [`a4bbe78`](https://github.com/lidofinance/easy-track/tree/a4bbe78934bdec534c4b78871f0bdce57467eab1). + - Total Issues: 3 - Low Issues: 3 (3 Fixed) @@ -239,12 +446,16 @@ See [full report](https://github.com/lidofinance/audits/blob/main/MixBytes%20Lid ### 09-2025 MixBytes WstETH Staker Security Audit +Audited at commit [`97e0f16`](https://github.com/lidofinance/si-lidity/tree/97e0f166baf638e082a2dcf72fe2ec3ba73e5ddb). + - Total Issues: 0 See [full report](https://github.com/lidofinance/audits/blob/main/MixBytes%20WstETH%20Staker%20Security%20Audit%20Report%2009-2025.pdf) for more details. ### 09-2025 Ackee Blockchain Lido Triggerable Withdrawals Security Audit +Audited at commit [`acf3188`](https://github.com/lidofinance/core/tree/acf3188c79e5616ef7594999f606473214e10f6b). + - Total Issues: 11 (9 Fixed, 1 Partially fixed, 1 Acknowledged) - Low Issues: 2 (2 Fixed) - Warning Issues: 4 (3 Fixed, 1 Acknowledged) @@ -254,6 +465,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Ackee%20Blockc ### 09-2025 Composable Security Lido Oracle v6 Security Audit +Audited at commit [`ec19c76`](https://github.com/lidofinance/lido-oracle/tree/ec19c76b5511ec48f0cfa51b9f216240e9373471). + - Total Issues: 4 (2 Fixed, 2 Acknowledged) - Low Issues: 2 (2 Acknowledged) - Info Issues: 2 (2 Fixed) @@ -266,12 +479,16 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Composable%20S ### 09-2025 MixBytes Easy Track CSM v2 Security Audit +Audited at commit [`12cd20d`](https://github.com/lidofinance/easy-track/tree/12cd20d95272a73338a084bb4a40a6fd8c3ec472). + - Total Issues: 0 See [full report](https://github.com/lidofinance/audits/blob/main/MixBytes%20Easy%20Track%20CSM%20v2%20Security%20Audit%20Report%2009-2025.pdf) for more details. ### 09-2025 Ackee Blockchain CSM v2 Security Audit +Audited at commit [`0e4b562`](https://github.com/lidofinance/community-staking-module/tree/0e4b562719cca51070c9cede5e5a8505eca18684). + - Total Issues: 20 (10 Fixed, 10 Acknowledged) - High Issues: 2 (2 Fixed) - Medium Issues: 1 (1 Fixed) @@ -283,6 +500,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Ackee%20Blockc ### 09-2025 Statemind Triggerable Withdrawals and CSM v2 Audit +Audited at commit [`0e4b562`](https://github.com/lidofinance/community-staking-module/tree/0e4b562719cca51070c9cede5e5a8505eca18684). + - Total Issues: 26 (17 Fixed, 9 Acknowledged) - Critical Issues: 0 - High Issues: 0 @@ -293,21 +512,29 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Statemind%20Tr ### 08-2025 Certora Dual Governance v.1.0.1 Hotfix Review +Audited at commit [`0d31f5b`](https://github.com/lidofinance/dual-governance/tree/0d31f5b3dbe0a553887604a2d5755d14033b8e3d). + Lido has engaged Certora to review and verify the correctness and safety of the Dual Governance v.1.0.1 hotfix. See full [report](https://github.com/lidofinance/audits/blob/main/Certora%20Dual%20Governance%20v1.0.1%20Hotfix%20Review%20Report%2008-2025.pdf) for more details. ### 08-2025 Statemind Dual Governance Escrow Fix Review and Deployment Validation +Audited at commit [`8c77d57`](https://github.com/lidofinance/dual-governance/tree/8c77d57a4c415bb69c6873d174be7c30b1103907). + See [note](https://github.com/lidofinance/audits/blob/main/Statemind%20Dual%20Governance%20Escrow%20Fix%20Review%20Report%2008-2025.pdf) contents for more details. ### 08-2025 Composable Security Off-chain Audit of Lido Oracle v5.4.1 +Audited at commit [`f17f089`](https://github.com/lidofinance/lido-oracle/tree/f17f0898cd8c46eefba5da0ad3162dc2f4bcf439). + A security audit for a hotfix to the Lido Oracle V5. Previous [report for V5](#04-2025-composable-security-off-chain-audit-of-lido-oracle-v5). See [full report](https://github.com/lidofinance/audits/blob/main/Composable%20Security%20Lido%20Oracle%20V5_4_1%2008-25.pdf) for more details. ### 08-2025 Code4rena Audit of Lido Community Staking Module +Audited at commit [`d63d123`](https://github.com/lidofinance/community-staking-module/tree/d63d123f24e2ed2fb2f039238e7562a3d61532b2). + - Total Issues: 2 (2 Acknowledged) - Low Issues: 2 (2 Acknowledged) @@ -315,6 +542,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Code4rena%20CS ### 07-2025 MixBytes On-chain Audit of Community Staking Module (LIP-23, LIP-25, LIP-26, LIP-27) +Audited at commit [`d29a0bf`](https://github.com/lidofinance/community-staking-module/tree/d29a0bf4dac8d78550016beb82dbf16431db5ced). + An updated report for [the previously audited Lido Community Staking Module](#10-2024-mixbytes-on-chain-audit-of-community-staking-module-lip-23-lip-25-lip-26) features a re-audit of the revised CS Verifier contract and deployment verification for the redeployed contract. This contract was updated to reflect changes introduced in LIP-27. No additional issues were found. @@ -323,6 +552,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/MixBytes%20Lid ### 07-2025 Nethermind Lido Accounting Zk Oracle Security Review +Audited at commit [`06bb68a`](https://github.com/lidofinance/sp1-lido-accounting-zk/tree/06bb68acdc86b3722ccb4be726f271a7f26a3b59). + - Total Issues: 9 (8 Fixed, 1 Acknowledged) - Critical Issues: 0 - High Issues: 1 (1 Fixed) @@ -335,22 +566,30 @@ See [full report](https://github.com/lidofinance/audits/blob/main/zk/Nethermind_ ### 06-2025 Statemind Dual Governance Deployment and Voting Script Review +Audited at commit [`7480c3c`](https://github.com/lidofinance/dual-governance/tree/7480c3cdc813312c0c36d6faf0f7e6e4148ebfb2). + See [note](https://github.com/lidofinance/audits/blob/main/Statemind%20Dual%20Governance%20Deployment%20and%20Voting%20Script%20Review%20Report%2006-2025.pdf) contents for more details. ### 06-2025 Composable Security Lido Oracle v5.2 Security Consultation +Audited at commit [`701306d`](https://github.com/lidofinance/lido-oracle/tree/701306dfba316ea4b8b3c0714a7297dc9ce6594b). + After conducting a consultation, the security assessment did not identify any vulnerabilities introduced by the introduced version that could directly compromise the security or operational integrity of the Oracle system. See [full report](https://github.com/lidofinance/audits/blob/main/Composable%20Security%20Lido%20Oracle%20V5_2%2006-25.pdf) for more details. ### 05-2025 MixBytes Lido RMC EasyTrack Security Audit +Audited at commit [`96606b6`](https://github.com/lidofinance/easy-track/tree/96606b69371268e5e3db3175bd1004d55c033a31). + - Total Issues: 0 See [full report](https://github.com/lidofinance/audits/blob/main/MixBytes%20Lido%20RMC%20EasyTrack%20Security%20Audit%20Report%2005-2025.pdf) for more details. ### 04-2025 MixBytes Off-chain Audit of Lido Oracle v5 +Audited at commit [`41f3f96`](https://github.com/lidofinance/lido-oracle/tree/41f3f9671ea7e349e048c3ac47264a118c7983a8). + - Total Issues: 6 (5 Fixed, 1 Acknowledged) - Critical Issues: 0 - High Issues: 0 @@ -361,6 +600,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/MixBytes%20Lid ### 04-2025 Composable Security Off-chain Audit of Lido Oracle v5 +Audited at commit [`41f3f96`](https://github.com/lidofinance/lido-oracle/tree/41f3f9671ea7e349e048c3ac47264a118c7983a8). + - Total Issues: 6 (4 Fixed, 2 Acknowledged) - Critical Issues: 0 - High Issues: 0 @@ -372,6 +613,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Composable%20S ### 04-2025 Ackee Blockchain Audit of Community Staking Module (LIP-26, LIP-27) +Audited at commit [`3469910`](https://github.com/lidofinance/community-staking-module/tree/3469910c0d29a54b37d0c4de3cf527a3e7be2099). + An updated report for [the previously audited Lido Community Staking Module](#10-2024-ackee-blockchain-audit-of-community-staking-module-lip-26) features a re-audit of the revised CS Verifier contract and deployment verification for the redeployed contract. This contract was updated to reflect changes introduced in LIP-27. No addition issues were found. @@ -380,10 +623,14 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Ackee%20Blockc ### 03-2025 Statemind GateSeal Deployment Validation Note +Audited at commit [`7e9704d`](https://github.com/lidofinance/gate-seals/tree/7e9704d9f40cd17652480a15f2ca9519d6b532d2). + See [note](https://github.com/lidofinance/audits/blob/main/Statemind%20GateSeal%20Deployment%20Validation%2003-2025.pdf) contents for more details. ### 02-2025 Certora Dual Governance Audit +Audited at commit [`3e0f1ae`](https://github.com/lidofinance/dual-governance/tree/3e0f1ae5740ef8410e928f6cc106e3a5f45a5a75). + - Total Issues: 6 (4 Fixed, 2 Acknowledged) - Critical Issues: 0 - High Issues: 0 @@ -394,6 +641,8 @@ See full [report](https://github.com/lidofinance/audits/blob/main/Certora%20Dual ### 02-2025 OpenZeppelin Dual Governance Re-Audit +Audited at commit [`3e0f1ae`](https://github.com/lidofinance/dual-governance/tree/3e0f1ae5740ef8410e928f6cc106e3a5f45a5a75). + - Total Issues: 9 (4 Fixed, 2 Acknowledged) - Critical Issues: 0 - High Issues: 0 @@ -405,12 +654,16 @@ See full [report](https://github.com/lidofinance/audits/blob/main/OpenZeppelin%2 ### 02-2025 Runtime Verification Dual Governance Formal Verification +Audited at commit [`3e0f1ae`](https://github.com/lidofinance/dual-governance/tree/3e0f1ae5740ef8410e928f6cc106e3a5f45a5a75). + Lido has engaged Runtime Verification to formally verify the correctness and safety properties of the smart contracts that comprise the Lido Dual Governance mechanism. See full [report](https://github.com/lidofinance/audits/blob/main/Runtime%20Verification%20Dual%20Governance%20Formal%20Verification%20Report%2002-2025.pdf) for more details. ### 11-2024 OpenZeppelin Dual Governance Audit +Audited at commit [`8296824`](https://github.com/lidofinance/dual-governance/tree/8296824213195dd5421222602cbeb3f5a25017b2). + - Total Issues: 26 (18 Fixed, 2 Acknowledged) - Critical Issues: 0 - High Issues: 0 @@ -422,6 +675,8 @@ See full [report](https://github.com/lidofinance/audits/blob/main/OpenZeppelin%2 ### 10-2024 Ackee Blockchain Audit of Staking Router v2 (LIP-25) +Audited at commit [`1ffbb7e`](https://github.com/lidofinance/core/tree/1ffbb7e49e112fcac678f59bf63ba57a7e522874). + - Total Issues: 7 (5 Fixed, 2 Acknowledged) - Critical Issues: 0 - High Issues: 0 @@ -434,6 +689,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Ackee%20Blockc ### 10-2024 Ackee Blockchain Audit of Community Staking Module (LIP-26) +Audited at commit [`347496d`](https://github.com/lidofinance/community-staking-module/tree/347496df916c3b987a7f3fe8b0bd85c9b62ad730). + - Total Issues: 39 (25 Fixed, 2 Partially fixed, 12 Acknowledged) - Critical Issues: 0 - High Issues: 0 @@ -446,6 +703,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Ackee%20Blockc ### 10-2024 MixBytes On-chain Audit of Community Staking Module (LIP-23, LIP-25, LIP-26) +Audited at commit [`9c7d014`](https://github.com/lidofinance/community-staking-module/tree/9c7d014844395b37a64b3ca4ed5dc27de0af23a5). + - Total Issues: 41 (18 Fixed, 23 Acknowledged) - Critical Issues: 0 - High Issues: 0 @@ -456,6 +715,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/MixBytes%20Lid ### 10-2024 MixBytes Off-chain Audit of Lido Oracle v4 +Audited at commit [`e363fe4`](https://github.com/lidofinance/lido-oracle/tree/e363fe41fa771ebbc54bb33395222a5f5c8855ad). + - Total Issues: 3 (2 Fixed, 1 Acknowledged) - Critical Issues: 0 - High Issues: 0 @@ -466,6 +727,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/MixBytes%20Lid ### 10-2024 Statemind Dual Governance Audit +Audited at commit [`3e0f1ae`](https://github.com/lidofinance/dual-governance/tree/3e0f1ae5740ef8410e928f6cc106e3a5f45a5a75). + - Total Issues: 46 (32 Fixed, 14 Acknowledged) - Critical Issues: 0 - High Issues: 0 @@ -476,6 +739,8 @@ See full [report](https://github.com/lidofinance/audits/blob/main/Statemind%20Du ### 09-2024 Certora Dual Governance Draft Audit +Audited at commit [`071f033`](https://github.com/lidofinance/dual-governance/tree/071f0338c05f3809ac3ebf90b58f2c21d718177f). + - Total Issues: 23 (22 Fixed, 1 Acknowledged) - Critical Issues: 2 (2 Fixed) - High Issues: 6 (6 Fixed) @@ -486,6 +751,8 @@ See full [report](https://github.com/lidofinance/audits/blob/main/Certora%20Dual ### 07-2024 Ackee Blockchain Audit of the Simple Delegation +Audited at commit [`50d9802`](https://github.com/lidofinance/aragon-apps/tree/50d9802f6e728388b80b275b7baea5d93b9b6b25). + - Total Issues: 14 (6 Fixed, 8 Acknowledged) - High Issues: 0 - Medium Issues: 0 @@ -496,6 +763,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Ackee%20Blockc ### 07-2024 Statemind Audit of the Simple Delegation +Audited at commit [`50d9802`](https://github.com/lidofinance/aragon-apps/tree/50d9802f6e728388b80b275b7baea5d93b9b6b25). + - Total Issues: 6 (2 Fixed, 4 Acknowledged) - Critical Issues: 0 - High Issues: 0 @@ -506,6 +775,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Statemind%20Li ### 07-2024 MixBytes Sanity Checker Security Audit (LIP-23) +Audited at commit [`f6deb4b`](https://github.com/lidofinance/core/tree/f6deb4bcd4f1a05a7336111c0a139fcebcae6b68). + - Total Issues: 8 (4 Fixed, 4 Acknowledged) - Critical Issues: 0 - High Issues: 0 @@ -516,6 +787,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Lido%20Sanity% ### 06-2024 ChainSecurity Code Assessment of the LIP-23: Rebase Check Smart Contracts +Audited at commit [`efeff81`](https://github.com/lidofinance/lido-dao/tree/efeff81c18f85451ebf98e8fd8bb78b8eb0095f6). + - Total Issues: 3 (3 Fixed) - Critical Issues: 0 - High Issues: 0 @@ -527,10 +800,14 @@ See [full report](https://github.com/lidofinance/audits/blob/main/ChainSecurity% ### 04-2024 Statemind GateSeal Deployment Validation Note +Audited at commit [`7e9704d`](https://github.com/lidofinance/gate-seals/tree/7e9704d9f40cd17652480a15f2ca9519d6b532d2). + See [note](https://github.com/lidofinance/audits/blob/main/Statemind%20GateSeal%20Deployment%20Validation%2004-2024.pdf) contents for more details. ### 03-2024 Ackee Blockchain Lido Stonks Audit +Audited at commit [`40af0bb`](https://github.com/lidofinance/stonks/tree/40af0bb3ede47b7666d402280b96f05e6c897546). + - Total Issues: 9 (7 Fixed, 2 Acknowledged) - Critical: 0 - High: 0 @@ -542,6 +819,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Ackee%20Blockc ### 01-2024 Statemind Lido Simple DVT Easy Track Factories Audit +Audited at commit [`bccc999`](https://github.com/lidofinance/easy-track/tree/bccc99912b8cd03cb152ebc13295cc3a3ea28664). + - Total Issues: 10 (7 Fixed, 3 Acknowledged) - Critical: 0 - High: 0 @@ -552,7 +831,9 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Statemind%20Li ### 12-2023 Pessimistic Lido Stonks Audit -This audit report covers the code up to commit [`ad6a9e83c095f5052e404bc13585ad2c752f242f`](https://github.com/lidofinance/stonks/tree/ad6a9e83c095f5052e404bc13585ad2c752f242f). For release version audit please go to [03-2024 Ackee Blockchain Lido Stonks Audit](#03-2024-ackee-blockchain-lido-stonks-audit). +Audited at commit [`10a2499`](https://github.com/lidofinance/stonks/tree/10a249971099be1b36c27f43a3f0a520389fb62e). + +This audit report covers the code up to the audited commit above. For release version audit please go to [03-2024 Ackee Blockchain Lido Stonks Audit](#03-2024-ackee-blockchain-lido-stonks-audit). - Total Issues: 8 (4 Fixed, 4 Acknowledged) - Critical: 0 @@ -564,6 +845,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Pessimistic%20 ### 10-2023 Statemind Lido roles analysis +Audited at commit: not applicable; on-chain analysis, no source commit. + | Impact severity \ Attack feasibility | Low | Medium | High | | ------------------------------------ | --- | ------ | ---- | | Critical | 56 | 5 | 0 | @@ -576,6 +859,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Statemind%20Li ### 10-2023 Oxorio Lido Easy Track Smart Contracts Security Audit (Easy Track Factories for Stablecoins) +Audited at commit [`425f4a2`](https://github.com/lidofinance/easy-track/tree/425f4a254ceb2be389f669580b9dc76618e92756). + - Total Issues: 9 (5 Fixed, 4 Acknowledged) - Critical: 0 - Major: 0 @@ -586,6 +871,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Oxorio%20Lido% ### 05-2023 Statemind Lido V2 Upgrade Template Audit +Audited at commit: audited commit is not public. + - Total Issues: 14 (7 Fixed, 7 Acknowledged) - Critical Issues: 0 - High Issues: 0 @@ -596,10 +883,14 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Statemind%20Li ### 05-2023 Statemind Lido V2 Deployment Validation Note +Audited at commit [`e45c4d6`](https://github.com/lidofinance/lido-dao/tree/e45c4d6fb8120fd29426b8d969c19d8a798ca974). + See [note](https://github.com/lidofinance/audits/blob/main/Statemind%20Lido%20V2%20Deployment%20Validation%2005-2023.pdf) contents for more details. ### 05-2023 Hexens Lido V2 Oracle Security Review +Audited at commit [`4467895`](https://github.com/lidofinance/lido-oracle/tree/44678954915b8291c949904c63de5e4e4983b427). + - Total Issues: 2 (2 Fixed) - Critical Issues: 0 - High Issues: 0 @@ -611,6 +902,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Hexens%20Lido% ### 05-2023 Oxorio Lido V2 On-chain Audit +Audited at commit [`e45c4d6`](https://github.com/lidofinance/lido-dao/tree/e45c4d6fb8120fd29426b8d969c19d8a798ca974). + - Total Issues: 43 (4 Fixed, 37 Acknowledged, 2 No Issue) - Critical: 0 - Major: 7 (7 Acknowledged) @@ -621,6 +914,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Oxorio%20Lido% ### 05-2023 Oxorio Lido V2 Off-chain Audit +Audited at commit [`4467895`](https://github.com/lidofinance/lido-oracle/tree/44678954915b8291c949904c63de5e4e4983b427). + - Total Issues: 11 (1 Fixed, 10 Acknowledged) - Critical: 0 - Major: 5 (5 Acknowledged) @@ -631,6 +926,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Oxorio%20Lido% ### 04-2023 Hexens Lido V2 Smart Contract Audit +Audited at commit [`e45c4d6`](https://github.com/lidofinance/lido-dao/tree/e45c4d6fb8120fd29426b8d969c19d8a798ca974). + - Total Issues: 25 (16 Fixed, 9 Acknowledged) - Critical Issues: 1 (1 Fixed) - High Issues: 3 (3 Fixed) @@ -642,6 +939,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Hexens%20Lido% ### 04-2023 MixBytes Camp Lido V2 Contest +Audited at commit [`e575177`](https://github.com/lidofinance/lido-dao/tree/e57517730c3e11a41e9cbc32ce018726722335b7). + - Total Issues: 17 (8 Fixed, 9 Acknowledged) - Critical Issues: 0 - High Issues: 1 (1 Acknowledged) @@ -652,6 +951,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/MixBytes%20Cam ### 04-2023 Statemind GateSeals Audit +Audited at commit [`7e9704d`](https://github.com/lidofinance/gate-seals/tree/7e9704d9f40cd17652480a15f2ca9519d6b532d2). + - Total Issues: 4 (3 Fixed, 1 Acknowledged) - Critical Issues: 0 - High Issues: 1 (1 Fixed) @@ -663,6 +964,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Statemind%20Ga ### 04-2023 Certora Lido V2 Audit +Audited at commit [`e45c4d6`](https://github.com/lidofinance/lido-dao/tree/e45c4d6fb8120fd29426b8d969c19d8a798ca974). + - Total Issues: 23 (14 Fixed, 9 Acknowledged) - Critical Issues: 2 (2 Fixed) - High Issues: 5 (1 Fixed, 4 Acknowledged) @@ -674,6 +977,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Certora%20Lido ### 04-2023 Statemind Lido V2 Audit +Audited at commit [`e45c4d6`](https://github.com/lidofinance/lido-dao/tree/e45c4d6fb8120fd29426b8d969c19d8a798ca974). + - Total Issues: 120 (75 Fixed, 45 Acknowledged) - Critical Issues: 2 (1 Fixed, 1 Acknowledged) - High Issues: 8 (6 Fixed, 2 Acknowledged) @@ -684,6 +989,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Statemind%20Li ### 03-2023 Sigma Prime dc4bc Security Audit +Audited at commit [`3f2e02a`](https://github.com/lidofinance/dc4bc/tree/3f2e02a). + - Total Issues: 8 (8 Fixed) - Critical Issues: 0 - High Issues: 0 @@ -695,6 +1002,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Sigma%20Prime% ### 02-2023 ChainSecurity Lido Staking Router Audit Report +Audited at commit [`d820fdd`](https://github.com/lidofinance/lido-dao/tree/d820fddb0b298088db2a4d76fbc6007fdada908d). + - Total Issues: 13 (10 Fixed, 3 Acknowledged) - Critical Issues: 0 - High Issues: 1 (1 Fixed) @@ -705,6 +1014,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/ChainSecurity% ### 01-2023 Statemind TRP Vesting Escrow Audit Report +Audited at commit [`69dd13a`](https://github.com/lidofinance/lido-vesting-escrow/tree/69dd13adcd9c5a88da8c134b221209ccded04121). + - Total Issues: 5 (4 Fixed, 1 Acknowledged) - Critical Issues: 0 - High Issues: 0 @@ -715,6 +1026,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Statemind%20TR ### 09-2022 Statemind MEV-Boost relay allowlist Security Audit Report +Audited at commit [`912f414`](https://github.com/lidofinance/mev-boost-relay-allowed-list/tree/912f4143387ab04a7042b4887df67d3eecc97179). + - Total Issues: 7 (5 Fixed, 2 Acknowledged) - Critical Issues: 0 - High Issues: 0 @@ -725,6 +1038,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Statemind%20ME ### 09-2022 Statemind Reserve Fund Audit Report +Audited at commit [`625d384`](https://github.com/lidofinance/insurance-fund/tree/625d384f12c3df791085ecc2d15535e2121224d5). + - Total Issues: 4 (1 Fixed, 3 Acknowledged) - Critical Issues: 0 - High Issues: 0 @@ -735,6 +1050,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Statemind%20Re ### 09-2022 Statemind Easy Track Payment Processor with limits +Audited at commit [`cf5e788`](https://github.com/lidofinance/easy-track/tree/cf5e7887b60a3043f92f6cc0c25b5b4034431556). + - Total Issues: 9 (9 Acknowledged) - Critical Issues: 0 - High Issues: 1 (1 Acknowledged) @@ -745,6 +1062,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Statemind%20Ea ### 08-2022 ChainSecurity Code Assessment of the Lido Smart Contracts Audit Report +Audited at commit [`08436ce`](https://github.com/lidofinance/lido-dao/tree/08436ce13d67501fa723169c1dc69fe47b90cde4). + - Total Issues: 9 (4 Risk accepted, 5 Acknowledged) - Critical Issues: 0 - High Issues: 0 @@ -756,10 +1075,14 @@ See [full report](https://github.com/lidofinance/audits/blob/main/ChainSecurity% ### 08-2022 MixBytes Lido Protocol Security Auditor's Note On The Deployed Code Compliance +Audited at commit [`08436ce`](https://github.com/lidofinance/lido-dao/tree/08436ce13d67501fa723169c1dc69fe47b90cde4). + See [note](https://github.com/lidofinance/audits/blob/main/MixBytes%20Note%20on%20Deployed%20Code%20Compliance%2008-22.pdf) contents for more details. ### 06-2022 MixBytes Lido Two-Phase Voting Security Audit Report +Audited at commit [`4f7646f`](https://github.com/lidofinance/aragon-apps/tree/4f7646fbc90ac31fada5ed9e95669fc4aecbbc1c). + - Total Issues: 10 (7 Fixed, 3 Acknowledged) - Critical Issues: 0 - High Issues: 0 @@ -770,6 +1093,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/MixBytes%20Lid ### 05-2022 Oxorio Jumpgate Smart Contracts Security Audit Report +Audited at commit [`0802ae1`](https://github.com/lidofinance/jumpgates/tree/0802ae168c81c9a4aa32a1b3f1ac0099cd090428). + - Total Issues: 12 (11 Fixed, 1 Acknowledged) - Critical Issues: 0 - Major Issues: 1 (1 Fixed) @@ -780,6 +1105,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Oxorio%20Jumpg ### 05-2022 MixBytes Lido Protocol Security Audit Report +Audited at commit [`08436ce`](https://github.com/lidofinance/lido-dao/tree/08436ce13d67501fa723169c1dc69fe47b90cde4). + - Total Issues: 15 (13 Fixed, 2 Acknowledged) - Critical Issues: 0 - High Issues: 1 (1 Fixed) @@ -790,6 +1117,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/MixBytes%20Lid ### 02-2022 MixBytes AAVE stETH integration Security Audit Report +Audited at commit [`2a42cb5`](https://github.com/lidofinance/aave-protocol-v2/tree/2a42cb58d49c350d72c87614f0cf86819b29daa3). + - Total Issues: 11 (3 Fixed, 2 Acknowledged) - Critical Issues: 0 - Major Issues: 1 (1 Fixed) @@ -800,6 +1129,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/MixBytes%20AAV ### 02-2022 MixBytes In-protocol Coverage Security Audit Report +Audited at commit [`3d6a3f5`](https://github.com/lidofinance/lido-dao/tree/3d6a3f527e27a87e33c97726cce7de1ae7262d9f). + - Total Issues: 3 (3 Fixed) - Critical Issues: 1 (1 Fixed) - Major Issues: 0 @@ -810,6 +1141,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/MixBytes%20In- ### 02-2022 MixBytes Deposit Security Module Security Audit Report +Audited at commit [`816bf1d`](https://github.com/lidofinance/lido-dao/tree/816bf1d0995ba5cfdfc264de4acda34a7fe93eba). + - Total Issues: 22 (17 Fixed, 5 Acknowledged) - Critical Issues: 0 - Major Issues: 2 (2 Fixed) @@ -820,6 +1153,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/MixBytes%20Dep ### 01-2022 MixBytes bETH Vault Security Audit Report +Audited at commit [`9dd1e34`](https://github.com/lidofinance/anchor-collateral-steth/tree/9dd1e3433dd3d0360b95cf9470dd8af29dfce9e9). + bETH Vault was re-audited by MixBytes to incorporate the changes made for the vault to work with Wormhole bridge instead of the Shuttle bridge. - Total Issues: 5 (3 Fixed, 2 Acknowledged) @@ -832,6 +1167,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/MixBytes%20bET ### 10-2021 MixBytes Aragon Voting Security Audit +Audited at commit [`8c46da8`](https://github.com/lidofinance/aragon-apps/tree/8c46da8704d0011c42ece2896dbf4aeee069b84a). + The version of the [Aragon Voting smart contract](https://github.com/lidofinance/aragon-apps/blob/8c46da8704d0011c42ece2896dbf4aeee069b84a/apps/voting/contracts/Voting.sol) with support of the voting time change. - Total Issues: 9 (9 Acknowledged) @@ -844,6 +1181,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/MixBytes%20Ara ### 10-2021 Sigma Prime Easy Track Smart Contract Security Review +Audited at commit [`d7c0b24`](https://github.com/lidofinance/easy-track/tree/d7c0b24776c5d6c673236ac70a15c6d1c17d2520). + The testing team identified a total of nine (9) issues during this assessment, of which: - One (1) is classified as high risk (1 resolved), @@ -854,6 +1193,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Sigma%20Prime% ### 09-2021 MixBytes wstETH Security Audit +Audited at commit [`ea6fa22`](https://github.com/lidofinance/lido-dao/tree/ea6fa222004b88e6a24b566a51e5b56b0079272d). + - Total Issues: 5 (3 Acknowledged, 2 No Issue) - Critical Issues: 0 - Major Issues: 0 @@ -864,6 +1205,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/MixBytes%20wst ### 09-2021 MixBytes Easy Track Security Audit +Audited at commit [`7acdfe0`](https://github.com/lidofinance/easy-track/tree/7acdfe0cc9d0f2fc34b03e094c8225c0c9c659a3). + - Total Issues: 3 (2 Fixed, 1 No Issue) - Critical Issues: 0 - Major Issues: 0 @@ -874,6 +1217,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/MixBytes%20Eas ### 09-2021 MixBytes 1inch Rewards Manager Security Audit +Audited at commit [`c2cd966`](https://github.com/lidofinance/1inch-rewards-manager/tree/c2cd9665666deda9452fa9e3461fbf3537413945). + - Total Issues: 4 (4 Acknowledged) - Critical Issues: 0 - Major Issues: 0 @@ -884,6 +1229,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/MixBytes%201in ### 08-2021 MixBytes bETH Vault Security Audit +Audited at commit [`2fed0a0`](https://github.com/lidofinance/anchor-collateral-steth/tree/2fed0a0c0ba4ce67c82d0ed49cf6872245f44d20). + bETH Vault was re-audited by MixBytes to incorporate the changes made since the previous audit. - Total Issues: 0 @@ -896,6 +1243,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/MixBytes%20bET ### 07-2021 MixBytes bETH Vault Security Audit +Audited at commit [`c24beb0`](https://github.com/lidofinance/anchor-collateral-steth/tree/c24beb01afd7f6397e78062dd9a46906d38bc08b). + - Total Issues: 5 (3 Fixed, 2 Acknowledged) - Critical Issues: 0 - Major Issues: 0 @@ -906,6 +1255,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/MixBytes%20bET ### 06-2021 MixBytes stETH Price Feed Security Audit +Audited at commit [`4a5db9a`](https://github.com/lidofinance/steth-price-feed/tree/4a5db9ad4b0c8d815388d087a023f2b390af351a). + - Total Issues: 10 (3 Fixed, 6 No issue, 1 Acknowledged) - Critical Issues: 0 - Major Issues: 0 @@ -916,6 +1267,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/MixBytes%20stE ### 05-2021 MixBytes Audit: stETH price oracle +Audited at commit: private source repository; no public commit available. + - Total Issues: 7 (4 Fixed, 1 No issue, 2 Acknowledged) - Critical Issues: 0 - Major Issues: 0 @@ -926,6 +1279,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/MixBytes%20stE ### 05-2021 MixBytes Audit: Withdrawals Manager Proxy and Stub +Audited at commit [`214d477`](https://github.com/lidofinance/withdrawals-manager-stub/tree/214d4773648134f970509bfe37184aee3aff4d24). + - Total Issues: 1 (1 Fixed) - Critical Issues: 0 - Major Issues: 0 @@ -936,6 +1291,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/MixBytes%20Wit ### 04-2021 MixBytes Audit: ETH2 Oracle +Audited at commit [`bea1be9`](https://github.com/lidofinance/lido-dao/tree/bea1be988a6f5db1dc2bd002ab5919b71e4ce796). + - Total Issues: 7 (1 Fixed, 6 No issue) - Critical Issues: 0 - Major Issues: 0 @@ -946,6 +1303,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/MixBytes%20ETH ### 12-2020 Sigma Prime Security Assessment +Audited at commit [`ad4b2f6`](https://github.com/lidofinance/lido-dao/tree/ad4b2f634850e46375bf5c8717abf0e49cdce3be). + The testing team identified a total of eighteen (18) issues during this assessment, of which: - Five (5) are classified as medium risk (4 resolved, 1 closed), @@ -956,6 +1315,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Sigma%20Prime% ### 12-2020 Quantstamp Audit +Audited at commit [`ad4b2f6`](https://github.com/lidofinance/lido-dao/tree/ad4b2f634850e46375bf5c8717abf0e49cdce3be). + - Total Issues: 14 (7 Resolved) - High Risk Issues: 0 (0 Resolved) - Medium Risk Issues: 1 (0 Resolved) @@ -965,52 +1326,78 @@ See [full report](https://github.com/lidofinance/audits/blob/main/Sigma%20Prime% See [full report](https://github.com/lidofinance/audits/blob/main/QSP%20Lido%20Report%2012-2020.pdf) for more details. -## Lido Multichain audit reports (18 reports) +## Lido Multichain audit reports (19 reports) + +### 03-2026 Cantina zkSync Lido Bridge PR-85 Fix Review + +Audited at commit [`b5bbb12`](https://github.com/lidofinance/lido-l2/tree/b5bbb12982cc5d3279243e271ae69de6e4e6725e). + +Review of fixes implemented for the zkSync L1ERC20Bridge (follow-up to the [October 2023 Cantina audit](https://github.com/lidofinance/audits/blob/main/L2/zkSync-2023-10-Cantina-Audit-Report.pdf)). No additional issues were identified. + +- Total Issues: 0 + +See [full report](https://github.com/lidofinance/audits/blob/main/L2/zkSync-2026-03-05-Cantina-PR-85-fix-report.pdf) for more details. ### 04-2025 MixBytes wstETH on Lisk Verification +Audited at commit [`a569a49`](https://github.com/lidofinance/lido-l2/tree/a569a49966360fbd223f4bd26a8720eab3799e5f). + The deployed contracts are verified in accordance to the [proposal](https://research.lido.fi/t/temperature-check-wsteth-deployment-to-lisk-and-ownership-acceptance-by-lido-nec/6669/5) See [full report](https://github.com/lidofinance/audits/blob/main/L2/wstETH-on-Lisk-2025-04-MixBytes-deployment-verification.pdf) for more details. ### 02-2025 MixBytes stETH on Unichain Verification +Audited at commit [`8f19e11`](https://github.com/lidofinance/lido-l2/tree/8f19e1101a211c8f3d42af7ffcb87ab0ebcf750c). + The deployed contracts are verified against the [stETH on Optimism](/deployed-contracts/#optimism) deployment. See [full report](https://github.com/lidofinance/audits/blob/main/L2/stETH-on-Unichain-2025-02-MixBytes-deployment-verification.pdf) for more details. ### 01-2025 MixBytes stETH on Soneium Verification +Audited at commit [`8f19e11`](https://github.com/lidofinance/lido-l2/tree/8f19e1101a211c8f3d42af7ffcb87ab0ebcf750c). + The deployed contracts are verified against the [stETH on Optimism](/deployed-contracts/#optimism) deployment. See [full report](https://github.com/lidofinance/audits/blob/main/L2/stETH-on-Soneium-2025-01-MixBytes-deployment-verification.pdf) for more details. ### 11-2024 Nethermind Security wstETH on Starknet Deployment Verification +Audited at commit: not applicable; deployment verification, no source commit. + The deployed contracts are verified in accordance to the [proposal](https://research.lido.fi/t/wsteth-deployment-on-starknet/6335/9) See the [full report](https://github.com/lidofinance/audits/blob/main/L2/Starknet-2024-11-14-deployment-verification.pdf) for more details. ### 10-2024 Quantstamp wstETH on Zircuit Verification +Audited at commit [`d8b68db`](https://github.com/lidofinance/lido-l2/tree/d8b68db14a98d49aeff20bfd4ccd581a02ed3f48). + The deployed contracts are verified against the [wstETH on Optimism](https://github.com/lidofinance/lido-l2) and [Governance crosschain bridges](https://github.com/lidofinance/governance-crosschain-bridges) references together with the [proposed setup](/token-guides/cross-chain-tokens-guide#mainnet-proposed-configuration) initialization. See [full report](https://github.com/lidofinance/audits/blob/main/L2/Zircuit_2024-10-02-Quantstamp-wstETH-deployment-verification.pdf) for more details. ### 08-2024 Oxorio wstETH on BNB Verification +Audited at commit [`0d37b0f`](https://github.com/wormhole-foundation/example-native-token-transfers/tree/0d37b0f4975084492c72ca881c1218d6e1aae9e3). + The deployed contracts are verified in accordance to the [proposal](https://research.lido.fi/t/wormhole-x-axelar-lido-bridge-implementation-for-wsteth-on-bnb-chain/6012) See full [initial](https://github.com/lidofinance/audits/blob/main/bsc/Lido-wstETH-on-BNB-Deployment-Verification-Report.pdf) and [remediated](https://github.com/lidofinance/audits/blob/main/bsc/Lido-wstETH-on-BNB-Deployment-Verification-Report-remediated.pdf) reports for more details. ### 07-2024 Cantina wstETH on Mode Verification +Audited at commit [`3c36d93`](https://github.com/lidofinance/lido-l2/tree/3c36d93cb05dbd83d854b24e071c2e5f80897272). + The deployed contracts are verified against the [wstETH on Base](/deployed-contracts/#base) deployment. See [full report](https://github.com/lidofinance/audits/blob/main/L2/Mode-2024-07-18-Cantina-wstETH-deployment-verification.pdf) for more details. ### 07-2024 MixBytes Lido a.DI Audit +Audited at commit [`41c8197`](https://github.com/lidofinance/aave-delivery-infrastructure/tree/41c81975c2ce5b430b283e6f4aab922c3bde1555). + - Total Issues: 13 (13 Acknowledged) - Critical Issues: 0 - High Issues: 0 @@ -1021,6 +1408,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/bsc/MixBytes%2 ### 06-2024 Ackee Blockchain stETH on Optimism Audit +Audited at commit [`8f19e11`](https://github.com/lidofinance/lido-l2/tree/8f19e1101a211c8f3d42af7ffcb87ab0ebcf750c). + - Total Issues: 15 (10 Fixed, 5 Acknowledged) - Critical Issues: 0 - High Issues: 0 @@ -1033,6 +1422,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/L2/stETH-on-Op ### 06-2024 MixBytes stETH on Optimism Audit +Audited at commit [`a31049a`](https://github.com/lidofinance/lido-l2/tree/a31049ac8828d6d6a214b63279ff678101d55308). + - Total Issues: 20 (15 Fixed, 5 Acknowledged) - Critical Issues: 0 - High Issues: 1 (1 Fixed) @@ -1043,6 +1434,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/L2/stETH-on-Op ### 01-2024 Zellic Scroll Lido Gateway Audit +Audited at commit [`69224eb`](https://github.com/scroll-tech/scroll/tree/69224ebb935d499c055c7859c1c8ade57244249c). + - Total Issues: 1 (1 No Issue) - Info Issues: 1 (1 No Issue) @@ -1050,12 +1443,16 @@ See [full report](https://github.com/lidofinance/audits/blob/main/L2/Scroll-2024 ### 12-2023 Diligence Linea Custom Bridged Token Audit +Audited at commit [`3cf8552`](https://github.com/Consensys/linea-contracts/tree/3cf85529fd4539eb06ba998030c37e47f98c528a). + - Total Issues: 0 See [full report](https://github.com/lidofinance/audits/blob/main/L2/Lidea-2023-12-Diligence-Custom-Bridged-Token-Audit-Report.pdf) for more details. ### 12-2023 OpenZeppelin Linea Bridge Audit +Audited at commit [`f08c190`](https://github.com/Consensys/linea-contracts/tree/f08c1906855198e2dc0413a47dcb38291b7087e5). + > NB: the most of the contracts and issues are related not to wstETH bridge but to the entire Linea L2 system. - Total Issues: 33 (20 Fixed, 3 Partially fixed) @@ -1069,6 +1466,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/L2/Linea-2023- ### 10-2023 Cantina zkSync Lido Bridge Audit +Audited at commit [`fa6a77e`](https://github.com/txfusion/lido-l2/tree/fa6a77e694a34dc6f03d57bb8c934941e554ac9d). + - Total Issues: 22 (15 Fixed, 3 Acknowledged, 4 No issue) - Critical Issues: 1 (1 Fixed) - High Issues: 0 @@ -1080,6 +1479,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/L2/zkSync-2023 ### 10-2023 Diligence Linea Cross‐Chain Governance Executor Audit +Audited at commit [`315308a`](https://github.com/Consensys/governance-crosschain-bridges/tree/315308a2640c696937185732159b130417f29997). + - Total Issues: 1 (1 Fixed) - Informational: 1 (1 Fixed) @@ -1087,6 +1488,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/L2/Linea-2023- ### 09-2023 Verilog Mantle L2 ERC20 Token Bridge Audit +Audited at commit [`cdd513c`](https://github.com/mantlenetworkio/lido-l2/tree/cdd513cd3d25699a8757f8e730b443a495d0240e). + - Total Issues: 5 (3 Fixed, 2 Acknowledged) - High: 0 - Medium: 0 @@ -1097,6 +1500,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/L2/Mantle-2023 ### 08-2022 Oxorio Governance Crosschain Bridges Smart Contracts Security Audit +Audited at commit [`8fa25b0`](https://github.com/lidofinance/governance-crosschain-bridges/tree/8fa25b0080dd3dcc2390313631aea6796a12c9d8). + - Total Issues: 8 (8 Acknowledged) - Critical Issues: 0 - Major Issues: 0 @@ -1107,6 +1512,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/L2/Governance- ### 07-2022 Oxorio Lido L2 Smart Contracts Security Audit +Audited at commit [`082e7eb`](https://github.com/lidofinance/lido-l2/tree/082e7eb59de63bd376b30886568813408d04f00b). + - Total Issues: 9 (6 Fixed, 2 Acknowledged, 1 No Issue) - Critical Issues: 1 (1 Acknowledged) - Major Issues: 1 (1 Fixed) @@ -1115,18 +1522,15 @@ See [full report](https://github.com/lidofinance/audits/blob/main/L2/Governance- See [full report](https://github.com/lidofinance/audits/blob/main/L2/Lido-L2-2022-07-Oxorio-Smart-Contracts-Security-Audit-Report.pdf) for more details. -## Lido on Polygon PoS (3 reports) - -### 03-2026 Cantina zkSync Lido Bridge PR-85 Fix Review - -Review of fixes implemented for the zkSync L1ERC20Bridge (follow-up to the [August 2023 Cantina audit](https://github.com/lidofinance/audits/blob/main/L2/zkSync-2023-10-Cantina-Audit-Report.pdf)). No additional issues were identified. +## Lido on Polygon PoS (2 reports) -- Total Issues: 0 - -See [full report](https://github.com/lidofinance/audits/blob/main/L2/zkSync-2026-03-05-Cantina-PR-85-fix-report.pdf) for more details. +> [!WARNING] +> Lido on Polygon PoS has been sunset. Staking was discontinued on December 16, 2024, and unstaking via the Lido on Polygon UI was supported until June 16, 2025. See [Sunsetting Lido on Polygon](https://help.lido.fi/en/articles/10289349-sunsetting-lido-on-polygon) and the [sunset announcement](https://blog.lido.fi/lido-on-polygon-sunset/). The reports below are kept for historical reference. ### 08-2022 Oxorio Lido on Polygon V2 +Audited at commit [`6b18e23`](https://github.com/lidofinance/polygon-contracts/tree/6b18e23ae258ff0aa84aecb82d8498f3c52f29e4). + - Total Issues: 107 (61 Fixed, 11 Acknowledged, 35 No Issue) - Critical Issues: 0 - Major Issues: 0 @@ -1137,6 +1541,8 @@ See [full report](https://github.com/lidofinance/audits/blob/main/polygon/Oxorio ### 04-2022 Lido On Polygon Smart Contracts Security Audit for PR#69 +Audited at commit [`1d8e469`](https://github.com/Shard-Labs/PoLido/tree/1d8e4696d9a225f9079bcaff1cb8a60c8eff8131). + - Total Issues: 9 (4 Fixed, 1 Acknowledged, 1 No Issue) - Critical Issues: 0 - Major Issues: 0 diff --git a/scripts/fetch-audits.js b/scripts/fetch-audits.js index f2e6efb05..e85785e87 100644 --- a/scripts/fetch-audits.js +++ b/scripts/fetch-audits.js @@ -12,9 +12,23 @@ function preprocessMarkdown(md) { const urlPrefix = 'https://github.com/lidofinance/audits/blob/main/'; const pdfRe = /(\[.*?\]\()(?!http|https|#|\/|mailto:)([^)]+\.pdf)(\))/g; - return md.replace(pdfRe, (_, p1, rel, p3) => p1 + urlPrefix + rel + p3); + const docsRe = /(\]\()https:\/\/docs\.lido\.fi(\/[^)]*)?(\))/g; + // Pages renamed on docs.lido.fi; onBrokenLinks='throw' doesn't follow redirects + const renamedDocs = { + '/token-guides/wsteth-bridging-guide#the-proposed-configuration': + '/token-guides/cross-chain-tokens-guide#mainnet-proposed-configuration', + }; + return md + .replace(pdfRe, (_, p1, rel, p3) => p1 + urlPrefix + rel + p3) + .replace(docsRe, (_, p1, rel, p3) => { + const local = rel || '/'; + return p1 + (renamedDocs[local] || local) + p3; + }); } +// Earn audits live on a separate page (earn/audits.md) +const skipSections = ['## Lido Earn']; + function sortAuditsAndCount(md) { const lines = md.split('\n'); const out = []; @@ -25,6 +39,10 @@ function sortAuditsAndCount(md) { function flushSection() { if (!inH2) return; + if (skipSections.includes(stripCount(curH2))) { + buf = []; + return; + } const { heading, body, nReports } = processSection(curH2, buf); counts[stripCount(heading)] = nReports; out.push(...body); From 72330ad9ded5c28a482810881ebc0adcf85ca64a Mon Sep 17 00:00:00 2001 From: Yuri Tkachenko Date: Tue, 4 Aug 2026 17:16:41 +0100 Subject: [PATCH 2/3] fix: formating Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- docs/security/audits.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/security/audits.md b/docs/security/audits.md index a4200c7d5..dcaf6032f 100644 --- a/docs/security/audits.md +++ b/docs/security/audits.md @@ -1524,8 +1524,9 @@ See [full report](https://github.com/lidofinance/audits/blob/main/L2/Lido-L2-202 ## Lido on Polygon PoS (2 reports) -> [!WARNING] -> Lido on Polygon PoS has been sunset. Staking was discontinued on December 16, 2024, and unstaking via the Lido on Polygon UI was supported until June 16, 2025. See [Sunsetting Lido on Polygon](https://help.lido.fi/en/articles/10289349-sunsetting-lido-on-polygon) and the [sunset announcement](https://blog.lido.fi/lido-on-polygon-sunset/). The reports below are kept for historical reference. +:::warning +Lido on Polygon PoS has been sunset. Staking was discontinued on December 16, 2024, and unstaking via the Lido on Polygon UI was supported until June 16, 2025. See [Sunsetting Lido on Polygon](https://help.lido.fi/en/articles/10289349-sunsetting-lido-on-polygon) and the [sunset announcement](https://blog.lido.fi/lido-on-polygon-sunset/). The reports below are kept for historical reference. +::: ### 08-2022 Oxorio Lido on Polygon V2 From d298c97f0a4717160bef129aa91b6bdfbad24f8f Mon Sep 17 00:00:00 2001 From: Yuri T Date: Tue, 11 Aug 2026 17:14:33 +0100 Subject: [PATCH 3/3] feat: refactor fetch functions --- .github/workflows/build.yml | 2 + .github/workflows/deploy.yml | 3 + README.md | 4 +- config/redirects.js | 114 ++++++++ docs/lips.md | 2 +- docs/security/audits.md | 44 +++- docusaurus.config.js | 101 +------ package.json | 3 +- scripts/fetch-audits.js | 213 +++++++-------- scripts/fetch-external.js | 18 -- scripts/fetch-lips.js | 324 +++++++++++------------ scripts/fetch-msig-quorums.js | 436 +++++++++++++++---------------- scripts/fetch.js | 34 +++ scripts/lib/async.js | 26 ++ scripts/lib/http.js | 26 ++ scripts/lib/markdown.js | 189 ++++++++++++++ scripts/lib/output.js | 8 + scripts/lib/tasks.js | 15 ++ tests/async.test.js | 40 +++ tests/fetch-audits.test.js | 229 ++++++++++++++++ tests/fetch-lips.test.js | 241 +++++++++++++++++ tests/fetch-msig-quorums.test.js | 321 +++++++++++++++++++++++ tests/fetch.test.js | 96 +++++++ tests/http.test.js | 96 +++++++ tests/output.test.js | 13 + tests/redirects.test.js | 18 ++ tests/tasks.test.js | 44 ++++ 27 files changed, 2016 insertions(+), 644 deletions(-) create mode 100644 config/redirects.js delete mode 100644 scripts/fetch-external.js create mode 100644 scripts/fetch.js create mode 100644 scripts/lib/async.js create mode 100644 scripts/lib/http.js create mode 100644 scripts/lib/markdown.js create mode 100644 scripts/lib/output.js create mode 100644 scripts/lib/tasks.js create mode 100644 tests/async.test.js create mode 100644 tests/fetch-audits.test.js create mode 100644 tests/fetch-lips.test.js create mode 100644 tests/fetch-msig-quorums.test.js create mode 100644 tests/fetch.test.js create mode 100644 tests/http.test.js create mode 100644 tests/output.test.js create mode 100644 tests/redirects.test.js create mode 100644 tests/tasks.test.js diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 32338ced5..6c4f4bbba 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,5 +21,7 @@ jobs: cache: npm - name: Install Dependencies run: npm ci + - name: Run Tests + run: npm test - name: Build Project run: npm run build diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 413da9a32..9deaa0c12 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -41,6 +41,9 @@ jobs: - name: Install Dependencies run: npm ci + - name: Run Tests + run: npm test + - name: Build Project run: npm run build diff --git a/README.md b/README.md index 98d88cf33..0a5764642 100644 --- a/README.md +++ b/README.md @@ -106,5 +106,7 @@ Generation lives in [`src/plugins`](src/plugins/README.md) — see that README f Fetch external markdown files to build an up-to-date version. ```console -npm run fetch-external +npm run fetch ``` + +This refreshes the audit and LIP indexes, then checks documented Safe multisig quorums against public chain RPC endpoints. Use `npm run fetch-audits`, `npm run fetch-lips`, or `npm run fetch-msig-quorums` to run one task. diff --git a/config/redirects.js b/config/redirects.js new file mode 100644 index 000000000..eaf527893 --- /dev/null +++ b/config/redirects.js @@ -0,0 +1,114 @@ +const redirects = [ + { + to: '/guides/lido-tokens-integration-guide', + from: '/guides/steth-integration-guide', + }, + { + to: '/token-guides/cross-chain-tokens-guide', + from: '/token-guides/wsteth-bridging-guide', + }, + { + to: '/run-on-lido/stvaults/', + from: '/guides/stvaults/', + }, + { + to: '/run-on-lido/stvaults/tech-documentation/pdg', + from: ['/guides/stvaults/pdg', '/run-on-lido/stvaults/pdg'], + }, + { + to: '/run-on-lido/stvaults/operational-and-management-guides/health-monitoring-guide', + from: '/run-on-lido/stvaults/health-monitoring-guide', + }, + { + to: '/run-on-lido/stvaults/operational-and-management-guides/health-emergency-guide', + from: '/run-on-lido/stvaults/health-emergency-guide', + }, + { + to: '/run-on-lido/stvaults/operational-and-management-guides/node-operators-identification', + from: '/run-on-lido/stvaults/node-operators-identification', + }, + { + to: '/run-on-lido/stvaults/features-and-mechanics/roles-and-permissions', + from: '/run-on-lido/stvaults/roles-and-permissions', + }, + { + to: '/run-on-lido/stvaults/features-and-mechanics/parameters-and-metrics', + from: '/run-on-lido/stvaults/parameters-and-metrics', + }, + { + to: '/run-on-lido/stvaults/tech-documentation/integration-overview', + from: '/run-on-lido/stvaults/integration-overview', + }, + { + to: '/run-on-lido/stvaults/tech-documentation/tech-design', + from: '/run-on-lido/stvaults/tech-design', + }, + { + to: '/run-on-lido/stvaults/tech-documentation/consolidation', + from: '/run-on-lido/stvaults/consolidation', + }, + { + to: '/multisigs/emergency-brakes', + from: '/multisigs/emergency-breaks', + }, + { + to: '/earn', + from: '/earn/introduction', + }, + { + to: '/contracts/circuit-breaker', + from: '/contracts/gate-seal', + }, + { + to: '/staking-modules/csm/contracts/Accounting', + from: '/staking-modules/csm/contracts/CSAccounting', + }, + { + to: '/staking-modules/csm/contracts/Ejector', + from: '/staking-modules/csm/contracts/CSEjector', + }, + { + to: '/staking-modules/csm/contracts/FeeOracle', + from: '/staking-modules/csm/contracts/CSFeeOracle', + }, + { + to: '/staking-modules/csm/contracts/ParametersRegistry', + from: '/staking-modules/csm/contracts/CSParametersRegistry', + }, + { + to: '/staking-modules/csm/contracts/Verifier', + from: '/staking-modules/csm/contracts/CSVerifier', + }, + { + to: '/staking-modules/csm/contracts/ExitPenalties', + from: '/staking-modules/csm/contracts/CSExitPenalties', + }, + { + to: '/staking-modules/csm/contracts/FeeDistributor', + from: '/staking-modules/csm/contracts/CSFeeDistributor', + }, + { + to: '/staking-modules/csm/contracts/ValidatorStrikes', + from: '/staking-modules/csm/contracts/CSStrikes', + }, + { + to: '/staking-modules/csm/contracts/MerkleGateFactory', + from: '/staking-modules/csm/contracts/VettedGateFactory', + }, +] + +const REDIRECTED_FRAGMENTS = { + '/token-guides/wsteth-bridging-guide#the-proposed-configuration': '#mainnet-proposed-configuration', +} + +function resolveRedirect(source) { + const fragmentIndex = source.indexOf('#') + const sourcePath = fragmentIndex === -1 ? source : source.slice(0, fragmentIndex) + const sourceFragment = fragmentIndex === -1 ? '' : source.slice(fragmentIndex) + const redirect = redirects.find(({ from }) => (Array.isArray(from) ? from.includes(sourcePath) : from === sourcePath)) + + if (!redirect) return source + return redirect.to + (REDIRECTED_FRAGMENTS[source] || sourceFragment) +} + +module.exports = { redirects, resolveRedirect } diff --git a/docs/lips.md b/docs/lips.md index a68c62c2f..af295ce25 100644 --- a/docs/lips.md +++ b/docs/lips.md @@ -14,7 +14,7 @@ More details on the contribution process and LIPs statuses can be found [here](h | LIP # | Title | Author | Discussions‑to | |------------|-------|--------|----------------| -| [37](https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-37.md) | "Execution Delegation Framework" | Raman Siamionau, Matsvei Talstalutski | [Link](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746) | +| [37](https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-37.md) | Execution Delegation Framework | Raman Siamionau, Matsvei Talstalutski | [Link](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746) | ## Approved diff --git a/docs/security/audits.md b/docs/security/audits.md index dcaf6032f..1c87f0137 100644 --- a/docs/security/audits.md +++ b/docs/security/audits.md @@ -1326,7 +1326,33 @@ Audited at commit [`ad4b2f6`](https://github.com/lidofinance/lido-dao/tree/ad4b2 See [full report](https://github.com/lidofinance/audits/blob/main/QSP%20Lido%20Report%2012-2020.pdf) for more details. -## Lido Multichain audit reports (19 reports) +## Lido Multichain audit reports (22 reports) + +### 08-2026 MixBytes Lido L2 Direct Staking Security Audit + +Audited at commit [`3d1d484`](https://github.com/lidofinance/l2-direct-staking/tree/3d1d484c31d39881cdbe8423614e3043e89e027f). + +- Total Issues: 4 (3 Fixed, 1 Acknowledged) +- Critical Issues: 0 +- High Issues: 0 +- Medium Issues: 0 +- Low Issues: 4 (3 Fixed, 1 Acknowledged) + +See [full report](https://github.com/lidofinance/audits/blob/main/L2/Direct-Staking-2026-08-MixBytes-Security-Audit-Report.pdf) for more details. + +### 08-2026 MixBytes Chainlink CCIP Linea Adapter Security Audit + +> NB: It is not authored by Lido. Used by Lido Direct Staking Automation contracts*. + +Audited at commit [`34344f5`](https://github.com/Aphyla/chainlink-csr/tree/34344f5b247de110c74c18df6b445ab1cb238f5e). + +- Total Issues: 1 (1 Acknowledged) +- Critical Issues: 0 +- High Issues: 0 +- Medium Issues: 0 +- Low Issues: 1 (1 Acknowledged) + +See [full report](https://github.com/lidofinance/audits/blob/main/L2/Direct-Staking-Linea-Adapter-2026-08-MixBytes-Security-Audit-Report.pdf) for more details. ### 03-2026 Cantina zkSync Lido Bridge PR-85 Fix Review @@ -1354,6 +1380,22 @@ The deployed contracts are verified against the [stETH on Optimism](/deployed-co See [full report](https://github.com/lidofinance/audits/blob/main/L2/stETH-on-Unichain-2025-02-MixBytes-deployment-verification.pdf) for more details. +### 02-2025 Trail of Bits Chainlink Custom Senders and Receivers Security Review + +> NB: It is not authored by Lido. Used by Lido Direct Staking Automation contracts. + +Audited at commit [`8e08c07`](https://github.com/Aphyla/chainlink-csr/tree/8e08c07ec55bbe32f447b450d915f6813eac6cc7). +The sources are also available at [Lido chainlink-csr mirror](https://github.com/lidofinance/chainlink-csr). + +- Total Issues: 7 (6 Fixed, 1 Acknowledged) +- Critical Issues: 0 +- High Issues: 1 (1 Fixed) +- Medium Issues: 2 (1 Fixed, 1 Acknowledged) +- Informational Issues: 3 (3 Fixed) + +See [full report](https://github.com/lidofinance/audits/blob/main/L2/Direct-Staking-2025-02-Trail-of-Bits-Chainlink-CSR-Security-Review.pdf) for more details. +The report is also available from [Trail of Bits publications](https://github.com/trailofbits/publications/blob/master/reviews/2025-02-chainlink-customsendersreceivers-securityreview.pdf). + ### 01-2025 MixBytes stETH on Soneium Verification Audited at commit [`8f19e11`](https://github.com/lidofinance/lido-l2/tree/8f19e1101a211c8f3d42af7ffcb87ab0ebcf750c). diff --git a/docusaurus.config.js b/docusaurus.config.js index 025ab419b..8d5cd2c5a 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -1,3 +1,5 @@ +const { redirects } = require('./config/redirects') + const aiArtifactsCollections = [ { path: 'docs', routeBasePath: '/', label: 'Main Docs' }, { path: 'run-on-lido', routeBasePath: 'run-on-lido', label: 'Run on Lido' }, @@ -106,104 +108,7 @@ module.exports = async function createConfigAsync() { [ '@docusaurus/plugin-client-redirects', { - redirects: [ - { - to: '/guides/lido-tokens-integration-guide', - from: '/guides/steth-integration-guide', - }, - { - to: '/token-guides/cross-chain-tokens-guide', - from: '/token-guides/wsteth-bridging-guide', - }, - { - to: '/run-on-lido/stvaults/', - from: '/guides/stvaults/', - }, - { - to: '/run-on-lido/stvaults/tech-documentation/pdg', - from: ['/guides/stvaults/pdg', '/run-on-lido/stvaults/pdg'], - }, - { - to: '/run-on-lido/stvaults/operational-and-management-guides/health-monitoring-guide', - from: '/run-on-lido/stvaults/health-monitoring-guide', - }, - { - to: '/run-on-lido/stvaults/operational-and-management-guides/health-emergency-guide', - from: '/run-on-lido/stvaults/health-emergency-guide', - }, - { - to: '/run-on-lido/stvaults/operational-and-management-guides/node-operators-identification', - from: '/run-on-lido/stvaults/node-operators-identification', - }, - { - to: '/run-on-lido/stvaults/features-and-mechanics/roles-and-permissions', - from: '/run-on-lido/stvaults/roles-and-permissions', - }, - { - to: '/run-on-lido/stvaults/features-and-mechanics/parameters-and-metrics', - from: '/run-on-lido/stvaults/parameters-and-metrics', - }, - { - to: '/run-on-lido/stvaults/tech-documentation/integration-overview', - from: '/run-on-lido/stvaults/integration-overview', - }, - { - to: '/run-on-lido/stvaults/tech-documentation/tech-design', - from: '/run-on-lido/stvaults/tech-design', - }, - { - to: '/run-on-lido/stvaults/tech-documentation/consolidation', - from: '/run-on-lido/stvaults/consolidation', - }, - { - to: '/multisigs/emergency-brakes', - from: '/multisigs/emergency-breaks', - }, - { - to: '/earn', - from: '/earn/introduction', - }, - { - to: '/contracts/circuit-breaker', - from: '/contracts/gate-seal', - }, - { - to: '/staking-modules/csm/contracts/Accounting', - from: '/staking-modules/csm/contracts/CSAccounting', - }, - { - to: '/staking-modules/csm/contracts/Ejector', - from: '/staking-modules/csm/contracts/CSEjector', - }, - { - to: '/staking-modules/csm/contracts/FeeOracle', - from: '/staking-modules/csm/contracts/CSFeeOracle', - }, - { - to: '/staking-modules/csm/contracts/ParametersRegistry', - from: '/staking-modules/csm/contracts/CSParametersRegistry', - }, - { - to: '/staking-modules/csm/contracts/Verifier', - from: '/staking-modules/csm/contracts/CSVerifier', - }, - { - to: '/staking-modules/csm/contracts/ExitPenalties', - from: '/staking-modules/csm/contracts/CSExitPenalties', - }, - { - to: '/staking-modules/csm/contracts/FeeDistributor', - from: '/staking-modules/csm/contracts/CSFeeDistributor', - }, - { - to: '/staking-modules/csm/contracts/ValidatorStrikes', - from: '/staking-modules/csm/contracts/CSStrikes', - }, - { - to: '/staking-modules/csm/contracts/MerkleGateFactory', - from: '/staking-modules/csm/contracts/VettedGateFactory', - }, - ], + redirects, }, ], [ diff --git a/package.json b/package.json index ee153aca1..fdf334439 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,10 @@ "serve": "docusaurus serve", "write-translations": "docusaurus write-translations", "write-heading-ids": "docusaurus write-heading-ids", + "test": "node --test tests/*.test.js", "format": "prettier --write docs/", + "fetch": "node scripts/fetch.js", "fetch-audits": "node scripts/fetch-audits.js", - "fetch-external": "node scripts/fetch-external.js", "fetch-lips": "node scripts/fetch-lips.js", "fetch-msig-quorums": "node scripts/fetch-msig-quorums.js" }, diff --git a/scripts/fetch-audits.js b/scripts/fetch-audits.js index e85785e87..7abaa0146 100644 --- a/scripts/fetch-audits.js +++ b/scripts/fetch-audits.js @@ -1,137 +1,102 @@ -const fs = require('fs'); -const https = require('https'); -const path = require('path'); - -// Source & dest ------------------------------------------------------------ -const fileUrl = - 'https://raw.githubusercontent.com/lidofinance/audits/refs/heads/main/README.md'; -const localPath = path.join(__dirname, '../docs/security/audits.md'); - -/* ------------------------------ helpers --------------------------------- */ -function preprocessMarkdown(md) { - const urlPrefix = - 'https://github.com/lidofinance/audits/blob/main/'; - const pdfRe = /(\[.*?\]\()(?!http|https|#|\/|mailto:)([^)]+\.pdf)(\))/g; - const docsRe = /(\]\()https:\/\/docs\.lido\.fi(\/[^)]*)?(\))/g; - // Pages renamed on docs.lido.fi; onBrokenLinks='throw' doesn't follow redirects - const renamedDocs = { - '/token-guides/wsteth-bridging-guide#the-proposed-configuration': - '/token-guides/cross-chain-tokens-guide#mainnet-proposed-configuration', - }; - return md - .replace(pdfRe, (_, p1, rel, p3) => p1 + urlPrefix + rel + p3) - .replace(docsRe, (_, p1, rel, p3) => { - const local = rel || '/'; - return p1 + (renamedDocs[local] || local) + p3; - }); +#!/usr/bin/env node + +const fs = require('node:fs') +const path = require('node:path') + +const { fetchText } = require('./lib/http') +const { convertGithubAlerts } = require('./lib/markdown') +const { printCounts } = require('./lib/output') +const { runTask } = require('./lib/tasks') +const { resolveRedirect } = require('../config/redirects') + +const AUDITS_URL = 'https://raw.githubusercontent.com/lidofinance/audits/refs/heads/main/README.md' +const OUTPUT_PATH = path.join(__dirname, '../docs/security/audits.md') +const AUDITS_REPOSITORY_URL = 'https://github.com/lidofinance/audits/blob/main/' +const RELATIVE_PDF_LINK = /(\[.*?\]\()(?!http|https|#|\/|mailto:)([^)]+\.pdf)(\))/g +const DOCS_LINK = /(\]\()https:\/\/docs\.lido\.fi(\/[^)]*)?(\))/g +const SKIPPED_SECTION = '## Lido Earn' + +function preprocessMarkdown(markdown) { + return convertGithubAlerts(markdown) + .replace(RELATIVE_PDF_LINK, (_, opening, relativePath, closing) => { + return opening + AUDITS_REPOSITORY_URL + relativePath + closing + }) + .replace(DOCS_LINK, (_, opening, pathWithFragment, closing) => { + const localPath = pathWithFragment || '/' + return opening + resolveRedirect(localPath) + closing + }) } -// Earn audits live on a separate page (earn/audits.md) -const skipSections = ['## Lido Earn']; - -function sortAuditsAndCount(md) { - const lines = md.split('\n'); - const out = []; - let curH2 = null; - let buf = []; - let inH2 = false; - const counts = {}; // { heading: nReports } - - function flushSection() { - if (!inH2) return; - if (skipSections.includes(stripCount(curH2))) { - buf = []; - return; +function splitByHeading(lines, prefix) { + const preamble = [] + const sections = [] + let currentSection = null + + for (const line of lines) { + if (line.startsWith(prefix)) { + currentSection = { heading: line, lines: [] } + sections.push(currentSection) + } else if (currentSection) { + currentSection.lines.push(line) + } else { + preamble.push(line) } - const { heading, body, nReports } = processSection(curH2, buf); - counts[stripCount(heading)] = nReports; - out.push(...body); - buf = []; } - function stripCount(h) { - return h.replace(/\s*\(\d+\s+reports?\)$/i, '').trim(); - } + return { preamble, sections } +} + +function stripReportCount(heading) { + return heading.replace(/\s*\(\d+\s+reports?\)$/i, '').trim() +} - function processSection(headingLine, buffer) { - const auditBlocks = []; - const preamble = []; - let cur = null; - - for (const ln of buffer) { - if (ln.startsWith('### ')) { - if (cur) auditBlocks.push(cur); - cur = { header: ln, lines: [] }; - } else { - (cur ? cur.lines : preamble).push(ln); - } +function reportDateValue(heading) { + const match = heading.match(/^###\s+(\d{1,2})-(\d{4})/) + return match ? Number(match[2]) * 12 + Number(match[1]) : 0 +} + +function sortAuditsAndCount(markdown) { + const document = splitByHeading(markdown.split('\n'), '## ') + const output = [...document.preamble] + const counts = {} + let skippedSectionFound = false + + for (const section of document.sections) { + const heading = stripReportCount(section.heading) + if (heading === SKIPPED_SECTION) { + skippedSectionFound = true + continue } - if (cur) auditBlocks.push(cur); - - auditBlocks.sort((a, b) => { - const r = /^###\s+(\d{1,2})-(\d{4})/; - const A = a.header.match(r); - const B = b.header.match(r); - const vA = A ? +A[2] * 12 + +A[1] : 0; - const vB = B ? +B[2] * 12 + +B[1] : 0; - return vB - vA; - }); - - const hClean = stripCount(headingLine); - const headingWithCount = `${hClean} (${auditBlocks.length} reports)`; - - const sectionLines = [headingWithCount, ...preamble]; - for (const blk of auditBlocks) { - sectionLines.push(blk.header, ...blk.lines); + + const { preamble, sections: reports } = splitByHeading(section.lines, '### ') + reports.sort((left, right) => reportDateValue(right.heading) - reportDateValue(left.heading)) + + counts[heading] = reports.length + output.push(`${heading} (${reports.length} reports)`, ...preamble) + for (const report of reports) { + output.push(report.heading, ...report.lines) } - return { - heading: hClean, - nReports: auditBlocks.length, - body: sectionLines, - }; } - for (const ln of lines) { - if (ln.startsWith('## ')) { - flushSection(); - curH2 = ln; - inH2 = true; - } else if (inH2) { - buf.push(ln); - } else { - out.push(ln); - } + if (!skippedSectionFound) { + throw new Error(`required audit section not found: ${SKIPPED_SECTION}`) } - flushSection(); - return { content: out.join('\n'), counts }; + + return { content: output.join('\n'), counts } } -/* ---------------------------- fetch & write ----------------------------- */ -https - .get(fileUrl, (res) => { - if (res.statusCode !== 200) { - console.error('Failed to fetch audits README –', res.statusCode); - process.exit(1); - } - let data = ''; - res.on('data', (c) => (data += c)); - res.on('end', () => { - const pre = preprocessMarkdown(data); - const { content, counts } = sortAuditsAndCount(pre); - - fs.mkdirSync(path.dirname(localPath), { recursive: true }); - fs.writeFileSync(localPath, content, 'utf8'); - - // Pretty-print counts summary - console.log('\nCategory counts:'); - for (const [cat, n] of Object.entries(counts)) { - console.log(` • ${cat}: ${n}`); - } - - console.log('\n👌 Audits list written →', localPath); - }); - }) - .on('error', (e) => { - console.error('Network error:', e.message); - process.exit(1); - }); +async function run() { + const markdown = await fetchText(AUDITS_URL) + const preprocessed = preprocessMarkdown(markdown) + const { content, counts } = sortAuditsAndCount(preprocessed) + + fs.mkdirSync(path.dirname(OUTPUT_PATH), { recursive: true }) + fs.writeFileSync(OUTPUT_PATH, content, 'utf8') + + printCounts(counts) + console.log('\n👌 Audits list written →', OUTPUT_PATH) +} + +if (require.main === module) runTask(run) + +module.exports = { preprocessMarkdown, run, sortAuditsAndCount } diff --git a/scripts/fetch-external.js b/scripts/fetch-external.js deleted file mode 100644 index 4f69053ca..000000000 --- a/scripts/fetch-external.js +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env node - -const { spawnSync } = require('node:child_process'); -const path = require('node:path'); - -const tasks = ['fetch-audits.js', 'fetch-lips.js']; - -for (const t of tasks) { - const abs = path.join(__dirname, t); - console.log(`\n▶ running ${t} …`); - const { status } = spawnSync(process.execPath, [abs], { stdio: 'inherit' }); - if (status !== 0) { - console.error(`✖ ${t} failed (exit ${status}) — aborting`); - process.exit(status); - } -} - -console.log('\n✅ All external docs fetched & generated successfully'); diff --git a/scripts/fetch-lips.js b/scripts/fetch-lips.js index d5c3562b0..513a0d91d 100644 --- a/scripts/fetch-lips.js +++ b/scripts/fetch-lips.js @@ -1,205 +1,193 @@ -// scripts/fetch-lips.js - -const fs = require("fs"); -const https = require("https"); -const path = require("path"); - -// Path to the lido improvement proposals repo API -const API_URL = - "https://api.github.com/repos/lidofinance/lido-improvement-proposals/contents/LIPS?ref=develop"; -// Raw path base dir -const RAW_BASE = - "https://raw.githubusercontent.com/lidofinance/lido-improvement-proposals/develop/LIPS/"; -// Where to put the file generate -const localPath = path.join(__dirname, "../docs/lips.md"); - -/* Canonical list & order of statuses we display */ -const STATUSES = [ - "WIP", - "Proposed", - "Approved", - "Implemented", - "Rejected", - "Withdrawn", - "Deferred", - "Moribund", -]; - -/* Mapping of raw → canonical */ +#!/usr/bin/env node + +const fs = require('node:fs') +const path = require('node:path') +const matter = require('gray-matter') + +const { createLimiter } = require('./lib/async') +const { fetchJson, fetchText } = require('./lib/http') +const { parseMarkdownTable } = require('./lib/markdown') +const { printCounts } = require('./lib/output') +const { runTask } = require('./lib/tasks') + +const API_URL = 'https://api.github.com/repos/lidofinance/lido-improvement-proposals/contents/LIPS?ref=develop' +const RAW_BASE_URL = 'https://raw.githubusercontent.com/lidofinance/lido-improvement-proposals/develop/LIPS/' +const REPOSITORY_BASE_URL = 'https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/' +const OUTPUT_PATH = path.join(__dirname, '../docs/lips.md') +const LIP_FETCH_CONCURRENCY = 4 + +const GITHUB_HEADERS = { + 'User-Agent': 'node/fetch-lips', + Accept: 'application/vnd.github+json', +} + +const STATUSES = ['WIP', 'Proposed', 'Approved', 'Implemented', 'Rejected', 'Withdrawn', 'Deferred', 'Moribund'] + const STATUS_MAP = { - draft: "WIP", - wip: "WIP", - proposed: "Proposed", - discussion: "Proposed", - review: "Proposed", - approved: "Approved", - voted: "Approved", - implemented: "Implemented", - accepted: "Implemented", - final: "Implemented", - rejected: "Rejected", - declined: "Rejected", - withdrawn: "Withdrawn", - deferred: "Deferred", - moribund: "Moribund", - deprecated: "Moribund", -}; - -function getFromGitHub(url, json = false) { - return new Promise((resolve, reject) => { - https - .get( - url, - { - headers: { - "User-Agent": "node/fetch-lips", - Accept: json ? "application/vnd.github+json" : "*/*", - }, - }, - (res) => { - if (res.statusCode !== 200) { - reject(new Error(`HTTP ${res.statusCode} → ${url}`)); - res.resume(); - return; - } - const bufs = []; - res.on("data", (d) => bufs.push(d)); - res.on("end", () => { - const txt = Buffer.concat(bufs).toString("utf8"); - resolve(json ? JSON.parse(txt) : txt); - }); - } - ) - .on("error", reject); - }); + draft: 'WIP', + wip: 'WIP', + proposed: 'Proposed', + discussion: 'Proposed', + review: 'Proposed', + approved: 'Approved', + voted: 'Approved', + implemented: 'Implemented', + accepted: 'Implemented', + final: 'Implemented', + rejected: 'Rejected', + declined: 'Rejected', + withdrawn: 'Withdrawn', + deferred: 'Deferred', + moribund: 'Moribund', + deprecated: 'Moribund', } -function parseYamlFrontMatter(md) { - const m = md.match(/^---[\r\n]+([\s\S]*?)^---/m); - if (!m) return null; - const meta = {}; - for (const line of m[1].split(/\r?\n/)) { - if (!line.trim()) continue; - const kv = line.match(/^([^:]+):\s*(.*)$/); - if (kv) meta[kv[1].trim().toLowerCase()] = kv[2].trim(); - } - return meta; +function parseYamlFrontMatter(markdown) { + if (!/^---(?:\r?\n)/.test(markdown)) return null + + return Object.fromEntries(Object.entries(matter(markdown).data).map(([key, value]) => [key.toLowerCase(), value])) } -function parseMarkdownTableHeader(md) { - const block = md.match(/^(?:\s*\|.*\n){3}/m); - if (!block) return null; - const [hdr, sep, vals] = block[0].trim().split(/\r?\n/); - const keys = hdr - .split("|") - .map((s) => s.trim().replace(/:$/, "").toLowerCase()) - .filter(Boolean); - const fields = vals - .split("|") - .map((s) => s.trim()) - .filter(Boolean); - if (!keys.includes("lip")) return null; - const meta = {}; - for (let i = 0; i < keys.length; i++) meta[keys[i]] = fields[i] || ""; - return meta; +function parseMarkdownTableHeader(markdown) { + const lines = markdown.split(/\r?\n/) + const headerIndex = lines.findIndex((line) => line.trim() !== '') + if (headerIndex === -1) return null + + const table = parseMarkdownTable(lines, headerIndex) + if (!table || table.rows.length === 0) return null + + const keys = table.headers.map((header) => header.replace(/:$/, '').toLowerCase()) + if (!keys.includes('lip')) return null + + const values = table.rows[0].cells + return Object.fromEntries(keys.map((key, keyIndex) => [key, values[keyIndex] || ''])) } -function parseMetadata(md) { - return parseYamlFrontMatter(md) || parseMarkdownTableHeader(md) || {}; +function parseMetadata(markdown) { + return parseYamlFrontMatter(markdown) || parseMarkdownTableHeader(markdown) || {} } -function normalizeLip(raw) { - const m = String(raw || "").match(/(\d+)/); - const num = m ? parseInt(m[1], 10) : NaN; - return { num, str: num ? String(num) : String(raw) }; +function parseLipNumber(rawValue) { + const match = String(rawValue ?? '').match(/(\d+)/) + return match ? Number.parseInt(match[1], 10) : null } -function canonicalStatus(raw = "") { - return STATUS_MAP[raw.toLowerCase()] || "WIP"; +function normalizeStatus(rawStatus = '') { + return STATUS_MAP[String(rawStatus).trim().toLowerCase()] || 'WIP' } -function esc(str = "") { - return str.replace(/\|/g, "|").replace(/\n/g, " "); +function escapeTableCell(value = '') { + return String(value).replace(/\|/g, '|').replace(/\r?\n/g, ' ') } -function renderDiscussion(val) { - if (!val || /^(none|null)$/i.test(val)) return "None"; - const urls = val.split(/[,\s]+/).filter(Boolean); +function renderDiscussionLinks(value) { + if (!value || /^(none|null)$/i.test(value)) return 'None' + + const urls = value.split(/[,\s]+/).filter(Boolean) return urls - .map((u, idx) => `[Link${urls.length > 1 ? " " + (idx + 1) : ""}](${u})`) - .join(", "); + .map((url, index) => { + const suffix = urls.length > 1 ? ` ${index + 1}` : '' + return `[Link${suffix}](${url})` + }) + .join(', ') } function renderTable(rows) { const header = - "| LIP # | Title | Author | Discussions‑to |\n|------------|-------|--------|----------------|"; + '| LIP # | Title | Author | Discussions‑to |\n' + '|------------|-------|--------|----------------|' const body = rows .map( - (r) => - `| [${r.num}](${r.link}) | ${esc(r.title)} | ${esc(r.author)} | ${renderDiscussion( - r.discussion - )} |` + (row) => + `| [${row.number}](${row.link}) | ${escapeTableCell(row.title)} | ` + + `${escapeTableCell(row.author)} | ${renderDiscussionLinks(row.discussion)} |`, ) - .join("\n"); - return `${header}\n${body}`; + .join('\n') + return `${header}\n${body}` } -async function main() { - const listing = await getFromGitHub(API_URL, true); - const files = listing.filter((f) => f.name.endsWith(".md")); - - const buckets = Object.fromEntries(STATUSES.map((s) => [s, []])); - - for (const f of files) { - const rawMd = await getFromGitHub(RAW_BASE + f.name); - const meta = parseMetadata(rawMd); - const { num } = normalizeLip(meta.lip); - - if (isNaN(num)) { - console.warn("⚠️ skipping malformed:", f.name); - continue; - } - - const entry = { - num, - title: meta.title || "", - author: meta.author || "", - discussion: meta["discussions-to"] || meta.discussion || "", - link: `https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-${num}.md`, - }; - - buckets[canonicalStatus(meta.status)].push(entry); +function parseLip(fileName, markdown) { + let metadata + try { + metadata = parseMetadata(markdown) + } catch { + return null } - for (const arr of Object.values(buckets)) arr.sort((a, b) => b.num - a.num); + const number = parseLipNumber(metadata.lip) + if (number === null) return null - const out = [ - "# Lido Improvement Proposals\n", - "Lido Improvement Proposals (LIPs) describe standards for the Lido platform, including core protocol specifications, client APIs, and contract standards.\n", - "More details on the contribution process and LIPs statuses can be found [here](https://github.com/lidofinance/lido-improvement-proposals).\n", - ]; + return { + number, + status: normalizeStatus(metadata.status), + title: metadata.title || '', + author: metadata.author || '', + discussion: metadata['discussions-to'] || metadata.discussion || '', + link: `${REPOSITORY_BASE_URL}${fileName}`, + } +} +function buildLipDocument(lips) { + const buckets = Object.fromEntries(STATUSES.map((status) => [status, []])) + for (const lip of lips) buckets[lip.status].push(lip) + for (const bucket of Object.values(buckets)) { + bucket.sort((left, right) => right.number - left.number) + } - // Pretty-print counts summary - console.log('\nCategory counts:'); + const lines = [ + '# Lido Improvement Proposals', + '', + 'Lido Improvement Proposals (LIPs) describe standards for the Lido platform, including core protocol specifications, client APIs, and contract standards.', + '', + 'More details on the contribution process and LIPs statuses can be found [here](https://github.com/lidofinance/lido-improvement-proposals).', + '', + ] + const counts = {} + + for (const status of STATUSES) { + const rows = buckets[status] + if (rows.length === 0) continue + + counts[status] = rows.length + lines.push(`## ${status}`, '', renderTable(rows), '') + } - for (const sec of STATUSES) { - if (!buckets[sec].length) continue; + return { content: lines.join('\n'), counts } +} - console.log(` • ${sec}: ${buckets[sec].length}`); +async function fetchLip(file) { + const markdown = await fetchText(`${RAW_BASE_URL}${file.name}`, { headers: GITHUB_HEADERS }) + const lip = parseLip(file.name, markdown) + if (!lip) console.warn('⚠️ skipping malformed:', file.name) + return lip +} - out.push(`## ${sec}\n`); - out.push(renderTable(buckets[sec])); - out.push(""); - } +async function run() { + const listing = await fetchJson(API_URL, { headers: GITHUB_HEADERS }) + const markdownFiles = listing.filter((file) => file.name.endsWith('.md')) + const limit = createLimiter(LIP_FETCH_CONCURRENCY) + const lips = (await Promise.all(markdownFiles.map((file) => limit(() => fetchLip(file))))).filter(Boolean) - fs.mkdirSync(path.dirname(localPath), { recursive: true }); - fs.writeFileSync(localPath, out.join("\n"), "utf8"); + const { content, counts } = buildLipDocument(lips) + fs.mkdirSync(path.dirname(OUTPUT_PATH), { recursive: true }) + fs.writeFileSync(OUTPUT_PATH, content, 'utf8') - console.log("\n👌 Lido improvement proposals fetched and summary written →", localPath); + printCounts(counts) + console.log('\n👌 Lido improvement proposals fetched and summary written →', OUTPUT_PATH) } -main().catch((e) => { - console.error(e); - process.exit(1); -}); +if (require.main === module) runTask(run) + +module.exports = { + buildLipDocument, + escapeTableCell, + fetchLip, + normalizeStatus, + parseLip, + parseLipNumber, + parseMarkdownTableHeader, + parseMetadata, + parseYamlFrontMatter, + renderDiscussionLinks, + renderTable, + run, +} diff --git a/scripts/fetch-msig-quorums.js b/scripts/fetch-msig-quorums.js index 02e682750..2eb833190 100644 --- a/scripts/fetch-msig-quorums.js +++ b/scripts/fetch-msig-quorums.js @@ -7,343 +7,315 @@ // // node scripts/fetch-msig-quorums.js -const fs = require('fs'); -const path = require('path'); +const fs = require('node:fs') +const path = require('node:path') -// --------------------------------------------------------------------------- -// Config -// --------------------------------------------------------------------------- -const ROOT = path.resolve(__dirname, '..'); -const DOC_DIRS = ['docs', 'earn', 'run-on-lido']; +const { createLimiter } = require('./lib/async') +const { fetchJson } = require('./lib/http') +const { rebuildTableRow, scanMarkdownTables, splitTableRow } = require('./lib/markdown') +const { runTask } = require('./lib/tasks') + +const ROOT = path.resolve(__dirname, '..') +const DOC_DIRS = ['docs', 'earn', 'run-on-lido'] // Safe URL chain prefix → ordered list of public JSON-RPC endpoints. Each is // tried in turn on transient failures (5xx/4xx/network). const CHAIN_RPCS = { - eth: ['https://eth.drpc.org', 'https://ethereum-rpc.publicnode.com', 'https://eth.llamarpc.com'], - base: ['https://base.drpc.org', 'https://base-rpc.publicnode.com', 'https://base.llamarpc.com'], - arb1: ['https://arbitrum.drpc.org', 'https://arbitrum-one-rpc.publicnode.com', 'https://arb1.arbitrum.io/rpc'], - oeth: ['https://optimism.drpc.org', 'https://optimism-rpc.publicnode.com', 'https://mainnet.optimism.io'], - matic: ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon-rpc.com'], - bnb: ['https://bsc.drpc.org', 'https://bsc-rpc.publicnode.com', 'https://binance.llamarpc.com'], - zksync: ['https://zksync.drpc.org', 'https://mainnet.era.zksync.io'], - gno: ['https://gnosis.drpc.org', 'https://gnosis-rpc.publicnode.com', 'https://rpc.gnosischain.com'], - avax: ['https://avalanche.drpc.org', 'https://avalanche-c-chain-rpc.publicnode.com'], - celo: ['https://celo.drpc.org', 'https://forno.celo.org'], - scr: ['https://scroll.drpc.org', 'https://rpc.scroll.io'], - linea: ['https://linea.drpc.org', 'https://rpc.linea.build'], - mnt: ['https://mantle.drpc.org', 'https://rpc.mantle.xyz'], - mantle: ['https://mantle.drpc.org', 'https://rpc.mantle.xyz'], + eth: ['https://eth.drpc.org', 'https://ethereum-rpc.publicnode.com', 'https://eth.llamarpc.com'], + base: ['https://base.drpc.org', 'https://base-rpc.publicnode.com', 'https://base.llamarpc.com'], + arb1: ['https://arbitrum.drpc.org', 'https://arbitrum-one-rpc.publicnode.com', 'https://arb1.arbitrum.io/rpc'], + oeth: ['https://optimism.drpc.org', 'https://optimism-rpc.publicnode.com', 'https://mainnet.optimism.io'], + matic: ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon-rpc.com'], + bnb: ['https://bsc.drpc.org', 'https://bsc-rpc.publicnode.com', 'https://binance.llamarpc.com'], + zksync: ['https://zksync.drpc.org', 'https://mainnet.era.zksync.io'], + gno: ['https://gnosis.drpc.org', 'https://gnosis-rpc.publicnode.com', 'https://rpc.gnosischain.com'], + avax: ['https://avalanche.drpc.org', 'https://avalanche-c-chain-rpc.publicnode.com'], + celo: ['https://celo.drpc.org', 'https://forno.celo.org'], + scr: ['https://scroll.drpc.org', 'https://rpc.scroll.io'], + linea: ['https://linea.drpc.org', 'https://rpc.linea.build'], + mnt: ['https://mantle.drpc.org', 'https://rpc.mantle.xyz'], + mantle: ['https://mantle.drpc.org', 'https://rpc.mantle.xyz'], unichain: ['https://unichain.drpc.org', 'https://mainnet.unichain.org'], - ink: ['https://ink.drpc.org', 'https://rpc-gel.inkonchain.com'], - lisk: ['https://lisk.drpc.org', 'https://rpc.api.lisk.com'], - mode: ['https://mode.drpc.org', 'https://mainnet.mode.network'], - soneium: ['https://soneium.drpc.org', 'https://rpc.soneium.org'], - plasma: ['https://plasma.drpc.org', 'https://rpc.plasma.to'], - sep: ['https://sepolia.drpc.org', 'https://ethereum-sepolia-rpc.publicnode.com'], - holesky: ['https://holesky.drpc.org', 'https://ethereum-holesky-rpc.publicnode.com'], - hoe: ['https://hoodi.drpc.org', 'https://ethereum-hoodi-rpc.publicnode.com'], -}; - -const CONCURRENCY = 4; -const FETCH_TIMEOUT_MS = 15_000; + ink: ['https://ink.drpc.org', 'https://rpc-gel.inkonchain.com'], + lisk: ['https://lisk.drpc.org', 'https://rpc.api.lisk.com'], + mode: ['https://mode.drpc.org', 'https://mainnet.mode.network'], + soneium: ['https://soneium.drpc.org', 'https://rpc.soneium.org'], + plasma: ['https://plasma.drpc.org', 'https://rpc.plasma.to'], + sep: ['https://sepolia.drpc.org', 'https://ethereum-sepolia-rpc.publicnode.com'], + holesky: ['https://holesky.drpc.org', 'https://ethereum-holesky-rpc.publicnode.com'], + hoe: ['https://hoodi.drpc.org', 'https://ethereum-hoodi-rpc.publicnode.com'], +} + +const CONCURRENCY = 4 // Match any URL/text containing a Safe `safe=:
` query param, // independent of the host/path (e.g. app.safe.global, safe.scroll.xyz, // multisig.mantle.xyz, …). Unsupported chain prefixes will surface as // `unsupported chain: ` errors so coverage stays visible. -const SAFE_LINK_RE = /[?&]safe=([a-z0-9]+):(0x[0-9a-fA-F]{40})/i; -const TABLE_SEP_CELL_RE = /^:?-{3,}:?$/; -const QUORUM_HEADER_RE = /^quorum$/i; -const INLINE_QUORUM_RE = /^\s*\*\*Quorum(?:\*\*:|:\*\*)\s*(\d+\s*\/\s*\d+)\s*$/; -const INLINE_QUORUM_REPLACE_RE = /(\*\*Quorum(?:\*\*:|:\*\*)\s*)\d+\s*\/\s*\d+/; -const HEADING_RE = /^#{1,6}\s/; - -// --------------------------------------------------------------------------- -// Concurrency limiter -// --------------------------------------------------------------------------- -function createLimiter(max) { - let active = 0; - const queue = []; - const next = () => { - if (active >= max || queue.length === 0) return; - active++; - const { fn, resolve, reject } = queue.shift(); - Promise.resolve() - .then(fn) - .then(resolve, reject) - .finally(() => { active--; next(); }); - }; - return (fn) => new Promise((resolve, reject) => { queue.push({ fn, resolve, reject }); next(); }); -} - -const limit = createLimiter(CONCURRENCY); +const SAFE_LINK_RE = /[?&]safe=([a-z0-9]+):(0x[0-9a-fA-F]{40})/i +const QUORUM_HEADER_RE = /^quorum$/i +const INLINE_QUORUM_RE = /^\s*\*\*Quorum(?:\*\*:|:\*\*)\s*(\d+\s*\/\s*\d+)\s*$/ +const INLINE_QUORUM_REPLACE_RE = /(\*\*Quorum(?:\*\*:|:\*\*)\s*)\d+\s*\/\s*\d+/ +const HEADING_RE = /^#{1,6}\s/ -// --------------------------------------------------------------------------- // Multisig threshold/owners read via JSON-RPC `eth_call` // // Safe (Gnosis Safe) ABI: // getThreshold() selector 0xe75235b8 → uint256 // getOwners() selector 0xa0e67e2b → address[] -// --------------------------------------------------------------------------- -const SELECTOR_GET_THRESHOLD = '0xe75235b8'; -const SELECTOR_GET_OWNERS = '0xa0e67e2b'; - -const quorumCache = new Map(); - -function fetchQuorum(chain, address) { - const key = `${chain}:${address.toLowerCase()}`; - if (!quorumCache.has(key)) { - // Evict on rejection so a later reference to the same Safe within this - // run gets a fresh attempt against the fallback RPCs instead of inheriting - // a cached failure. - const promise = limit(() => resolveQuorum(chain, address)).catch((err) => { - if (quorumCache.get(key) === promise) quorumCache.delete(key); - throw err; - }); - quorumCache.set(key, promise); - } - return quorumCache.get(key); -} +const SELECTOR_GET_THRESHOLD = '0xe75235b8' +const SELECTOR_GET_OWNERS = '0xa0e67e2b' async function resolveQuorum(chain, address) { - const rpcs = CHAIN_RPCS[chain]; - if (!rpcs) throw new Error(`unsupported chain: ${chain}`); + const rpcs = CHAIN_RPCS[chain] + if (!rpcs) throw new Error(`unsupported chain: ${chain}`) const [threshold, owners] = await Promise.all([ rpcCallWithFallback(rpcs, address, SELECTOR_GET_THRESHOLD).then(decodeUint), rpcCallWithFallback(rpcs, address, SELECTOR_GET_OWNERS).then(decodeArrayLength), - ]); + ]) if (!(threshold > 0 && threshold <= owners)) { - throw new Error(`invalid quorum: ${threshold}/${owners}`); + throw new Error(`invalid quorum: ${threshold}/${owners}`) } - return `${threshold}/${owners}`; + return `${threshold}/${owners}` } -async function rpcCallWithFallback(rpcs, to, data) { - let lastErr; - for (const url of rpcs) { +function createQuorumFetcher(resolve, maxConcurrency) { + const limit = createLimiter(maxConcurrency) + const cache = new Map() + + return function fetchQuorum(chain, address) { + const key = `${chain}:${address.toLowerCase()}` + if (cache.has(key)) return cache.get(key) + + const request = limit(() => resolve(chain, address)).catch((error) => { + if (cache.get(key) === request) cache.delete(key) + throw error + }) + cache.set(key, request) + return request + } +} + +const fetchQuorum = createQuorumFetcher(resolveQuorum, CONCURRENCY) + +async function rpcCallWithFallback(rpcUrls, address, data) { + let lastError + for (const rpcUrl of rpcUrls) { try { - return await rpcCall(url, to, data); - } catch (err) { - lastErr = err; + return await rpcCall(rpcUrl, address, data) + } catch (error) { + lastError = error } } - throw lastErr ?? new Error('no rpcs configured'); + throw lastError ?? new Error('no rpcs configured') } -async function rpcCall(rpcUrl, to, data) { - const body = JSON.stringify({ jsonrpc: '2.0', method: 'eth_call', params: [{ to, data }, 'latest'], id: 1 }); - const res = await fetch(rpcUrl, { +async function rpcCall(rpcUrl, address, data) { + const payload = await fetchJson(rpcUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body, - signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), - }); - if (!res.ok) throw new Error(`rpc HTTP ${res.status}`); - const json = await res.json(); - if (json.error) throw new Error(json.error.message ?? JSON.stringify(json.error)); - return json.result; + body: JSON.stringify({ + jsonrpc: '2.0', + method: 'eth_call', + params: [{ to: address, data }, 'latest'], + id: 1, + }), + }) + if (payload.error) { + throw new Error(payload.error.message ?? JSON.stringify(payload.error)) + } + return payload.result } function decodeUint(hex) { // Some RPCs (notably zkSync) return `0x` for calls to nonexistent contracts // instead of erroring; guard explicitly so we never write `NaN/N` into docs. - if (!hex || hex.length < 2 + 64) throw new Error(`empty rpc result: ${hex}`); - return parseInt(hex.slice(2), 16); + if (!hex || hex.length < 2 + 64) throw new Error(`empty rpc result: ${hex}`) + return Number.parseInt(hex.slice(2), 16) } function decodeArrayLength(hex) { // ABI-encoded address[]: offset(32) + length(32) + entries... // For a single-arg dynamic return, the offset word is always 0x20. - if (!hex || hex.length < 2 + 128) throw new Error('not an address[] response'); - const offset = parseInt(hex.slice(2, 2 + 64), 16); - if (offset !== 0x20) throw new Error(`unexpected address[] offset: 0x${offset.toString(16)}`); - return parseInt(hex.slice(2 + 64, 2 + 128), 16); + if (!hex || hex.length < 2 + 128) throw new Error('not an address[] response') + const offset = Number.parseInt(hex.slice(2, 2 + 64), 16) + if (offset !== 0x20) throw new Error(`unexpected address[] offset: 0x${offset.toString(16)}`) + return Number.parseInt(hex.slice(2 + 64, 2 + 128), 16) } -// --------------------------------------------------------------------------- // Markdown scanners — yield `{ lineNo, chain, address, current, write(value) }` -// --------------------------------------------------------------------------- -function splitTableRow(line) { - const t = line.trim(); - if (!t.startsWith('|') || !t.endsWith('|')) return null; - return t.slice(1, -1).split('|').map((c) => c.trim()); -} - -function rebuildTableRow(line, cells) { - // Preserve the original line's pipe positions and whitespace padding. - const m = line.match(/^(\s*\|)(.*)(\|\s*)$/); - if (!m) return line; - const segments = m[2].split('|'); - if (segments.length !== cells.length) return line; - const inner = segments - .map((seg, idx) => seg.replace(/^(\s*).*?(\s*)$/, `$1${cells[idx]}$2`)) - .join('|'); - return `${m[1]}${inner}${m[3]}`; -} - function findSafeLink(text) { - const m = text.match(SAFE_LINK_RE); - return m && { chain: m[1], address: m[2] }; + const match = text.match(SAFE_LINK_RE) + return match && { chain: match[1].toLowerCase(), address: match[2] } } function* scanTables(lines) { - for (let i = 0; i < lines.length - 1; i++) { - const head = splitTableRow(lines[i]); - const sep = splitTableRow(lines[i + 1]); - if (!head || !sep || !sep.every((c) => TABLE_SEP_CELL_RE.test(c))) continue; - const qIdx = head.findIndex((h) => QUORUM_HEADER_RE.test(h)); - if (qIdx === -1) continue; - - let j = i + 2; - while (j < lines.length) { - const row = splitTableRow(lines[j]); - if (!row || row.length !== head.length) break; - const link = row.map(findSafeLink).find(Boolean); + for (const { headers, rows } of scanMarkdownTables(lines)) { + const quorumColumn = headers.findIndex((header) => QUORUM_HEADER_RE.test(header)) + if (quorumColumn === -1) continue + + for (const { cells: row, lineIndex } of rows) { + const link = row.map(findSafeLink).find(Boolean) if (link) { - const lineNo = j; + const lineNo = lineIndex yield { lineNo, ...link, - current: row[qIdx], + current: row[quorumColumn], write: (value) => { - const next = [...row]; - next[qIdx] = value; - lines[lineNo] = rebuildTableRow(lines[lineNo], next); + const updatedRow = [...row] + while (updatedRow.length <= quorumColumn) updatedRow.push('') + updatedRow[quorumColumn] = value + lines[lineNo] = rebuildTableRow(lines[lineNo], updatedRow) }, - }; + } } - j++; } - i = j - 1; } } function* scanInline(lines) { - let pending = null; - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (HEADING_RE.test(line)) { pending = null; continue; } - - const link = findSafeLink(line); - if (link) pending = link; - - const m = line.match(INLINE_QUORUM_RE); - if (m && pending) { - const lineNo = i; - const safe = pending; + let pending = null + for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) { + const line = lines[lineIndex] + if (HEADING_RE.test(line)) { + pending = null + continue + } + + const link = findSafeLink(line) + if (link) pending = link + + const quorum = line.match(INLINE_QUORUM_RE) + if (quorum && pending) { + const lineNo = lineIndex + const safe = pending // Consume the pairing — a later orphan `**Quorum:**` must require its own // Safe link rather than re-pairing with this one. - pending = null; + pending = null yield { lineNo, ...safe, - current: m[1].replace(/\s+/g, ''), + current: quorum[1].replace(/\s+/g, ''), // Tie the replacement to the `**Quorum:**` label so an unrelated `M/N` // elsewhere on the line cannot be clobbered. write: (value) => { - lines[lineNo] = lines[lineNo].replace(INLINE_QUORUM_REPLACE_RE, `$1${value}`); + lines[lineNo] = lines[lineNo].replace(INLINE_QUORUM_REPLACE_RE, `$1${value}`) }, - }; + } } } } function* scanQuorumSites(lines) { - yield* scanTables(lines); - yield* scanInline(lines); + yield* scanTables(lines) + yield* scanInline(lines) } -// --------------------------------------------------------------------------- -// Per-file processing -// --------------------------------------------------------------------------- -async function processFile(file, onFileDone) { - const original = fs.readFileSync(file, 'utf8'); - const lines = original.split('\n'); - const rel = path.relative(ROOT, file); - +async function updateQuorums(markdown, getQuorum) { + const lines = markdown.split('\n') const checks = await Promise.all( [...scanQuorumSites(lines)].map(async (site) => { + const { write, ...check } = site try { - const onchain = await fetchQuorum(site.chain, site.address); - if (site.current === onchain) return { ...site, status: 'ok', onchain }; - site.write(onchain); - return { ...site, status: 'drift', onchain }; - } catch (err) { - return { ...site, status: 'error', message: err.message }; + const onchain = await getQuorum(site.chain, site.address) + if (site.current === onchain) return { ...check, status: 'ok', onchain } + write(onchain) + return { ...check, status: 'drift', onchain } + } catch (error) { + return { ...check, status: 'error', message: error.message } } }), - ); + ) // Preserve source order — `Promise.all` already returns results in input // order, but sites came from two scanners (table + inline) which may not be // sorted by lineNo overall. - checks.sort((a, b) => a.lineNo - b.lineNo); + checks.sort((a, b) => a.lineNo - b.lineNo) - onFileDone({ rel, checks }); - return { rel, checks, content: lines.join('\n'), original }; + return { checks, content: lines.join('\n') } +} + +async function processFile(file, onFileChecked) { + const original = fs.readFileSync(file, 'utf8') + const relativePath = path.relative(ROOT, file) + const { checks, content } = await updateQuorums(original, fetchQuorum) + + onFileChecked({ relativePath, checks }) + return { relativePath, content, original } } -// --------------------------------------------------------------------------- -// File discovery -// --------------------------------------------------------------------------- function* walkMarkdown(dir) { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { - if (entry.name.startsWith('.') || entry.name === 'node_modules') continue; - const p = path.join(dir, entry.name); - if (entry.isDirectory()) yield* walkMarkdown(p); - else if (entry.name.endsWith('.md')) yield p; + if (entry.name.startsWith('.') || entry.name === 'node_modules') continue + const entryPath = path.join(dir, entry.name) + if (entry.isDirectory()) yield* walkMarkdown(entryPath) + else if (entry.name.endsWith('.md')) yield entryPath } } function discoverDocFiles() { - return DOC_DIRS - .map((d) => path.join(ROOT, d)) - .filter((p) => fs.existsSync(p)) - .flatMap((p) => [...walkMarkdown(p)]); + return DOC_DIRS.map((directory) => path.join(ROOT, directory)) + .filter((directory) => fs.existsSync(directory)) + .flatMap((directory) => [...walkMarkdown(directory)]) +} + +const SYMBOL = { ok: '✓', drift: '✗', error: '!' } + +function formatCheckDetails(check) { + if (check.status === 'drift') return `${check.current} → ${check.onchain}` + if (check.status === 'error') { + return `${check.current ?? '?'} (${check.message})` + } + return check.current } -// --------------------------------------------------------------------------- -// Main -// --------------------------------------------------------------------------- -const SYMBOL = { ok: '✓', drift: '✗', error: '!' }; - -function formatCheck(c, refWidth) { - const line = `L${String(c.lineNo + 1).padStart(5)}`; - const ref = `${c.chain}:${c.address}`.padEnd(refWidth); - const tail = - c.status === 'drift' ? `${c.current} → ${c.onchain}` : - c.status === 'error' ? `${c.current ?? '?'} (${c.message})` : - c.current; - return ` ${SYMBOL[c.status]} ${line} ${ref} ${tail}`; +function formatCheck(check, referenceWidth) { + const line = `L${String(check.lineNo + 1).padStart(5)}` + const reference = `${check.chain}:${check.address}`.padEnd(referenceWidth) + return ` ${SYMBOL[check.status]} ${line} ${reference} ${formatCheckDetails(check)}` } -async function main() { - const totals = { ok: 0, drift: 0, error: 0 }; +async function run() { + const totals = { ok: 0, drift: 0, error: 0 } // Reserve enough width for the longest configured `chain:0x…` reference so // columns align even when short and long prefixes mix on a single page. - const longestChain = Object.keys(CHAIN_RPCS).reduce((a, b) => (b.length > a.length ? b : a), ''); - const REF_WIDTH = `${longestChain}:0x${'0'.repeat(40)}`.length; - - const onFileDone = ({ rel, checks }) => { - if (checks.length === 0) return; - console.log(`\n${rel}`); - for (const c of checks) { - totals[c.status]++; - console.log(formatCheck(c, REF_WIDTH)); + const longestChain = Object.keys(CHAIN_RPCS).reduce((a, b) => (b.length > a.length ? b : a), '') + const referenceWidth = `${longestChain}:0x${'0'.repeat(40)}`.length + + const onFileChecked = ({ relativePath, checks }) => { + if (checks.length === 0) return + console.log(`\n${relativePath}`) + for (const check of checks) { + totals[check.status] += 1 + console.log(formatCheck(check, referenceWidth)) } - }; + } - const files = discoverDocFiles(); - const results = await Promise.all(files.map((f) => processFile(f, onFileDone))); + const files = discoverDocFiles() + const results = await Promise.all(files.map((file) => processFile(file, onFileChecked))) - for (const { rel, content, original } of results) { - if (content !== original) fs.writeFileSync(path.join(ROOT, rel), content); + for (const { relativePath, content, original } of results) { + if (content !== original) fs.writeFileSync(path.join(ROOT, relativePath), content) } - const total = totals.ok + totals.drift + totals.error; - console.log(`\n${total} checked: ${totals.ok} ok, ${totals.drift} drift, ${totals.error} error`); + const total = totals.ok + totals.drift + totals.error + console.log(`\n${total} checked: ${totals.ok} ok, ${totals.drift} drift, ${totals.error} error`) } -main().catch((err) => { - console.error(err); - process.exit(1); -}); +if (require.main === module) runTask(run) + +module.exports = { + createQuorumFetcher, + decodeArrayLength, + decodeUint, + findSafeLink, + formatCheck, + rebuildTableRow, + resolveQuorum, + rpcCall, + rpcCallWithFallback, + run, + scanQuorumSites, + splitTableRow, + updateQuorums, +} diff --git a/scripts/fetch.js b/scripts/fetch.js new file mode 100644 index 000000000..817e8a9b4 --- /dev/null +++ b/scripts/fetch.js @@ -0,0 +1,34 @@ +#!/usr/bin/env node + +const { spawnSync } = require('node:child_process') +const path = require('node:path') + +const { runTask } = require('./lib/tasks') + +const TASKS = ['fetch-audits.js', 'fetch-lips.js', 'fetch-msig-quorums.js'] + +function runTasks(tasks = TASKS) { + for (const task of tasks) { + const scriptPath = path.resolve(__dirname, task) + console.log(`\n▶ running ${task} …`) + const { status } = spawnSync(process.execPath, [scriptPath], { + stdio: 'inherit', + }) + if (status !== 0) { + const exitCode = status ?? 1 + console.error(`✖ ${task} failed (exit ${exitCode}) — aborting`) + return exitCode + } + } + + console.log('\n✅ All fetch tasks completed successfully') + return 0 +} + +function run() { + return runTasks() +} + +if (require.main === module) runTask(run) + +module.exports = { TASKS, run, runTasks } diff --git a/scripts/lib/async.js b/scripts/lib/async.js new file mode 100644 index 000000000..f33bdb601 --- /dev/null +++ b/scripts/lib/async.js @@ -0,0 +1,26 @@ +function createLimiter(maxConcurrency) { + let active = 0 + const queue = [] + + const runNext = () => { + if (active >= maxConcurrency || queue.length === 0) return + + active += 1 + const { task, resolve, reject } = queue.shift() + Promise.resolve() + .then(task) + .then(resolve, reject) + .finally(() => { + active -= 1 + runNext() + }) + } + + return (task) => + new Promise((resolve, reject) => { + queue.push({ task, resolve, reject }) + runNext() + }) +} + +module.exports = { createLimiter } diff --git a/scripts/lib/http.js b/scripts/lib/http.js new file mode 100644 index 000000000..8f9e64440 --- /dev/null +++ b/scripts/lib/http.js @@ -0,0 +1,26 @@ +const REQUEST_TIMEOUT_MS = 15_000 + +function addRequestTimeout(options = {}) { + const timeoutSignal = AbortSignal.timeout(REQUEST_TIMEOUT_MS) + const signal = options.signal ? AbortSignal.any([options.signal, timeoutSignal]) : timeoutSignal + return { ...options, signal } +} + +async function fetchResponse(url, options) { + const response = await fetch(url, addRequestTimeout(options)) + if (!response.ok) { + if (response.body) await Promise.resolve(response.body.cancel()).catch(() => undefined) + throw new Error(`HTTP ${response.status} → ${url}`) + } + return response +} + +async function fetchText(url, options) { + return (await fetchResponse(url, options)).text() +} + +async function fetchJson(url, options) { + return (await fetchResponse(url, options)).json() +} + +module.exports = { fetchJson, fetchText } diff --git a/scripts/lib/markdown.js b/scripts/lib/markdown.js new file mode 100644 index 000000000..c9f4b3221 --- /dev/null +++ b/scripts/lib/markdown.js @@ -0,0 +1,189 @@ +const GITHUB_ALERT_TYPES = { + NOTE: 'note', + TIP: 'tip', + IMPORTANT: 'important', + WARNING: 'warning', + CAUTION: 'caution', +} + +const GITHUB_ALERT_PATTERN = /^ {0,3}>[ \t]*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\][ \t]*$/i +const BLOCKQUOTE_LINE_PATTERN = /^ {0,3}>[ \t]?(.*)$/ +const OPENING_FENCE_PATTERN = /^ {0,3}(`{3,}|~{3,})/ +const CLOSING_FENCE_PATTERN = /^ {0,3}(`+|~+)[ \t]*$/ +const TABLE_DELIMITER_CELL_PATTERN = /^:?-+:?$/ + +function splitMarkdownLines(markdown) { + return markdown.match(/[^\r\n]*(?:\r\n|\n|$)/g)?.filter(Boolean) || [] +} + +function parseMarkdownLine(rawLine) { + const newline = rawLine.match(/\r?\n$/)?.[0] || '' + const content = rawLine.slice(0, rawLine.length - newline.length) + return { content, newline } +} + +function findOpeningFence(content) { + return content.match(OPENING_FENCE_PATTERN)?.[1] || null +} + +function closesFence(content, openingFence) { + const closingFence = content.match(CLOSING_FENCE_PATTERN)?.[1] + return closingFence?.[0] === openingFence[0] && closingFence.length >= openingFence.length +} + +function findAlertType(content) { + const githubType = content.match(GITHUB_ALERT_PATTERN)?.[1] + return GITHUB_ALERT_TYPES[githubType?.toUpperCase()] || null +} + +function convertAlertBlock(rawLines, markerIndex, alertType) { + const markerLine = parseMarkdownLine(rawLines[markerIndex]) + const bodyLines = [] + let lastNewline = '' + let nextIndex = markerIndex + 1 + + while (nextIndex < rawLines.length) { + const line = parseMarkdownLine(rawLines[nextIndex]) + const blockquoteLine = line.content.match(BLOCKQUOTE_LINE_PATTERN) + if (!blockquoteLine) break + + bodyLines.push(blockquoteLine[1] + line.newline) + lastNewline = line.newline + nextIndex += 1 + } + + if (bodyLines.length === 0) return null + + const newlineBeforeClosingFence = lastNewline ? '' : markerLine.newline + const markdown = + `:::${alertType}${markerLine.newline}` + bodyLines.join('') + `${newlineBeforeClosingFence}:::${lastNewline}` + + return { markdown, nextIndex } +} + +function convertGithubAlerts(markdown) { + const lines = splitMarkdownLines(markdown) + const output = [] + let openFence = null + let index = 0 + + while (index < lines.length) { + const line = parseMarkdownLine(lines[index]) + + if (openFence) { + output.push(lines[index]) + if (closesFence(line.content, openFence)) openFence = null + index += 1 + continue + } + + openFence = findOpeningFence(line.content) + if (openFence) { + output.push(lines[index]) + index += 1 + continue + } + + const alertType = findAlertType(line.content) + const convertedAlert = alertType ? convertAlertBlock(lines, index, alertType) : null + if (convertedAlert) { + output.push(convertedAlert.markdown) + index = convertedAlert.nextIndex + continue + } + + output.push(lines[index]) + index += 1 + } + + return output.join('') +} + +function isEscaped(value, index) { + let backslashes = 0 + for (let cursor = index - 1; cursor >= 0 && value[cursor] === '\\'; cursor -= 1) backslashes += 1 + return backslashes % 2 === 1 +} + +function splitUnescapedPipes(value) { + const cells = [''] + for (let index = 0; index < value.length; index += 1) { + if (value[index] === '|' && !isEscaped(value, index)) cells.push('') + else cells[cells.length - 1] += value[index] + } + return cells +} + +function splitTableRow(line) { + const trimmed = line.trim() + const cells = splitUnescapedPipes(trimmed) + if (cells.length === 1) return null + + if (trimmed.startsWith('|')) cells.shift() + if (trimmed.endsWith('|') && !isEscaped(trimmed, trimmed.length - 1)) cells.pop() + if (cells.length === 0) return null + + return cells.map((cell) => cell.trim().replace(/\\\|/g, '|')) +} + +function rebuildTableRow(line, cells) { + const leadingWhitespace = line.match(/^\s*/)[0] + const trailingWhitespace = line.match(/\s*$/)[0] + let content = line.slice(leadingWhitespace.length, line.length - trailingWhitespace.length) + const leadingPipe = content.startsWith('|') + const trailingPipe = content.endsWith('|') && !isEscaped(content, content.length - 1) + + if (leadingPipe) content = content.slice(1) + if (trailingPipe) content = content.slice(0, -1) + + const segments = splitUnescapedPipes(content) + if (segments.length > cells.length) return line + while (segments.length < cells.length) segments.push(' ') + + const rebuilt = segments + .map((segment, index) => { + const cell = String(cells[index]).replace(/\|/g, '\\|') + if (segment.trim() === '') return ` ${cell} ` + return segment.replace(/^(\s*).*?(\s*)$/, (_match, leading, trailing) => leading + cell + trailing) + }) + .join('|') + + return leadingWhitespace + (leadingPipe ? '|' : '') + rebuilt + (trailingPipe ? '|' : '') + trailingWhitespace +} + +function parseTableHeader(lines, headerIndex) { + const headers = splitTableRow(lines[headerIndex]) + const delimiters = splitTableRow(lines[headerIndex + 1] || '') + if (!headers || !delimiters || headers.length !== delimiters.length) return null + if (!delimiters.every((cell) => TABLE_DELIMITER_CELL_PATTERN.test(cell))) return null + return headers +} + +function parseMarkdownTable(lines, headerIndex) { + const headers = parseTableHeader(lines, headerIndex) + if (!headers) return null + + const rows = [] + let lineIndex = headerIndex + 2 + while (lineIndex < lines.length) { + if (parseTableHeader(lines, lineIndex)) break + const cells = splitTableRow(lines[lineIndex]) + if (!cells) break + rows.push({ cells, lineIndex }) + lineIndex += 1 + } + + return { headers, rows, nextLineIndex: lineIndex } +} + +function* scanMarkdownTables(lines) { + for (let lineIndex = 0; lineIndex < lines.length - 1; lineIndex += 1) { + const table = parseMarkdownTable(lines, lineIndex) + if (!table) continue + + yield table + lineIndex = table.nextLineIndex - 1 + } +} + +module.exports = { convertGithubAlerts, parseMarkdownTable, rebuildTableRow, scanMarkdownTables, splitTableRow } diff --git a/scripts/lib/output.js b/scripts/lib/output.js new file mode 100644 index 000000000..e420ff9e4 --- /dev/null +++ b/scripts/lib/output.js @@ -0,0 +1,8 @@ +function printCounts(counts) { + console.log('\nCategory counts:') + for (const [label, count] of Object.entries(counts)) { + console.log(` • ${label}: ${count}`) + } +} + +module.exports = { printCounts } diff --git a/scripts/lib/tasks.js b/scripts/lib/tasks.js new file mode 100644 index 000000000..85fe88c66 --- /dev/null +++ b/scripts/lib/tasks.js @@ -0,0 +1,15 @@ +function runTask(task) { + return Promise.resolve() + .then(task) + .then((exitCode) => { + if (Number.isInteger(exitCode) && exitCode !== 0) { + process.exitCode = exitCode + } + }) + .catch((error) => { + console.error(error) + process.exitCode = 1 + }) +} + +module.exports = { runTask } diff --git a/tests/async.test.js b/tests/async.test.js new file mode 100644 index 000000000..215ae3990 --- /dev/null +++ b/tests/async.test.js @@ -0,0 +1,40 @@ +const test = require('node:test') +const assert = require('node:assert/strict') + +const { createLimiter } = require('../scripts/lib/async') + +test('limits concurrent tasks and continues after a rejection', async () => { + const releases = [] + let active = 0 + let highestActive = 0 + + const limit = createLimiter(2) + const tasks = ['first', 'failed', 'last'].map((name) => + limit( + () => + new Promise((resolve, reject) => { + active += 1 + highestActive = Math.max(highestActive, active) + releases.push(() => { + active -= 1 + if (name === 'failed') reject(new Error(name)) + else resolve(name) + }) + }), + ), + ) + + await new Promise(setImmediate) + assert.equal(releases.length, 2) + releases.shift()() + await new Promise(setImmediate) + assert.equal(releases.length, 2) + for (const release of releases) release() + + assert.deepEqual(await Promise.allSettled(tasks), [ + { status: 'fulfilled', value: 'first' }, + { status: 'rejected', reason: new Error('failed') }, + { status: 'fulfilled', value: 'last' }, + ]) + assert.equal(highestActive, 2) +}) diff --git a/tests/fetch-audits.test.js b/tests/fetch-audits.test.js new file mode 100644 index 000000000..023863bc3 --- /dev/null +++ b/tests/fetch-audits.test.js @@ -0,0 +1,229 @@ +const test = require('node:test') +const assert = require('node:assert/strict') + +const { preprocessMarkdown, sortAuditsAndCount } = require('../scripts/fetch-audits') + +const alertTypes = { + NOTE: 'note', + TIP: 'tip', + IMPORTANT: 'important', + WARNING: 'warning', + CAUTION: 'caution', +} + +for (const [githubType, docusaurusType] of Object.entries(alertTypes)) { + test(`converts GitHub ${githubType} to Docusaurus ${docusaurusType}`, () => { + const input = `> [!${githubType}]\n> Message.\n` + const expected = `:::${docusaurusType}\nMessage.\n:::\n` + + assert.equal(preprocessMarkdown(input), expected) + }) +} + +test('preserves content around a multiline alert', () => { + const input = `Before + +> [!WARNING] +> First line. +> +> Second line with a [link](https://example.com). + +After` + + const expected = `Before + +:::warning +First line. + +Second line with a [link](https://example.com). +::: + +After` + + assert.equal(preprocessMarkdown(input), expected) +}) + +test('converts an alert at the end of the file', () => { + const input = '> [!WARNING] \n> Take care.' + const expected = ':::warning\nTake care.\n:::' + + assert.equal(preprocessMarkdown(input), expected) +}) + +test('preserves Markdown and nested quotes inside an alert', () => { + const input = `> [!WARNING] +> **Before continuing:** +> +> - Check the first item. +> - Check the second item. +> +> > Keep this as a nested quote. +` + const expected = `:::warning +**Before continuing:** + +- Check the first item. +- Check the second item. + +> Keep this as a nested quote. +::: +` + + assert.equal(preprocessMarkdown(input), expected) +}) + +test('converts multiple alert types independently', () => { + const input = `> [!NOTE] +> First. + +Between. + +> [!CAUTION] +> Second. +` + const expected = `:::note +First. +::: + +Between. + +:::caution +Second. +::: +` + + assert.equal(preprocessMarkdown(input), expected) +}) + +test('preserves CRLF line endings in a converted alert', () => { + const input = '> [!TIP]\r\n> First.\r\n>\r\n> Second.\r\n\r\nAfter\r\n' + const expected = ':::tip\r\nFirst.\r\n\r\nSecond.\r\n:::\r\n\r\nAfter\r\n' + + assert.equal(preprocessMarkdown(input), expected) +}) + +test('accepts valid blockquote whitespace and lowercase alert types', () => { + const input = ` >[!warning] + >First line. + >\tSecond line. +` + const expected = `:::warning +First line. +Second line. +::: +` + + assert.equal(preprocessMarkdown(input), expected) +}) + +test('does not change ordinary blockquotes or unknown GitHub alerts', () => { + const input = `> Ordinary quote. + +> [!QUESTION] +> A question. +` + + assert.equal(preprocessMarkdown(input), input) +}) + +test('does not change alert syntax inside fenced code blocks', () => { + const backticks = `\`\`\`md +> [!IMPORTANT] +> Example only. +\`\`\` +` + const tildes = `~~~md +> [!CAUTION] +> Example only. +~~~ +` + + assert.equal(preprocessMarkdown(backticks), backticks) + assert.equal(preprocessMarkdown(tildes), tildes) +}) + +test('does not convert an alert marker without a quoted body', () => { + const input = '> [!IMPORTANT]\nUnquoted text.\n' + + assert.equal(preprocessMarkdown(input), input) +}) + +test('rewrites relative PDF and docs.lido.fi links', () => { + const input = [ + '[Report](reports/audit.pdf)', + '[Guide](https://docs.lido.fi/guides/example)', + '[Renamed](https://docs.lido.fi/token-guides/wsteth-bridging-guide#the-proposed-configuration)', + '[External](https://example.com/report.pdf)', + ].join('\n') + + const expected = [ + '[Report](https://github.com/lidofinance/audits/blob/main/reports/audit.pdf)', + '[Guide](/guides/example)', + '[Renamed](/token-guides/cross-chain-tokens-guide#mainnet-proposed-configuration)', + '[External](https://example.com/report.pdf)', + ].join('\n') + + assert.equal(preprocessMarkdown(input), expected) +}) + +test('sorts reports by date and refreshes section counts', () => { + const input = `Introduction. + +## Reports (99 reports) + +Section preamble. + +### 01-2024 Older + +Older report. + +### 12-2025 Newer + +Newer report. + +## Lido Earn + +### 01-2025 Separate report +` + + const { content, counts } = sortAuditsAndCount(input) + + assert.deepEqual(counts, { '## Reports': 2 }) + assert.match(content, /## Reports \(2 reports\)/) + assert.ok(content.indexOf('### 12-2025 Newer') < content.indexOf('### 01-2024 Older')) + assert.match(content, /Section preamble\./) +}) + +test('omits audit sections maintained on separate pages', () => { + const input = `# Audits + +## Lido Earn + +### 03-2025 Earn report + +Hidden here. + +## Protocol + +### 04-2025 Protocol report + +Visible here. +` + + const { content, counts } = sortAuditsAndCount(input) + + assert.doesNotMatch(content, /Lido Earn|Earn report|Hidden here/) + assert.match(content, /## Protocol \(1 reports\)/) + assert.deepEqual(counts, { '## Protocol': 1 }) +}) + +test('fails when the separately maintained audit section is missing', () => { + const input = `# Audits + +## Protocol + +### 04-2025 Protocol report +` + + assert.throws(() => sortAuditsAndCount(input), /required audit section not found: ## Lido Earn/) +}) diff --git a/tests/fetch-lips.test.js b/tests/fetch-lips.test.js new file mode 100644 index 000000000..1031ae183 --- /dev/null +++ b/tests/fetch-lips.test.js @@ -0,0 +1,241 @@ +const test = require('node:test') +const assert = require('node:assert/strict') + +const { + buildLipDocument, + escapeTableCell, + fetchLip, + normalizeStatus, + parseLip, + parseLipNumber, + parseMarkdownTableHeader, + parseMetadata, + parseYamlFrontMatter, + renderDiscussionLinks, + renderTable, +} = require('../scripts/fetch-lips') + +test('parses YAML front matter', () => { + const markdown = `--- +LIP: 42 +Title: "Upgrade: phase two" +Status: Review +--- + +# Proposal +` + + assert.deepEqual(parseYamlFrontMatter(markdown), { + lip: 42, + title: 'Upgrade: phase two', + status: 'Review', + }) + assert.equal(parseYamlFrontMatter('# No metadata\n'), null) +}) + +test('parses the legacy metadata table', () => { + const markdown = `| LIP | Title | Status | Discussions-to | +| --- | --- | --- | --- | +| 7 | Staking router | Final | https://example.com/topic | + +# Proposal +` + + assert.deepEqual(parseMarkdownTableHeader(markdown), { + lip: '7', + title: 'Staking router', + status: 'Final', + 'discussions-to': 'https://example.com/topic', + }) +}) + +test('only treats a leading table as legacy metadata', () => { + const markdown = `# Proposal + +This table belongs to the proposal body. + +| LIP | Title | Status | Author | +| --- | --- | --- | --- | +| 999 | Fabricated metadata | Final | Nobody | +` + + assert.equal(parseMarkdownTableHeader(markdown), null) + assert.deepEqual(parseMetadata(markdown), {}) + assert.equal(parseLip('invalid.md', markdown), null) +}) + +test('validates legacy table separators and preserves empty cells', () => { + const malformed = `| LIP | Title | +| --- | title | +| 7 | Invalid | +` + const withEmptyCell = `| LIP | Title | Status | +| --- | --- | --- | +| 7 | | Final | +` + + assert.equal(parseMarkdownTableHeader(malformed), null) + assert.deepEqual(parseMarkdownTableHeader(withEmptyCell), { + lip: '7', + title: '', + status: 'Final', + }) +}) + +test('accepts GFM tables without outer pipes and with short delimiters', () => { + const markdown = `LIP | Title | Status +- | - | -: +7 | Legacy proposal | Final +` + + assert.deepEqual(parseMarkdownTableHeader(markdown), { + lip: '7', + title: 'Legacy proposal', + status: 'Final', + }) +}) + +test('prefers YAML metadata over the legacy table', () => { + const markdown = `--- +lip: 2 +title: YAML title +--- + +| LIP | Title | +| --- | --- | +| 3 | Table title | +` + + assert.deepEqual(parseMetadata(markdown), { + lip: 2, + title: 'YAML title', + }) + assert.deepEqual(parseMetadata('# No metadata\n'), {}) +}) + +test('normalizes LIP numbers', () => { + assert.equal(parseLipNumber(0), 0) + assert.equal(parseLipNumber('LIP-0042'), 42) + assert.equal(parseLipNumber('proposal 7 draft'), 7) + assert.equal(parseLipNumber('missing'), null) +}) + +test('maps status aliases to canonical sections', () => { + const cases = { + draft: 'WIP', + discussion: 'Proposed', + voted: 'Approved', + final: 'Implemented', + declined: 'Rejected', + withdrawn: 'Withdrawn', + deferred: 'Deferred', + deprecated: 'Moribund', + unknown: 'WIP', + } + + for (const [raw, expected] of Object.entries(cases)) { + assert.equal(normalizeStatus(raw), expected) + } +}) + +test('escapes table content', () => { + assert.equal(escapeTableCell('first | second\nthird'), 'first | second third') +}) + +test('renders discussion links', () => { + assert.equal(renderDiscussionLinks(''), 'None') + assert.equal(renderDiscussionLinks('null'), 'None') + assert.equal(renderDiscussionLinks('https://one.example'), '[Link](https://one.example)') + assert.equal( + renderDiscussionLinks('https://one.example, https://two.example'), + '[Link 1](https://one.example), [Link 2](https://two.example)', + ) +}) + +test('renders a LIP table row', () => { + const table = renderTable([ + { + number: 42, + link: 'https://example.com/lip-42', + title: 'A | B', + author: 'Alice\nBob', + discussion: 'https://example.com/discussion', + }, + ]) + + assert.match(table, /\| LIP # \| Title \| Author \| Discussions‑to \|/) + assert.match( + table, + /\| \[42\]\(https:\/\/example\.com\/lip-42\) \| A | B \| Alice Bob \| \[Link\]\(https:\/\/example\.com\/discussion\) \|/, + ) +}) + +test('parses a LIP entry using its source file name', () => { + const markdown = `--- +lip: LIP-42 +title: Test proposal +author: Alice +status: final +discussions-to: https://example.com/topic +--- +` + + assert.deepEqual(parseLip('custom-name.md', markdown), { + number: 42, + status: 'Implemented', + title: 'Test proposal', + author: 'Alice', + discussion: 'https://example.com/topic', + link: 'https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/custom-name.md', + }) + assert.equal(parseLip('invalid.md', '# Missing metadata\n'), null) + assert.equal(parseLip('malformed.md', '---\nlip: [\n---\n'), null) +}) + +test('warns and skips a malformed LIP file', async (context) => { + const warnings = [] + context.mock.method(global, 'fetch', async () => ({ + ok: true, + text: async () => '# Missing metadata\n', + })) + context.mock.method(console, 'warn', (...parts) => warnings.push(parts.join(' '))) + + assert.equal(await fetchLip({ name: 'broken.md' }), null) + assert.deepEqual(warnings, ['⚠️ skipping malformed: broken.md']) +}) + +test('groups LIPs by status and sorts them newest first', () => { + const lips = [ + { + number: 1, + status: 'WIP', + title: 'First', + author: 'Alice', + discussion: '', + link: 'https://example.com/1', + }, + { + number: 3, + status: 'WIP', + title: 'Third', + author: 'Bob', + discussion: '', + link: 'https://example.com/3', + }, + { + number: 2, + status: 'Approved', + title: 'Second', + author: 'Carol', + discussion: '', + link: 'https://example.com/2', + }, + ] + + const { content, counts } = buildLipDocument(lips) + + assert.deepEqual(counts, { WIP: 2, Approved: 1 }) + assert.ok(content.indexOf('[3]') < content.indexOf('[1]')) + assert.ok(content.indexOf('## WIP') < content.indexOf('## Approved')) + assert.doesNotMatch(content, /## Proposed/) +}) diff --git a/tests/fetch-msig-quorums.test.js b/tests/fetch-msig-quorums.test.js new file mode 100644 index 000000000..c72bb6471 --- /dev/null +++ b/tests/fetch-msig-quorums.test.js @@ -0,0 +1,321 @@ +const { afterEach, test } = require('node:test') +const assert = require('node:assert/strict') + +const { + createQuorumFetcher, + decodeArrayLength, + decodeUint, + findSafeLink, + formatCheck, + rebuildTableRow, + resolveQuorum, + rpcCall, + rpcCallWithFallback, + scanQuorumSites, + splitTableRow, + updateQuorums, +} = require('../scripts/fetch-msig-quorums') + +const ADDRESS = `0x${'a'.repeat(40)}` +const originalFetch = global.fetch + +afterEach(() => { + global.fetch = originalFetch +}) + +function abiWord(value) { + return BigInt(value).toString(16).padStart(64, '0') +} + +test('decodes Safe ABI results', () => { + assert.equal(decodeUint(`0x${abiWord(3)}`), 3) + assert.equal(decodeArrayLength(`0x${abiWord(32)}${abiWord(5)}`), 5) +}) + +test('rejects malformed Safe ABI results', () => { + assert.throws(() => decodeUint('0x'), /empty rpc result/) + assert.throws(() => decodeArrayLength('0x'), /not an address\[\] response/) + assert.throws(() => decodeArrayLength(`0x${abiWord(64)}${abiWord(2)}`), /unexpected address\[\] offset/) +}) + +test('sends JSON-RPC eth_call requests', async () => { + const requests = [] + global.fetch = async (url, options) => { + requests.push({ url, options }) + return { ok: true, json: async () => ({ result: '0xresult' }) } + } + + assert.equal(await rpcCall('https://rpc.example', ADDRESS, '0xselector'), '0xresult') + assert.equal(requests.length, 1) + assert.equal(requests[0].url, 'https://rpc.example') + assert.deepEqual(JSON.parse(requests[0].options.body), { + jsonrpc: '2.0', + method: 'eth_call', + params: [{ to: ADDRESS, data: '0xselector' }, 'latest'], + id: 1, + }) +}) + +test('reports JSON-RPC errors', async () => { + global.fetch = async () => ({ + ok: true, + json: async () => ({ error: { message: 'execution reverted' } }), + }) + + await assert.rejects(rpcCall('https://rpc.example', ADDRESS, '0xselector'), /execution reverted/) +}) + +test('falls back to the next RPC endpoint', async () => { + const calls = [] + global.fetch = async (url) => { + calls.push(url) + if (url === 'https://first.example') return { ok: false, status: 503 } + return { ok: true, json: async () => ({ result: '0xresult' }) } + } + + const result = await rpcCallWithFallback(['https://first.example', 'https://second.example'], ADDRESS, '0xselector') + + assert.equal(result, '0xresult') + assert.deepEqual(calls, ['https://first.example', 'https://second.example']) +}) + +test('reports exhaustion of RPC fallbacks', async () => { + global.fetch = async () => ({ ok: false, status: 503 }) + + await assert.rejects(rpcCallWithFallback(['https://first.example'], ADDRESS, '0xselector'), /HTTP 503/) + await assert.rejects(rpcCallWithFallback([], ADDRESS, '0xselector'), /no rpcs configured/) +}) + +test('resolves and validates an on-chain quorum', async () => { + global.fetch = async (url, options) => { + const { data } = JSON.parse(options.body).params[0] + const result = data === '0xe75235b8' ? `0x${abiWord(2)}` : `0x${abiWord(32)}${abiWord(3)}` + return { ok: true, json: async () => ({ result }) } + } + + assert.equal(await resolveQuorum('eth', ADDRESS), '2/3') + await assert.rejects(resolveQuorum('unknown', ADDRESS), /unsupported chain/) + + global.fetch = async (url, options) => { + const { data } = JSON.parse(options.body).params[0] + const result = data === '0xe75235b8' ? `0x${abiWord(4)}` : `0x${abiWord(32)}${abiWord(3)}` + return { ok: true, json: async () => ({ result }) } + } + await assert.rejects(resolveQuorum('eth', ADDRESS), /invalid quorum: 4\/3/) +}) + +test('finds Safe references in arbitrary URLs', () => { + assert.deepEqual(findSafeLink(`https://safe.example/app?tab=home&safe=ETH:${ADDRESS}`), { + chain: 'eth', + address: ADDRESS, + }) + assert.equal(findSafeLink('https://example.com'), null) +}) + +test('splits and rebuilds Markdown table rows', () => { + const source = '| Council | 2/3 | Active |' + + assert.deepEqual(splitTableRow(source), ['Council', '2/3', 'Active']) + assert.deepEqual(splitTableRow('Council | 2/3'), ['Council', '2/3']) + assert.deepEqual(splitTableRow('| Council \\| backup | 2/3 |'), ['Council | backup', '2/3']) + assert.equal(rebuildTableRow(source, ['Council', '3/5', 'Active']), '| Council | 3/5 | Active |') + assert.equal(rebuildTableRow('| $1 | $& | 2/3 |', ['$1', '$&', '3/5']), '| $1 | $& | 3/5 |') +}) + +test('finds and updates quorum cells in Markdown tables', () => { + const lines = [ + '| Safe | Quorum | Notes |', + '| --- | :---: | --- |', + `| [Council](https://app.safe.global/home?safe=eth:${ADDRESS}) | 2/3 | Active |`, + '', + ] + + const sites = [...scanQuorumSites(lines)] + + assert.equal(sites.length, 1) + assert.deepEqual( + { + lineNo: sites[0].lineNo, + chain: sites[0].chain, + address: sites[0].address, + current: sites[0].current, + }, + { lineNo: 2, chain: 'eth', address: ADDRESS, current: '2/3' }, + ) + + sites[0].write('3/5') + assert.match(lines[2], /\| 3\/5 \|/) +}) + +test('does not consume an adjacent Markdown table', async () => { + const secondAddress = `0x${'b'.repeat(40)}` + const secondSafe = `https://app.safe.global/home?safe=eth:${secondAddress}` + const markdown = [ + '| Safe | Quorum |', + '| --- | --- |', + `| [Council](https://app.safe.global/home?safe=eth:${ADDRESS}) | 1/2 |`, + '| Name | Safe |', + '| --- | --- |', + `| Treasury | [Safe](${secondSafe}) |`, + ].join('\n') + + const requestedAddresses = [] + const { checks, content } = await updateQuorums(markdown, async (_chain, address) => { + requestedAddresses.push(address) + return '2/3' + }) + + assert.deepEqual(requestedAddresses, [ADDRESS]) + assert.equal(checks.length, 1) + assert.match(content, /\| \[Council\].*\| 2\/3 \|/) + assert.equal(content.split('\n').at(-1), `| Treasury | [Safe](${secondSafe}) |`) +}) + +test('repairs a short table row without recurring drift', async () => { + const markdown = [ + '| Safe | Notes | Quorum |', + '| --- | --- | --- |', + `| [Council](https://app.safe.global/home?safe=eth:${ADDRESS}) | Active |`, + ].join('\n') + + const first = await updateQuorums(markdown, async () => '2/3') + const second = await updateQuorums(first.content, async () => '2/3') + + assert.equal(first.checks[0].status, 'drift') + assert.match(first.content, /\| Active \| 2\/3 \|$/) + assert.equal(second.checks[0].status, 'ok') + assert.equal(second.content, first.content) +}) + +test('pairs an inline quorum with the preceding Safe link once', () => { + const lines = [`Safe: https://app.safe.global/home?safe=eth:${ADDRESS}`, '**Quorum:** 2 / 4', '**Quorum:** 1/2'] + + const sites = [...scanQuorumSites(lines)] + + assert.equal(sites.length, 1) + assert.equal(sites[0].current, '2/4') + sites[0].write('3/4') + assert.equal(lines[1], '**Quorum:** 3/4') + assert.equal(lines[2], '**Quorum:** 1/2') +}) + +test('does not pair inline quorums across headings', () => { + const lines = [`Safe: https://app.safe.global/home?safe=eth:${ADDRESS}`, '## Another multisig', '**Quorum:** 2/4'] + + assert.deepEqual([...scanQuorumSites(lines)], []) +}) + +test('formats quorum check results', () => { + const base = { lineNo: 4, chain: 'eth', address: ADDRESS, current: '2/3' } + + assert.match(formatCheck({ ...base, status: 'ok' }, 50), /✓ L\s+5.*2\/3$/) + assert.match(formatCheck({ ...base, status: 'drift', onchain: '3/5' }, 50), /✗ L\s+5.*2\/3 → 3\/5$/) + assert.match(formatCheck({ ...base, status: 'error', message: 'offline' }, 50), /! L\s+5.*2\/3 \(offline\)$/) +}) + +test('caches successful quorum requests by normalized Safe address', async () => { + let calls = 0 + const fetchQuorum = createQuorumFetcher(async () => { + calls += 1 + return '2/3' + }, 2) + + const [first, second] = await Promise.all([ + fetchQuorum('eth', ADDRESS.toUpperCase()), + fetchQuorum('eth', ADDRESS.toLowerCase()), + ]) + + assert.equal(first, '2/3') + assert.equal(second, '2/3') + assert.equal(calls, 1) +}) + +test('evicts failed quorum requests from the cache', async () => { + let attempts = 0 + const fetchQuorum = createQuorumFetcher(async () => { + attempts += 1 + if (attempts === 1) throw new Error('temporary failure') + return '2/3' + }, 1) + + await assert.rejects(fetchQuorum('eth', ADDRESS), /temporary failure/) + assert.equal(await fetchQuorum('eth', ADDRESS), '2/3') + assert.equal(attempts, 2) +}) + +test('limits concurrent quorum requests', async () => { + const addresses = ['a', 'b', 'c'].map((character) => `0x${character.repeat(40)}`) + const releases = [] + let active = 0 + let highestActive = 0 + let started = 0 + const fetchQuorum = createQuorumFetcher(() => { + active += 1 + started += 1 + highestActive = Math.max(highestActive, active) + return new Promise((resolve) => { + releases.push(() => { + active -= 1 + resolve('1/1') + }) + }) + }, 2) + + const requests = addresses.map((address) => fetchQuorum('eth', address)) + await new Promise(setImmediate) + assert.equal(started, 2) + assert.equal(highestActive, 2) + + releases.shift()() + await new Promise(setImmediate) + assert.equal(started, 3) + assert.equal(highestActive, 2) + + for (const release of releases) release() + assert.deepEqual(await Promise.all(requests), ['1/1', '1/1', '1/1']) +}) + +test('updates quorum drift and reports checks in source order', async () => { + const secondAddress = `0x${'b'.repeat(40)}` + const thirdAddress = `0x${'c'.repeat(40)}` + const markdown = [ + `Safe: https://app.safe.global/home?safe=eth:${ADDRESS}`, + '**Quorum:** 2/3', + '', + '| Safe | Quorum |', + '| --- | --- |', + `| [Council](https://app.safe.global/home?safe=eth:${secondAddress}) | 1/2 |`, + '', + `Safe: https://app.safe.global/home?safe=eth:${thirdAddress}`, + '**Quorum:** 1/2', + ].join('\n') + + const getQuorum = async (chain, address) => { + assert.equal(chain, 'eth') + if (address === ADDRESS) return '2/3' + if (address === secondAddress) return '3/5' + throw new Error('RPC unavailable') + } + + const { checks, content } = await updateQuorums(markdown, getQuorum) + + assert.deepEqual( + checks.map(({ lineNo, status }) => ({ lineNo, status })), + [ + { lineNo: 1, status: 'ok' }, + { lineNo: 5, status: 'drift' }, + { lineNo: 8, status: 'error' }, + ], + ) + assert.ok(checks.every((check) => !('write' in check))) + assert.match(content, /\| 3\/5 \|/) + assert.match(content, /\*\*Quorum:\*\* 2\/3/) + assert.match(content, /\*\*Quorum:\*\* 1\/2$/) +}) + +test('leaves Markdown without quorum references unchanged', async () => { + const markdown = '# No multisigs\n' + const result = await updateQuorums(markdown, async () => '1/1') + + assert.deepEqual(result, { checks: [], content: markdown }) +}) diff --git a/tests/fetch.test.js b/tests/fetch.test.js new file mode 100644 index 000000000..6f48cb588 --- /dev/null +++ b/tests/fetch.test.js @@ -0,0 +1,96 @@ +const { afterEach, test } = require('node:test') +const assert = require('node:assert/strict') +const fs = require('node:fs') +const os = require('node:os') +const path = require('node:path') + +const { TASKS, runTasks } = require('../scripts/fetch') + +const temporaryDirectories = [] +const originalConsoleLog = console.log +const originalConsoleError = console.error +const originalTestLog = process.env.FETCH_TASK_TEST_LOG + +afterEach(() => { + console.log = originalConsoleLog + console.error = originalConsoleError + if (originalTestLog === undefined) delete process.env.FETCH_TASK_TEST_LOG + else process.env.FETCH_TASK_TEST_LOG = originalTestLog + for (const directory of temporaryDirectories.splice(0)) { + fs.rmSync(directory, { recursive: true, force: true }) + } +}) + +function createTaskDirectory() { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'lido-docs-fetch-')) + temporaryDirectories.push(directory) + return directory +} + +function writeTask(directory, name, source) { + const file = path.join(directory, name) + fs.writeFileSync(file, source) + return file +} + +function silenceOutput() { + console.log = () => {} + console.error = () => {} +} + +test('runs fetch tasks in order', () => { + const directory = createTaskDirectory() + const logFile = path.join(directory, 'order.log') + process.env.FETCH_TASK_TEST_LOG = logFile + const first = writeTask( + directory, + 'first.js', + "require('node:fs').appendFileSync(process.env.FETCH_TASK_TEST_LOG, 'first\\n')", + ) + const second = writeTask( + directory, + 'second.js', + "require('node:fs').appendFileSync(process.env.FETCH_TASK_TEST_LOG, 'second\\n')", + ) + silenceOutput() + + assert.equal(runTasks([first, second]), 0) + assert.equal(fs.readFileSync(logFile, 'utf8'), 'first\nsecond\n') +}) + +test('registers every fetch script by default', () => { + assert.deepEqual(TASKS, ['fetch-audits.js', 'fetch-lips.js', 'fetch-msig-quorums.js']) +}) + +test('stops after the first failed task', () => { + const directory = createTaskDirectory() + const skippedMarker = path.join(directory, 'skipped') + process.env.FETCH_TASK_TEST_LOG = skippedMarker + const failed = writeTask(directory, 'failed.js', 'process.exitCode = 7') + const skipped = writeTask( + directory, + 'skipped.js', + "require('node:fs').writeFileSync(process.env.FETCH_TASK_TEST_LOG, 'ran')", + ) + silenceOutput() + + assert.equal(runTasks([failed, skipped]), 7) + assert.equal(fs.existsSync(skippedMarker), false) +}) + +test('uses exit code 1 when a task terminates without a status', () => { + const directory = createTaskDirectory() + const terminated = writeTask(directory, 'terminated.js', "process.kill(process.pid, 'SIGTERM')") + silenceOutput() + + assert.equal(runTasks([terminated]), 1) +}) + +test('documents the aggregate fetch command', () => { + const readme = fs.readFileSync(path.join(__dirname, '../README.md'), 'utf8') + + assert.match(readme, /npm run fetch\b/) + assert.match(readme, /public chain RPC endpoints/) + assert.match(readme, /npm run fetch-msig-quorums/) + assert.doesNotMatch(readme, /npm run fetch-external\b/) +}) diff --git a/tests/http.test.js b/tests/http.test.js new file mode 100644 index 000000000..acb586835 --- /dev/null +++ b/tests/http.test.js @@ -0,0 +1,96 @@ +const { afterEach, test } = require('node:test') +const assert = require('node:assert/strict') +const { spawn } = require('node:child_process') +const { once } = require('node:events') +const http = require('node:http') +const path = require('node:path') + +const { fetchJson, fetchText } = require('../scripts/lib/http') + +const originalFetch = global.fetch + +afterEach(() => { + global.fetch = originalFetch +}) + +test('fetches text with the provided request options', async () => { + const calls = [] + global.fetch = async (...args) => { + calls.push(args) + return { ok: true, text: async () => 'content' } + } + + const options = { headers: { Accept: 'text/plain' } } + + assert.equal(await fetchText('https://example.com/file', options), 'content') + assert.equal(calls.length, 1) + assert.equal(calls[0][0], 'https://example.com/file') + assert.deepEqual(calls[0][1].headers, options.headers) + assert.equal(calls[0][1].signal instanceof AbortSignal, true) +}) + +test('fetches JSON', async () => { + const payload = [{ name: 'proposal.md' }] + global.fetch = async () => ({ ok: true, json: async () => payload }) + + assert.deepEqual(await fetchJson('https://example.com/api'), payload) +}) + +test('rejects unsuccessful responses', async () => { + let bodyCancelled = false + let requestSignal + global.fetch = async (url, options) => { + requestSignal = options.signal + return { + ok: false, + status: 503, + body: { + cancel: async () => { + bodyCancelled = true + }, + }, + } + } + + await assert.rejects(fetchText('https://example.com/file'), /HTTP 503 → https:\/\/example\.com\/file/) + assert.equal(bodyCancelled, true) + assert.equal(requestSignal instanceof AbortSignal, true) +}) + +test('an HTTP error releases the response socket and exits with code 1', { timeout: 3_000 }, async (context) => { + const sockets = new Set() + const server = http.createServer((request, response) => { + response.writeHead(503, { 'Content-Type': 'text/plain' }) + response.write('unfinished error response') + }) + server.on('connection', (socket) => { + sockets.add(socket) + socket.on('close', () => sockets.delete(socket)) + }) + server.listen(0, '127.0.0.1') + await once(server, 'listening') + + const httpModule = path.join(__dirname, '../scripts/lib/http') + const tasksModule = path.join(__dirname, '../scripts/lib/tasks') + const source = + `const { fetchText } = require(${JSON.stringify(httpModule)});` + + `const { runTask } = require(${JSON.stringify(tasksModule)});` + + 'runTask(() => fetchText(process.env.FETCH_TEST_URL));' + const child = spawn(process.execPath, ['-e', source], { + env: { + ...process.env, + FETCH_TEST_URL: `http://127.0.0.1:${server.address().port}`, + }, + stdio: 'ignore', + }) + + context.after(() => { + child.kill('SIGKILL') + for (const socket of sockets) socket.destroy() + server.close() + }) + + const [exitCode, signal] = await once(child, 'exit') + assert.equal(exitCode, 1) + assert.equal(signal, null) +}) diff --git a/tests/output.test.js b/tests/output.test.js new file mode 100644 index 000000000..694d38e4b --- /dev/null +++ b/tests/output.test.js @@ -0,0 +1,13 @@ +const test = require('node:test') +const assert = require('node:assert/strict') + +const { printCounts } = require('../scripts/lib/output') + +test('prints category counts in insertion order', (context) => { + const lines = [] + context.mock.method(console, 'log', (...parts) => lines.push(parts.join(' '))) + + printCounts({ WIP: 2, Approved: 1 }) + + assert.deepEqual(lines, ['\nCategory counts:', ' • WIP: 2', ' • Approved: 1']) +}) diff --git a/tests/redirects.test.js b/tests/redirects.test.js new file mode 100644 index 000000000..22b3ffcce --- /dev/null +++ b/tests/redirects.test.js @@ -0,0 +1,18 @@ +const test = require('node:test') +const assert = require('node:assert/strict') + +const { redirects, resolveRedirect } = require('../config/redirects') + +test('shares Docusaurus redirects with imported documentation', () => { + assert.ok( + redirects.some( + ({ from, to }) => + from === '/token-guides/wsteth-bridging-guide' && to === '/token-guides/cross-chain-tokens-guide', + ), + ) + assert.equal( + resolveRedirect('/token-guides/wsteth-bridging-guide#the-proposed-configuration'), + '/token-guides/cross-chain-tokens-guide#mainnet-proposed-configuration', + ) + assert.equal(resolveRedirect('/guides/current'), '/guides/current') +}) diff --git a/tests/tasks.test.js b/tests/tasks.test.js new file mode 100644 index 000000000..775b8e827 --- /dev/null +++ b/tests/tasks.test.js @@ -0,0 +1,44 @@ +const { afterEach, test } = require('node:test') +const assert = require('node:assert/strict') + +const { runTask } = require('../scripts/lib/tasks') + +const TASK_MODULES = [ + '../scripts/fetch-audits', + '../scripts/fetch', + '../scripts/fetch-lips', + '../scripts/fetch-msig-quorums', +] + +for (const taskModule of TASK_MODULES) { + test(`${taskModule} exposes a run task`, () => { + assert.equal(typeof require(taskModule).run, 'function') + }) +} + +const originalExitCode = process.exitCode +const originalConsoleError = console.error + +afterEach(() => { + process.exitCode = originalExitCode + console.error = originalConsoleError +}) + +test('uses a task return value as the process exit code', async () => { + await runTask(() => 7) + + assert.equal(process.exitCode, 7) +}) + +test('reports rejected tasks and sets exit code 1', async () => { + const errors = [] + console.error = (error) => errors.push(error) + + await runTask(async () => { + throw new Error('failed') + }) + + assert.equal(process.exitCode, 1) + assert.equal(errors.length, 1) + assert.match(errors[0].message, /failed/) +})