Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,27 @@ ETHERSCAN_API_KEY=

# Testing parameters.
FORK_PROVIDER=https://base-mainnet.public.blastapi.io

# Fork block numbers for consistent coverage between local and CI runs.
# Each chain has independent block heights, so we need different blocks per chain.
# When undefined, Hardhat forks at latest block which causes coverage variability (±0.2%).
# These blocks were captured on 2025-12-16 and should be updated periodically.
# Update process: Run scripts/get-blocks.mjs to fetch latest blocks, then run coverage and update baseline.
#FORK_BLOCK_NUMBER_BASE=39590000
#FORK_BLOCK_NUMBER_ETHEREUM=21500000
#FORK_BLOCK_NUMBER_ARBITRUM_ONE=412000000
#FORK_BLOCK_NUMBER_OP_MAINNET=128000000
#FORK_BLOCK_NUMBER_POLYGON_MAINNET=81000000
#FORK_BLOCK_NUMBER_AVALANCHE=74000000
#FORK_BLOCK_NUMBER_BSC=72000000
#FORK_BLOCK_NUMBER_LINEA=27000000
#FORK_BLOCK_NUMBER_UNICHAIN=1000000

USDC_OWNER_ADDRESS=0x498581fF718922c3f8e6A244956aF099B2652b2b
GHO_OWNER_ADDRESS=0x498581fF718922c3f8e6A244956aF099B2652b2b
EURC_OWNER_ADDRESS=0x498581fF718922c3f8e6A244956aF099B2652b2b
WETH_OWNER_ADDRESS=0x498581fF718922c3f8e6A244956aF099B2652b2b
PRIME_OWNER_ADDRESS=0x75a44A70cCb0E886E25084Be14bD45af57915451
USDC_OWNER_ETH_ADDRESS=0x40ec5B33f54e0E8A33A975908C5BA1c14e5BbbDf
DAI_OWNER_ETH_ADDRESS=0x40ec5B33f54e0E8A33A975908C5BA1c14e5BbbDf
WBTC_OWNER_ETH_ADDRESS=0x40ec5B33f54e0E8A33A975908C5BA1c14e5BbbDf
29 changes: 29 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Auto detect text files and perform LF normalization
* text=auto

# Force LF line endings for all text files
* text eol=lf

# Explicitly set line endings for source files
*.sol text eol=lf
*.ts text eol=lf
*.js text eol=lf
*.json text eol=lf
*.md text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.sh text eol=lf
*.mjs text eol=lf

# Binary files
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.pdf binary
*.ico binary
*.woff binary
*.woff2 binary
*.ttf binary
*.eot binary

127 changes: 15 additions & 112 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:

jobs:
coverage:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04

steps:
- name: Checkout code
Expand All @@ -21,124 +21,27 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Setup environment variables
run: cp .env.example .env

- name: Get baseline from main branch
- name: Print environment versions
run: |
# Fetch main branch
git fetch origin main
# Get baseline from main (for comparison - must not decrease)
git show origin/main:coverage-baseline.json > baseline-from-main.json 2>/dev/null || echo '{"lines":"0","functions":"0","branches":"0","statements":"0"}' > baseline-from-main.json
echo "📊 Baseline from main branch:"
cat baseline-from-main.json
echo "Node.js: $(node -v)"
echo "NPM: $(npm -v)"
npx hardhat --version
npx solcjs --version 2>/dev/null || echo "(will be downloaded by Hardhat if needed)"

- name: Get baseline from PR
- name: Setup environment variables
run: |
# Get baseline from current PR branch (what developer committed)
if [ -f coverage-baseline.json ]; then
# Validate JSON format
if jq empty coverage-baseline.json 2>/dev/null; then
cp coverage-baseline.json baseline-from-pr.json
echo "📊 Baseline from PR (committed by developer):"
cat baseline-from-pr.json
else
echo "❌ ERROR: coverage-baseline.json is not valid JSON!"
echo "Please run: npm run coverage:update-baseline"
exit 1
fi
else
echo "❌ ERROR: No coverage-baseline.json found in PR!"
echo ""
echo "You must run coverage locally and commit the baseline file."
echo ""
echo "📝 To fix: Run these commands locally and commit the result:"
echo " npm run coverage"
echo " npm run coverage:update-baseline"
echo " git add coverage-baseline.json"
echo " git commit -m 'chore: update coverage baseline'"
echo ""
exit 1
fi
cp .env.example .env
echo "Fork block numbers configured:"
grep "FORK_BLOCK_NUMBER" .env || echo " (none found)"

- name: Run coverage
id: run_coverage
timeout-minutes: 15
- name: Run coverage and verify
run: |
set -e # Exit immediately if coverage fails
set -e
echo "Running coverage..."
npm run coverage
echo "✅ Coverage completed successfully"

