Skip to content

Commit 688b200

Browse files
committed
updated deterministic hardhat tests
1 parent 34e9877 commit 688b200

File tree

5 files changed

+98
-6
lines changed

5 files changed

+98
-6
lines changed

.env.example

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,22 @@ ETHERSCAN_API_KEY=
4040

4141
# Testing parameters.
4242
FORK_PROVIDER=https://base-mainnet.public.blastapi.io
43+
44+
# Fork block numbers for consistent coverage between local and CI runs.
45+
# Each chain has independent block heights, so we need different blocks per chain.
46+
# When undefined, Hardhat forks at latest block which causes coverage variability (±0.2%).
47+
# These blocks were captured on 2025-12-16 and should be updated periodically.
48+
# Update process: Run scripts/get-blocks.mjs to fetch latest blocks, then run coverage and update baseline.
49+
FORK_BLOCK_NUMBER_BASE=39550474
50+
FORK_BLOCK_NUMBER_ETHEREUM=24024515
51+
FORK_BLOCK_NUMBER_ARBITRUM_ONE=411254516
52+
FORK_BLOCK_NUMBER_OP_MAINNET=127000000
53+
FORK_BLOCK_NUMBER_POLYGON_MAINNET=80390425
54+
FORK_BLOCK_NUMBER_AVALANCHE=73848966
55+
FORK_BLOCK_NUMBER_BSC=71852875
56+
FORK_BLOCK_NUMBER_LINEA=26756433
57+
FORK_BLOCK_NUMBER_UNICHAIN=1000000
58+
4359
USDC_OWNER_ADDRESS=0x498581fF718922c3f8e6A244956aF099B2652b2b
4460
GHO_OWNER_ADDRESS=0x498581fF718922c3f8e6A244956aF099B2652b2b
4561
EURC_OWNER_ADDRESS=0x498581fF718922c3f8e6A244956aF099B2652b2b

COVERAGE.md

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,20 +56,25 @@ npm run coverage:update-baseline
5656

5757
**Step-by-step:**
5858
1. Make your code changes
59-
2. Run coverage locally:
59+
2. Ensure `.env` file exists with pinned fork blocks (copy from `.env.example` if needed):
60+
```bash
61+
cp .env.example .env
62+
```
63+
**Important:** Using the same fork blocks as `.env.example` ensures your local coverage matches CI coverage.
64+
3. Run coverage locally:
6065
```bash
6166
npm run coverage
6267
```
63-
3. Update the baseline file:
68+
4. Update the baseline file:
6469
```bash
6570
npm run coverage:update-baseline
6671
```
67-
4. Commit the baseline file:
72+
5. Commit the baseline file:
6873
```bash
6974
git add coverage-baseline.json
7075
git commit -m "chore: update coverage baseline"
7176
```
72-
5. Push your PR
77+
6. Push your PR
7378

7479
**What CI validates:**
7580
-**Check 1:** Your committed baseline matches CI coverage (proves you ran coverage)
@@ -108,6 +113,35 @@ Current baseline (as of initial setup):
108113
### Environment Setup for CI
109114
The workflow copies `.env.example` to `.env` to enable fork tests with public RPC endpoints during coverage runs.
110115

116+
### Fork Block Pinning for Deterministic Coverage
117+
118+
**Why fork blocks are pinned:**
119+
Coverage tests fork mainnet at specific block heights. Without pinning:
120+
- Developer runs locally → forks at block X → gets 96.93% coverage
121+
- CI runs 30 mins later → forks at block Y → gets 96.82% coverage
122+
- Different blocks = different contract states = different test paths = different coverage
123+
124+
**Solution:**
125+
Pin each chain to a specific block number in `.env.example`:
126+
```bash
127+
FORK_BLOCK_NUMBER_BASE=39550474
128+
FORK_BLOCK_NUMBER_ETHEREUM=24024515
129+
FORK_BLOCK_NUMBER_ARBITRUM_ONE=411254516
130+
# etc...
131+
```
132+
133+
This ensures both local and CI environments fork from **identical blockchain state**, producing **identical coverage results**.
134+
135+
**Updating fork blocks:**
136+
When you need to test against newer mainnet state:
137+
1. Run the helper script: `node scripts/get-blocks.mjs`
138+
2. Copy the output to `.env.example`
139+
3. Run coverage: `npm run coverage`
140+
4. If tests pass, update baseline: `npm run coverage:update-baseline`
141+
5. Commit both `.env.example` and `coverage-baseline.json`
142+
143+
**Note:** Each blockchain has independent block heights, so each needs its own pinned block number.
144+
111145
### Branch Protection
112146
To enforce coverage checks, enable branch protection on main:
113147
1. GitHub Settings → Branches → Branch protection rules

