Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
fa80c06
Add Arbitrum gateway
viatrix Dec 11, 2025
2614b4b
Merge branch 'main' into feat-arbitrum-gateway
viatrix Dec 11, 2025
a99aa0b
Update scripts and coverage
viatrix Dec 11, 2025
229201e
Fixes after review
viatrix Dec 13, 2025
34e9877
Remove duplicated test
viatrix Dec 16, 2025
688b200
updated deterministic hardhat tests
LyonSsS Dec 16, 2025
ca22cbd
fixed lint issues
LyonSsS Dec 16, 2025
e7ffbd8
push change
LyonSsS Dec 17, 2025
110547b
updated debug behavior
LyonSsS Dec 17, 2025
52dbfc2
Update Everclear test
lastperson Dec 17, 2025
36824b8
Merge branch 'feat/update_deterministic_hardhat_tests' of ssh://githu…
LyonSsS Dec 17, 2025
bdff146
updated baseline and added ubuntu 22 to github coverage action
LyonSsS Dec 17, 2025
4379e3f
normalized files
LyonSsS Dec 17, 2025
895684d
updated .env.example to use latest block and nor forked block
LyonSsS Dec 17, 2025
4e45989
print github action env versions
LyonSsS Dec 17, 2025
b8e4e88
generated coverage-baseline on ubuntu and aded 0.2 percentage tollerance
LyonSsS Dec 17, 2025
7ad3687
updated woflwo coverage script usage
LyonSsS Dec 17, 2025
81912c2
fixed coverage script
LyonSsS Dec 17, 2025
c4aa6c0
updated coverage and cleaned branch
LyonSsS Dec 18, 2025
c0dc1c8
test ssh signed commit
LyonSsS Dec 18, 2025
9ba1f4a
fixed fail coverage job
LyonSsS Dec 18, 2025
b7cf55d
fix: use node to parse JSON in coverage workflow
LyonSsS Dec 18, 2025
bf20db6
removed .gitattributes configuration
LyonSsS Dec 18, 2025
e739570
verify ci coverage with main coverage
LyonSsS Dec 18, 2025
3dc28d3
fixed code review changes
LyonSsS Dec 19, 2025
2574d78
test: verify commit signing
LyonSsS Dec 19, 2025
66b36aa
fixed lint issues
LyonSsS Dec 19, 2025
e8e846c
updated harhat behafior to cover main
LyonSsS Dec 19, 2025
1fd7472
fix: correct USDC_OWNER_ADDRESS typo in .env.example
LyonSsS Dec 19, 2025
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
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,5 @@ 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
144 changes: 45 additions & 99 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,70 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Print environment versions
run: |
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: Setup environment variables
run: cp .env.example .env
run: |
cp .env.example .env
echo "✅ Environment configured"

- name: Get baseline from main branch
- name: Fetch main branch baseline
run: |
# Fetch main branch
echo "📥 Fetching coverage baseline from 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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this code was abandoned?

echo "📊 Baseline from main branch:"
cat baseline-from-main.json

- name: Get baseline from PR
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

- name: Run coverage
id: run_coverage
timeout-minutes: 15
run: |
set -e # Exit immediately if coverage fails
set -e
echo ""
echo "📊 Running test coverage analysis in CI..."
echo "This generates fresh coverage from your PR code"
npm run coverage
echo "✅ Coverage completed successfully"

- name: Validate coverage
- name: Display coverage comparison
run: |
set -e
echo ""
echo "=================================================="
echo "🔍 COVERAGE VALIDATION"
echo " COVERAGE VALIDATION"
echo "=================================================="

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the get-coverage-percentage script not used anymore, and instead inline nodejs code is used?


# Get baselines
PR_LINES=$(jq -r .lines baseline-from-pr.json)
MAIN_LINES=$(jq -r .lines baseline-from-main.json)
Comment on lines -82 to -83
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why abandoning this part if it was perfectly alright?


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
# Extract coverage from CI run (actual)
CI_COV=$(npx ts-node --files scripts/get-coverage-percentage.ts)

echo ""
# Extract PR baseline (what developer committed)
PR_BASELINE=$(jq -r '.lines // "0"' coverage-baseline.json)

# 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
# Extract main baseline (production baseline)
MAIN_BASELINE=$(jq -r '.lines // "0"' baseline-from-main.json)

echo "📊 Coverage Results (Lines):"
echo " CI (actual): ${CI_COV}% ← Fresh coverage from this PR"
echo " PR baseline: ${PR_BASELINE}% ← Baseline you committed"
echo " Main baseline: ${MAIN_BASELINE}% ← Baseline from main branch"
echo ""
echo "✓ Check 1: CI coverage should match PR baseline"
echo " (proves you ran coverage locally)"
echo "✓ Check 2: CI coverage should be >= Main baseline - 0.2%"
echo " (proves coverage didn't decrease beyond tolerance)"
echo ""
echo "💡 Allowed tolerance: ±0.2%"
echo ""
echo "=================================================="
echo "✅ ALL CHECKS PASSED"
echo "=================================================="
echo ""

- name: Verify coverage
run: |
set -e
echo "🔍 Running coverage validation..."
npx ts-node --files scripts/check-coverage.ts --main-baseline=baseline-from-main.json

- name: Upload coverage report (optional)
if: always()
Expand Down
59 changes: 35 additions & 24 deletions COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,35 @@ This project uses automated coverage checks to prevent test coverage from decrea