- name: Validate coverage
run: |
echo "=================================================="
echo "🔍 COVERAGE VALIDATION"
echo "=================================================="

# Parse CI-generated coverage using dedicated script
CI_LINES=$(npx ts-node --files scripts/get-coverage-percentage.ts)

# Get baselines
PR_LINES=$(jq -r .lines baseline-from-pr.json)
MAIN_LINES=$(jq -r .lines baseline-from-main.json)

echo ""
echo "📊 Coverage Results:"
echo " CI (actual): $CI_LINES%"
echo " PR baseline: $PR_LINES%"
echo " Main baseline: $MAIN_LINES%"
echo ""

# Check 1: CI must match PR baseline (developer ran coverage correctly)
echo "Check 1: Did developer run coverage locally?"
if [ "$CI_LINES" = "$PR_LINES" ]; then
echo " ✅ PASS - CI coverage matches PR baseline ($CI_LINES% == $PR_LINES%)"
else
echo " ❌ FAIL - CI coverage doesn't match PR baseline!"
echo ""
echo " Expected: $PR_LINES% (from your committed coverage-baseline.json)"
echo " Actual: $CI_LINES% (from fresh CI coverage run)"
echo ""
echo "💡 This means either:"
echo " 1. You forgot to run 'npm run coverage:update-baseline' locally"
echo " 2. You modified coverage-baseline.json manually (cheating)"
echo " 3. Your local coverage differs from CI (check .env setup)"
echo ""
echo "📝 To fix: Run these commands locally and commit the result:"
echo " npm run coverage"
echo " npm run coverage:update-baseline"
echo " git add coverage-baseline.json"
echo " git commit -m 'chore: update coverage baseline'"
echo ""
exit 1
fi

echo ""

# Check 2: CI must be >= main baseline (coverage didn't decrease)
echo "Check 2: Did coverage decrease?"
if awk "BEGIN {exit !($CI_LINES >= $MAIN_LINES)}"; then
if awk "BEGIN {exit !($CI_LINES > $MAIN_LINES)}"; then
echo " ✅ PASS - Coverage improved! ($MAIN_LINES% → $CI_LINES%)"
else
echo " ✅ PASS - Coverage maintained ($CI_LINES%)"
fi
else
echo " ❌ FAIL - Coverage decreased!"
echo ""
echo " Main baseline: $MAIN_LINES%"
echo " Your PR: $CI_LINES%"
echo " Decrease: $(awk "BEGIN {print $MAIN_LINES - $CI_LINES}")%"
echo ""
echo "💡 Please add tests to maintain or improve coverage."
echo ""
exit 1
fi

echo ""
echo "=================================================="
echo "✅ ALL CHECKS PASSED"
echo "=================================================="
echo "Verifying coverage against baseline..."
npm run coverage:check

- name: Upload coverage report (optional)
if: always()
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v22.13.0
v22.21.1
42 changes: 38 additions & 4 deletions COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,20 +56,25 @@ npm run coverage:update-baseline

**Step-by-step:**
1. Make your code changes
2. Run coverage locally:
2. Ensure `.env` file exists with pinned fork blocks (copy from `.env.example` if needed):
```bash
cp .env.example .env
```
**Important:** Using the same fork blocks as `.env.example` ensures your local coverage matches CI coverage.
3. Run coverage locally:
```bash
npm run coverage
```
3. Update the baseline file:
4. Update the baseline file:
```bash
npm run coverage:update-baseline
```
4. Commit the baseline file:
5. Commit the baseline file:
```bash
git add coverage-baseline.json
git commit -m "chore: update coverage baseline"
```
5. Push your PR
6. Push your PR

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

### Fork Block Pinning for Deterministic Coverage

**Why fork blocks are pinned:**
Coverage tests fork mainnet at specific block heights. Without pinning:
- Developer runs locally → forks at block X → gets 96.93% coverage
- CI runs 30 mins later → forks at block Y → gets 96.82% coverage
- Different blocks = different contract states = different test paths = different coverage

**Solution:**
Pin each chain to a specific block number in `.env.example`:
```bash
FORK_BLOCK_NUMBER_BASE=39550474
FORK_BLOCK_NUMBER_ETHEREUM=24024515
FORK_BLOCK_NUMBER_ARBITRUM_ONE=411254516
# etc...
```

This ensures both local and CI environments fork from **identical blockchain state**, producing **identical coverage results**.