coverage-baseline.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"lines": "96.93",
33
"functions": "98.58",
4-
"branches": "87.79",
4+
"branches": "87.63",
55
"statements": "96.93"
66
}

hardhat.config.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -699,7 +699,14 @@ const config: HardhatUserConfig = {
699699
url: isSet(process.env.DRY_RUN) || isSet(process.env.FORK_TEST)
700700
? process.env[`${process.env.DRY_RUN || process.env.FORK_TEST}_RPC`]!
701701
: (process.env.FORK_PROVIDER || process.env.BASE_RPC || "https://base-mainnet.public.blastapi.io"),
702-
blockNumber: process.env.FORK_BLOCK_NUMBER ? parseInt(process.env.FORK_BLOCK_NUMBER) : undefined,
702+
blockNumber: (() => {
703+
// Determine which chain is being forked
704+
const chain = (process.env.DRY_RUN || process.env.FORK_TEST || 'BASE').toUpperCase();
705+
// Look up the per-chain fork block number
706+
const blockVar = `FORK_BLOCK_NUMBER_${chain}`;
707+
const blockNumber = process.env[blockVar];
708+
return blockNumber ? parseInt(blockNumber) : undefined;
709+
})(),
703710
},
704711
accounts: isSet(process.env.DRY_RUN)
705712
? [{privateKey: process.env.PRIVATE_KEY!, balance: "1000000000000000000"}]

scripts/get-blocks.mjs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { ethers } from 'ethers';
2+
3+
const chains = {
4+
'BASE': process.env.BASE_RPC || 'https://base-mainnet.public.blastapi.io',
5+
'ETHEREUM': process.env.ETHEREUM_RPC || 'https://eth-mainnet.public.blastapi.io',
6+
'ARBITRUM_ONE': process.env.ARBITRUM_ONE_RPC || 'https://arbitrum-one.public.blastapi.io',
7+
'OP_MAINNET': process.env.OP_MAINNET_RPC || 'https://public-op-mainnet.fastnode.io',
8+
'POLYGON_MAINNET': process.env.POLYGON_MAINNET_RPC || 'https://polygon-bor-rpc.publicnode.com',
9+
'AVALANCHE': process.env.AVALANCHE_RPC || 'https://avalanche-c-chain-rpc.publicnode.com',
10+
'BSC': process.env.BSC_RPC || 'https://bsc-mainnet.public.blastapi.io',
11+
'LINEA': process.env.LINEA_RPC || 'https://linea-rpc.publicnode.com',
12+
};
13+
14+
async function getBlockNumber(name, url) {
15+
try {
16+
const provider = new ethers.JsonRpcProvider(url);
17+
const blockNumber = await provider.getBlockNumber();
18+
// Subtract 1000 blocks for safety margin
19+
const safeBlock = blockNumber - 1000;
20+
console.log(`FORK_BLOCK_NUMBER_${name}=${safeBlock}`);
21+
return safeBlock;
22+
} catch (error) {
23+
console.error(`# Error fetching ${name}: ${error.message}`);
24+
return null;
25+
}
26+
}
27+
28+
async function main() {
29+
console.log('# Fetching current block numbers...');
30+
for (const [name, url] of Object.entries(chains)) {
31+
await getBlockNumber(name, url);
32+
}
33+
}
34+
35+
main().catch(console.error);

0 commit comments

Comments
 (0)