## How It Works: Dual Validation

Developers run coverage locally and commit the baseline file. CI validates both that the developer ran coverage correctly AND that coverage didn't decrease.
Developers run coverage locally and commit the baseline file. CI validates both that the developer ran coverage correctly AND that coverage didn't decrease beyond tolerance.

### Coverage Workflow (`.github/workflows/coverage.yml`)
**Triggers:** Every pull request to main

**What it does:**
1. **Fetches baseline from main branch** - the current production baseline
2. **Reads baseline from PR branch** - the baseline you committed
3. **Runs coverage fresh in CI** - generates actual coverage from your code
4. **Performs two validations:**
1. **Fetches baseline from main branch** - the current production baseline (coverage-baseline-main.json)
2. **Reads baseline from PR branch** - the baseline you committed (coverage-baseline.json)
3. **Runs coverage fresh in CI** - generates actual coverage from your code (coverage/lcov.info)
4. **Displays all three values** - Shows CI actual, PR baseline, and Main baseline side-by-side
5. **Performs two validations with ±0.2% tolerance:**

**Validation 1: Did you run coverage locally?**
- ✅ **PASS** if `CI coverage === PR baseline` (you ran coverage correctly)
- ❌ **FAIL** if `CI coverage !== PR baseline` (you forgot to run coverage or tampered with file)
- ✅ **PASS** if `CI coverage PR baseline (±0.2%)` (you ran coverage correctly)
- ❌ **FAIL** if difference exceeds tolerance (you forgot to run coverage or tampered with file)

**Validation 2: Did coverage decrease?**
- ✅ **PASS** if `CI coverage >= main baseline` (coverage maintained or improved)
- ❌ **FAIL** if `CI coverage < main baseline` (coverage decreased)
- ✅ **PASS** if `CI coverage >= main baseline - 0.2%` (coverage maintained within tolerance)
- ❌ **FAIL** if `CI coverage < main baseline - 0.2%` (coverage decreased beyond tolerance)

**Tolerance:**
A ±0.2% tolerance is applied to both checks to account for:
- Minor variations in test execution
- Rounding differences in coverage calculation
- Small changes in external contract states (tests fork at latest block)

**Security Model:**
- ✅ **Can't skip running coverage** - CI checks if your committed baseline matches actual coverage
- ✅ **Can't decrease coverage** - CI checks if your coverage is below main's baseline
- ✅ **Can't skip running coverage** - CI checks if your committed baseline matches actual coverage (within tolerance)
- ✅ **Can't decrease coverage** - CI checks if your coverage is below main's baseline (beyond tolerance)
- ✅ **Can't cheat** - CI regenerates coverage fresh and validates against both baselines
- ✅ **Can't commit invalid baseline** - CI validates JSON format before processing
- ✅ **Can't skip baseline file** - CI fails immediately if baseline file is missing
Expand Down Expand Up @@ -56,30 +63,33 @@ npm run coverage:update-baseline

**Step-by-step:**
1. Make your code changes
2. Run coverage locally:
2. Ensure `.env` file exists (copy from `.env.example` if needed):
```bash
cp .env.example .env
```
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)
- ✅ **Check 2:** Your coverage is >= main's baseline (proves coverage didn't drop)
- ✅ **Check 1:** Your committed baseline matches CI coverage within ±0.2% (proves you ran coverage)
- ✅ **Check 2:** Your coverage is >= main's baseline - 0.2% (proves coverage didn't drop beyond tolerance)

**If CI fails:**
- **"No coverage-baseline.json found in PR"** → You forgot to commit the baseline file. Run steps 2-4 above and push.
- **"No coverage-baseline.json found in PR"** → You forgot to commit the baseline file. Run steps 3-5 above and push.
- **"coverage-baseline.json is not valid JSON"** → The baseline file is corrupted. Run `npm run coverage:update-baseline` and commit.
- **"CI coverage doesn't match PR baseline"** → You forgot to update the baseline. Run steps 2-3 above and push.
- **"Coverage decreased"** → Add more tests to maintain or improve coverage.
- **"Coverage decreased beyond tolerance"** → Coverage dropped more than 0.2% compared to PR baseline or main baseline. Add more tests to maintain or improve coverage.

### For Maintainers

Expand All @@ -101,12 +111,13 @@ Current baseline (as of initial setup):
- Uses Hardhat's built-in coverage tool (generates `coverage/lcov.info`)
- Parses LCOV format to extract: lines, functions, branches, statements
- Stores baseline in `coverage-baseline.json` at repository root
- CI fetches main branch baseline as `coverage-baseline-main.json`
- Scripts:
- `scripts/check-coverage.ts` - Local validation (compares coverage against baseline)
- `scripts/get-coverage-percentage.ts` - Extracts coverage percentage from lcov.info (used by CI)
- `scripts/check-coverage.ts` - Validates coverage against both PR and main baselines with ±0.2% tolerance
- Accepts `--main-baseline=<path>` parameter to compare against main branch baseline

### Environment Setup for CI
The workflow copies `.env.example` to `.env` to enable fork tests with public RPC endpoints during coverage runs.
### Environment Setup
The workflow copies `.env.example` to `.env` to enable fork tests with public RPC endpoints during coverage runs. Tests fork at the latest block to ensure they work with current mainnet state.

### Branch Protection
To enforce coverage checks, enable branch protection on main:
Expand Down
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