**Updating fork blocks:**
When you need to test against newer mainnet state:
1. Run the helper script: `node scripts/get-blocks.mjs`
2. Copy the output to `.env.example`
3. Run coverage: `npm run coverage`
4. If tests pass, update baseline: `npm run coverage:update-baseline`
5. Commit both `.env.example` and `coverage-baseline.json`

**Note:** Each blockchain has independent block heights, so each needs its own pinned block number.

### Branch Protection
To enforce coverage checks, enable branch protection on main:
1. GitHub Settings → Branches → Branch protection rules
Expand Down
2 changes: 2 additions & 0 deletions contracts/Deps.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@

/* solhint-disable no-unused-import */
import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
/* solhint-disable no-unused-import */
import {console} from "hardhat/console.sol";

Check failure on line 7 in contracts/Deps.sol

View workflow job for this annotation

GitHub Actions / Lint Solidity

Unexpected import of console file
19 changes: 17 additions & 2 deletions contracts/Repayer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {AcrossAdapter} from "./utils/AcrossAdapter.sol";
import {StargateAdapter} from "./utils/StargateAdapter.sol";
import {EverclearAdapter} from "./utils/EverclearAdapter.sol";
import {SuperchainStandardBridgeAdapter} from "./utils/SuperchainStandardBridgeAdapter.sol";
import {ArbitrumGatewayAdapter} from "./utils/ArbitrumGatewayAdapter.sol";
import {ERC7201Helper} from "./utils/ERC7201Helper.sol";

/// @title Performs repayment to Liquidity Pools on same/different chains.
Expand All @@ -28,7 +29,8 @@ contract Repayer is
AcrossAdapter,
StargateAdapter,
EverclearAdapter,
SuperchainStandardBridgeAdapter
SuperchainStandardBridgeAdapter,
ArbitrumGatewayAdapter
{
using SafeERC20 for IERC20;
using BitMaps for BitMaps.BitMap;
Expand Down Expand Up @@ -95,13 +97,15 @@ contract Repayer is
address wrappedNativeToken,
address stargateTreasurer,
address optimismBridge,
address baseBridge
address baseBridge,
address arbitrumGatewayRouter
)
CCTPAdapter(cctpTokenMessenger, cctpMessageTransmitter)
AcrossAdapter(acrossSpokePool)
StargateAdapter(stargateTreasurer)
EverclearAdapter(everclearFeeAdapter)
SuperchainStandardBridgeAdapter(optimismBridge, baseBridge, wrappedNativeToken)
ArbitrumGatewayAdapter(arbitrumGatewayRouter)
{
ERC7201Helper.validateStorageLocation(
STORAGE_LOCATION,
Expand Down Expand Up @@ -225,6 +229,17 @@ contract Repayer is
DOMAIN,
$.inputOutputTokens[address(token)]
);
} else
if (provider == Provider.ARBITRUM_GATEWAY) {
initiateTransferArbitrum(
token,
amount,
destinationPool,
destinationDomain,
extraData,
DOMAIN,
$.inputOutputTokens[address(token)]
);
} else {
// Unreachable atm, but could become so when more providers are added to enum.
revert UnsupportedProvider();
Expand Down
41 changes: 41 additions & 0 deletions contracts/interfaces/IArbitrumGatewayRouter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// SPDX-License-Identifier: MIT

pragma solidity 0.8.28;

/**
* @title Interface for Arbitrum Gateway Router
*/
interface IArbitrumGatewayRouter {

event TransferRouted(
address indexed token,
address indexed _userFrom,
address indexed _userTo,
address gateway
);

/**
* @notice For new versions of gateways it's recommended to use outboundTransferCustomRefund() method.
* @notice Some legacy gateways (for example, DAI) don't have the outboundTransferCustomRefund method
* @notice so using outboundTransfer() method is a universal solution
*/
function outboundTransfer(
address _token,
address _to,
uint256 _amount,
uint256 _maxGas,
uint256 _gasPriceBid,
bytes calldata _data
) external payable returns (bytes memory);

/**
* @notice Calculate the address used when bridging an ERC20 token
* @dev the L1 and L2 address oracles may not always be in sync.
* For example, a custom token may have been registered but not deploy or the contract self destructed.
* @param l1ERC20 address of L1 token
* @return L2 address of a bridged ERC20 token
*/
function calculateL2TokenAddress(address l1ERC20) external view returns (address);

function getGateway(address _token) external view returns (address gateway);
}
3 changes: 2 additions & 1 deletion contracts/interfaces/IRoute.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ interface IRoute {
ACROSS,
STARGATE,
EVERCLEAR,
SUPERCHAIN_STANDARD_BRIDGE
SUPERCHAIN_STANDARD_BRIDGE,
ARBITRUM_GATEWAY
}

enum PoolType {
Expand Down
Loading
Loading