From fa80c06bb3d67699dbbe63efd3788622ea5caec2 Mon Sep 17 00:00:00 2001 From: viatrix Date: Thu, 11 Dec 2025 23:50:38 +0200 Subject: [PATCH 01/19] Add Arbitrum gateway --- contracts/Repayer.sol | 19 +- .../interfaces/IArbitrumGatewayRouter.sol | 41 ++ contracts/interfaces/IRoute.sol | 3 +- contracts/testing/TestArbitrum.sol | 42 ++ contracts/testing/TestRepayer.sol | 6 +- contracts/utils/ArbitrumGatewayAdapter.sol | 57 ++ network.config.ts | 4 + scripts/common.ts | 2 + specific-fork-test/ethereum/Repayer.ts | 280 ++++++- test/Repayer.ts | 697 +++++++++++++++++- 10 files changed, 1129 insertions(+), 22 deletions(-) create mode 100644 contracts/interfaces/IArbitrumGatewayRouter.sol create mode 100644 contracts/testing/TestArbitrum.sol create mode 100644 contracts/utils/ArbitrumGatewayAdapter.sol diff --git a/contracts/Repayer.sol b/contracts/Repayer.sol index 7de894b..9c02afc 100644 --- a/contracts/Repayer.sol +++ b/contracts/Repayer.sol @@ -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. @@ -28,7 +29,8 @@ contract Repayer is AcrossAdapter, StargateAdapter, EverclearAdapter, - SuperchainStandardBridgeAdapter + SuperchainStandardBridgeAdapter, + ArbitrumGatewayAdapter { using SafeERC20 for IERC20; using BitMaps for BitMaps.BitMap; @@ -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, @@ -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(); diff --git a/contracts/interfaces/IArbitrumGatewayRouter.sol b/contracts/interfaces/IArbitrumGatewayRouter.sol new file mode 100644 index 0000000..08d9248 --- /dev/null +++ b/contracts/interfaces/IArbitrumGatewayRouter.sol @@ -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); +} diff --git a/contracts/interfaces/IRoute.sol b/contracts/interfaces/IRoute.sol index bcf2aab..3d7cf4c 100644 --- a/contracts/interfaces/IRoute.sol +++ b/contracts/interfaces/IRoute.sol @@ -27,7 +27,8 @@ interface IRoute { ACROSS, STARGATE, EVERCLEAR, - SUPERCHAIN_STANDARD_BRIDGE + SUPERCHAIN_STANDARD_BRIDGE, + ARBITRUM_GATEWAY } enum PoolType { diff --git a/contracts/testing/TestArbitrum.sol b/contracts/testing/TestArbitrum.sol new file mode 100644 index 0000000..aef0a78 --- /dev/null +++ b/contracts/testing/TestArbitrum.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity 0.8.28; + +import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IArbitrumGatewayRouter} from "../interfaces/IArbitrumGatewayRouter.sol"; + +contract TestArbitrumGatewayRouter is IArbitrumGatewayRouter { + + address public immutable LOCAL_TOKEN; + address public immutable L2_TOKEN; + + error InvalidToken(); + error SimulatedRevert(); + + constructor(address _localtoken, address _l2token) { + LOCAL_TOKEN = _localtoken; + L2_TOKEN = _l2token; + } + + function calculateL2TokenAddress(address) external view override returns (address) { + return L2_TOKEN; + } + + function getGateway(address) external view returns (address gateway) { + return address(this); + } + + function outboundTransfer( + address _token, + address _to, + uint256 _amount, + uint256, + uint256, + bytes calldata + ) external payable returns (bytes memory) { + require(_token == LOCAL_TOKEN, InvalidToken()); + require(_amount != 2000, SimulatedRevert()); + SafeERC20.safeTransferFrom(IERC20(LOCAL_TOKEN), msg.sender, address(this), _amount); + emit TransferRouted(LOCAL_TOKEN, msg.sender, _to, address(this)); + return "GATEWAY_DATA"; + } +} diff --git a/contracts/testing/TestRepayer.sol b/contracts/testing/TestRepayer.sol index 2d9f939..73d3649 100644 --- a/contracts/testing/TestRepayer.sol +++ b/contracts/testing/TestRepayer.sol @@ -14,7 +14,8 @@ contract TestRepayer is Repayer { address wrappedNativeToken, address stargateTreasurer, address optimismBridge, - address baseBridge + address baseBridge, + address arbitrumGatewayRouter ) Repayer( localDomain, assets, @@ -25,7 +26,8 @@ contract TestRepayer is Repayer { wrappedNativeToken, stargateTreasurer, optimismBridge, - baseBridge + baseBridge, + arbitrumGatewayRouter ) {} function domainCCTP(Domain destinationDomain) public pure override returns (uint32) { diff --git a/contracts/utils/ArbitrumGatewayAdapter.sol b/contracts/utils/ArbitrumGatewayAdapter.sol new file mode 100644 index 0000000..a7b62b2 --- /dev/null +++ b/contracts/utils/ArbitrumGatewayAdapter.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity 0.8.28; + +import {BitMaps} from "@openzeppelin/contracts/utils/structs/BitMaps.sol"; +import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IArbitrumGatewayRouter} from ".././interfaces/IArbitrumGatewayRouter.sol"; +import {AdapterHelper} from "./AdapterHelper.sol"; + +abstract contract ArbitrumGatewayAdapter is AdapterHelper { + using SafeERC20 for IERC20; + + IArbitrumGatewayRouter immutable public ARBITRUM_GATEWAY_ROUTER; + + event ArbitrumERC20TransferInitiated(bytes gatewayData); + + constructor( + address arbitrumGatewayRouter + ) { + // No check for address(0) to allow deployment on chains where Arbitrum Bridge is not available + ARBITRUM_GATEWAY_ROUTER = IArbitrumGatewayRouter(arbitrumGatewayRouter); + } + + function initiateTransferArbitrum( + IERC20 token, + uint256 amount, + address destinationPool, + Domain destinationDomain, + bytes calldata extraData, + Domain localDomain, + mapping(bytes32 => BitMaps.BitMap) storage outputTokens + ) internal { + // We are only interested in fast L1->L2 bridging, because the reverse is slow. + require(localDomain == Domain.ETHEREUM, UnsupportedDomain()); + require(destinationDomain == Domain.ARBITRUM_ONE, UnsupportedDomain()); + IArbitrumGatewayRouter router = ARBITRUM_GATEWAY_ROUTER; + require(address(router) != address(0), ZeroAddress()); + (address outputToken, uint256 maxGas, uint256 gasPriceBid, bytes memory data) = + abi.decode(extraData, (address, uint256, uint256, bytes)); + + _validateOutputToken(_addressToBytes32(outputToken), destinationDomain, outputTokens); + // Get output token from the gateway + address gatewayOutputToken = ARBITRUM_GATEWAY_ROUTER.calculateL2TokenAddress(address(token)); + // Check that output tokens match + require(gatewayOutputToken == outputToken, InvalidOutputToken()); + address gateway = ARBITRUM_GATEWAY_ROUTER.getGateway(address(token)); + token.forceApprove(gateway, amount); + bytes memory gatewayData = router.outboundTransfer{value: msg.value}( + address(token), + destinationPool, + amount, + maxGas, + gasPriceBid, + data + ); + emit ArbitrumERC20TransferInitiated(gatewayData); + } +} diff --git a/network.config.ts b/network.config.ts index d589b6b..712f984 100644 --- a/network.config.ts +++ b/network.config.ts @@ -83,6 +83,7 @@ export enum Provider { EVERCLEAR = "EVERCLEAR", STARGATE = "STARGATE", SUPERCHAIN_STANDARD_BRIDGE = "SUPERCHAIN_STANDARD_BRIDGE", + ARBITRUM_GATEWAY = "ARBITRUM_GATEWAY", } export enum Token { @@ -164,6 +165,7 @@ export interface NetworkConfig { EverclearFeeAdapter?: string; OptimismStandardBridge?: string; BaseStandardBridge?: string; + ArbitrumGatewayRouter?: string; Tokens: { [Token.USDC]: string; [Token.USDT]?: string; @@ -213,6 +215,7 @@ export const networkConfig: NetworksConfig = { EverclearFeeAdapter: "0xd0185bfb8107c5b2336bC73cE3fdd9Bfb504540e", OptimismStandardBridge: "0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1", BaseStandardBridge: "0x3154Cf16ccdb4C6d922629664174b904d80F2C35", + ArbitrumGatewayRouter: "0x72Ce9c846789fdB6fC1f34aC4AD25Dd9ef7031ef", Tokens: { USDC: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", USDT: "0xdAC17F958D2ee523a2206206994597C13D831ec7", @@ -310,6 +313,7 @@ export const networkConfig: NetworksConfig = { EverclearFeeAdapter: "0xd0185bfb8107c5b2336bC73cE3fdd9Bfb504540e", OptimismStandardBridge: "0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1", BaseStandardBridge: "0x3154Cf16ccdb4C6d922629664174b904d80F2C35", + ArbitrumGatewayRouter: "0x72Ce9c846789fdB6fC1f34aC4AD25Dd9ef7031ef", Tokens: { USDC: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", USDT: "0xdAC17F958D2ee523a2206206994597C13D831ec7", diff --git a/scripts/common.ts b/scripts/common.ts index 798fe8c..23461f7 100644 --- a/scripts/common.ts +++ b/scripts/common.ts @@ -48,6 +48,7 @@ export const ProviderSolidity = { STARGATE: 3n, EVERCLEAR: 4n, SUPERCHAIN_STANDARD_BRIDGE: 5n, + ARBITRUM_GATEWAY: 6n, }; export const DomainSolidity = { @@ -93,6 +94,7 @@ export const SolidityProvider: { [n: number]: Provider } = { 3: Provider.STARGATE, 4: Provider.EVERCLEAR, 5: Provider.SUPERCHAIN_STANDARD_BRIDGE, + 6: Provider.ARBITRUM_GATEWAY, }; export const CCTPDomain: { [n: number]: Network } = { diff --git a/specific-fork-test/ethereum/Repayer.ts b/specific-fork-test/ethereum/Repayer.ts index eb21a20..236bfa3 100644 --- a/specific-fork-test/ethereum/Repayer.ts +++ b/specific-fork-test/ethereum/Repayer.ts @@ -20,7 +20,7 @@ import {networkConfig} from "../../network.config"; describe("Repayer", function () { const deployAll = async () => { - const [deployer, admin, repayUser, user, setTokensUser] = await hre.ethers.getSigners(); + const [deployer, admin, repayUser, setTokensUser] = await hre.ethers.getSigners(); await setCode(repayUser.address, "0x00"); const forkNetworkConfig = networkConfig.ETHEREUM; @@ -29,6 +29,8 @@ describe("Repayer", function () { const DEPOSIT_PROFIT_ROLE = toBytes32("DEPOSIT_PROFIT_ROLE"); const usdc = await hre.ethers.getContractAt("ERC20", forkNetworkConfig.Tokens.USDC); + const dai = await hre.ethers.getContractAt("ERC20", forkNetworkConfig.Tokens.DAI!); + const wbtc = await hre.ethers.getContractAt("ERC20", forkNetworkConfig.Tokens.WBTC!); const liquidityPool = (await deploy( "TestLiquidityPool", deployer, @@ -69,10 +71,16 @@ describe("Repayer", function () { "ISuperchainStandardBridge", forkNetworkConfig.BaseStandardBridge! ); + const arbitrumGatewayRouter = await hre.ethers.getContractAt( + "IArbitrumGatewayRouter", + forkNetworkConfig.ArbitrumGatewayRouter! + ); const everclearFeeAdapter = await hre.ethers.getContractAt("IFeeAdapterV2", forkNetworkConfig.EverclearFeeAdapter!); const weth = await hre.ethers.getContractAt("IWrappedNativeToken", forkNetworkConfig.WrappedNativeToken); const USDC_DEC = 10n ** (await usdc.decimals()); + const DAI_DEC = 10n ** (await dai.decimals()); + const WBTC_DEC = 10n ** (await wbtc.decimals()); const repayerImpl = ( await deployX("Repayer", deployer, "Repayer", {}, @@ -86,16 +94,23 @@ describe("Repayer", function () { stargateTreasurer, optimismStandardBridge, baseStandardBridge, + arbitrumGatewayRouter, ) ) as Repayer; const repayerInit = (await repayerImpl.initialize.populateTransaction( admin, repayUser, setTokensUser, - [liquidityPool, liquidityPool2, liquidityPool, liquidityPool], - [Domain.ETHEREUM, Domain.ETHEREUM, Domain.OP_MAINNET, Domain.BASE], - [Provider.LOCAL, Provider.LOCAL, Provider.SUPERCHAIN_STANDARD_BRIDGE, Provider.SUPERCHAIN_STANDARD_BRIDGE], - [true, false, true, true], + [liquidityPool, liquidityPool2, liquidityPool, liquidityPool, liquidityPool], + [Domain.ETHEREUM, Domain.ETHEREUM, Domain.OP_MAINNET, Domain.BASE, Domain.ARBITRUM_ONE], + [ + Provider.LOCAL, + Provider.LOCAL, + Provider.SUPERCHAIN_STANDARD_BRIDGE, + Provider.SUPERCHAIN_STANDARD_BRIDGE, + Provider.ARBITRUM_GATEWAY + ], + [true, false, true, true, true], [ { inputToken: usdc, @@ -109,6 +124,33 @@ describe("Repayer", function () { {destinationDomain: Domain.BASE, outputToken: addressToBytes32(networkConfig.BASE.Tokens.USDC)} ] }, + { + inputToken: dai, + destinationTokens: [ + { + destinationDomain: Domain.ARBITRUM_ONE, + outputToken: addressToBytes32(networkConfig.ARBITRUM_ONE.Tokens.DAI) + } + ] + }, + { + inputToken: wbtc, + destinationTokens: [ + { + destinationDomain: Domain.ARBITRUM_ONE, + outputToken: addressToBytes32(networkConfig.ARBITRUM_ONE.Tokens.WBTC) + } + ] + }, + { + inputToken: weth, + destinationTokens: [ + { + destinationDomain: Domain.ARBITRUM_ONE, + outputToken: addressToBytes32(networkConfig.ARBITRUM_ONE.Tokens.WETH) + } + ] + }, ], )).data; const repayerProxy = (await deployX( @@ -122,10 +164,11 @@ describe("Repayer", function () { await liquidityPool.grantRole(DEPOSIT_PROFIT_ROLE, repayer); return { - deployer, admin, repayUser, user, usdc, setTokensUser, + deployer, admin, repayUser, usdc, setTokensUser, USDC_DEC, liquidityPool, liquidityPool2, repayer, repayerProxy, repayerAdmin, cctpTokenMessenger, cctpMessageTransmitter, REPAYER_ROLE, DEFAULT_ADMIN_ROLE, acrossV3SpokePool, weth, stargateTreasurer, everclearFeeAdapter, forkNetworkConfig, optimismStandardBridge, baseStandardBridge, + arbitrumGatewayRouter, dai, DAI_DEC, wbtc, WBTC_DEC, }; }; @@ -288,4 +331,229 @@ describe("Repayer", function () { expect(await getBalance(repayer)).to.equal(0n); expect(await weth.balanceOf(repayer)).to.equal(0n); }); + + it("Should allow repayer to initiate Arbitrum Gateway DAI repay on fork", async function () { + const { + repayer, repayUser, liquidityPool, arbitrumGatewayRouter, dai, DAI_DEC + } = await loadFixture(deployAll); + + assertAddress(process.env.DAI_OWNER_ETH_ADDRESS, "Env variables not configured (DAI_OWNER_ETH_ADDRESS missing)"); + const DAI_OWNER_ETH_ADDRESS = process.env.DAI_OWNER_ETH_ADDRESS; + const daiOwner = await hre.ethers.getImpersonatedSigner(DAI_OWNER_ETH_ADDRESS); + await setBalance(DAI_OWNER_ETH_ADDRESS, 10n ** 18n); + + const amount = 4n * DAI_DEC; + const maxGas = 10000000n; + const gasPriceBid = 60000000n; + const maxSubmissionCost = 100000000000000n; + const fee = 1000000000000000n; + await dai.connect(daiOwner).transfer(repayer, amount); + + const outputToken = networkConfig.ARBITRUM_ONE.Tokens.DAI; + + const data = AbiCoder.defaultAbiCoder().encode( + ["uint256", "bytes"], + [maxSubmissionCost, "0x"], + ); + const extraData = AbiCoder.defaultAbiCoder().encode( + ["address", "uint256", "uint256", "bytes"], + [outputToken, maxGas, gasPriceBid, data] + ); + + const gatewayAddress = await arbitrumGatewayRouter.getGateway(dai.target); + const tx = repayer.connect(repayUser).initiateRepay( + dai, + amount, + liquidityPool, + Domain.ARBITRUM_ONE, + Provider.ARBITRUM_GATEWAY, + extraData, + {value: fee} + ); + await expect(tx) + .to.emit(repayer, "InitiateRepay") + .withArgs(dai.target, amount, liquidityPool.target, Domain.ARBITRUM_ONE, Provider.ARBITRUM_GATEWAY); + await expect(tx) + .to.emit(arbitrumGatewayRouter, "TransferRouted") + .withArgs(dai.target, repayer.target, liquidityPool.target, gatewayAddress); + expect(await dai.balanceOf(repayer)).to.equal(0n); + }); + + it("Should allow repayer to initiate Arbitrum Gateway WBTC repay on fork", async function () { + const { + repayer, repayUser, liquidityPool, arbitrumGatewayRouter, wbtc, WBTC_DEC + } = await loadFixture(deployAll); + + assertAddress(process.env.WBTC_OWNER_ETH_ADDRESS, "Env variables not configured (WBTC_OWNER_ETH_ADDRESS missing)"); + const WBTC_OWNER_ETH_ADDRESS = process.env.WBTC_OWNER_ETH_ADDRESS; + const wbtcOwner = await hre.ethers.getImpersonatedSigner(WBTC_OWNER_ETH_ADDRESS); + await setBalance(WBTC_OWNER_ETH_ADDRESS, 10n ** 18n); + + const amount = 4n * WBTC_DEC; + const maxGas = 10000000n; + const gasPriceBid = 60000000n; + const maxSubmissionCost = 100000000000000n; + const fee = 1000000000000000n; + await wbtc.connect(wbtcOwner).transfer(repayer, amount); + + const outputToken = networkConfig.ARBITRUM_ONE.Tokens.WBTC; + + const data = AbiCoder.defaultAbiCoder().encode( + ["uint256", "bytes"], + [maxSubmissionCost, "0x"], + ); + const extraData = AbiCoder.defaultAbiCoder().encode( + ["address", "uint256", "uint256", "bytes"], + [outputToken, maxGas, gasPriceBid, data] + ); + + const gatewayAddress = await arbitrumGatewayRouter.getGateway(wbtc.target); + const tx = repayer.connect(repayUser).initiateRepay( + wbtc, + amount, + liquidityPool, + Domain.ARBITRUM_ONE, + Provider.ARBITRUM_GATEWAY, + extraData, + {value: fee} + ); + await expect(tx) + .to.emit(repayer, "InitiateRepay") + .withArgs(wbtc.target, amount, liquidityPool.target, Domain.ARBITRUM_ONE, Provider.ARBITRUM_GATEWAY); + await expect(tx) + .to.emit(arbitrumGatewayRouter, "TransferRouted") + .withArgs(wbtc.target, repayer.target, liquidityPool.target, gatewayAddress); + expect(await wbtc.balanceOf(repayer)).to.equal(0n); + }); + + it("Should allow repayer to initiate Arbitrum Gateway DAI repay on fork", async function () { + const { + repayer, repayUser, liquidityPool, arbitrumGatewayRouter, dai, DAI_DEC + } = await loadFixture(deployAll); + + assertAddress(process.env.DAI_OWNER_ETH_ADDRESS, "Env variables not configured (DAI_OWNER_ETH_ADDRESS missing)"); + const DAI_OWNER_ETH_ADDRESS = process.env.DAI_OWNER_ETH_ADDRESS; + const daiOwner = await hre.ethers.getImpersonatedSigner(DAI_OWNER_ETH_ADDRESS); + await setBalance(DAI_OWNER_ETH_ADDRESS, 10n ** 18n); + + const amount = 4n * DAI_DEC; + const maxGas = 10000000n; + const gasPriceBid = 60000000n; + const maxSubmissionCost = 100000000000000n; + const fee = 1000000000000000n; + await dai.connect(daiOwner).transfer(repayer, amount); + + const outputToken = networkConfig.ARBITRUM_ONE.Tokens.DAI; + + const data = AbiCoder.defaultAbiCoder().encode( + ["uint256", "bytes"], + [maxSubmissionCost, "0x"], + ); + const extraData = AbiCoder.defaultAbiCoder().encode( + ["address", "uint256", "uint256", "bytes"], + [outputToken, maxGas, gasPriceBid, data] + ); + + const gatewayAddress = await arbitrumGatewayRouter.getGateway(dai.target); + const tx = repayer.connect(repayUser).initiateRepay( + dai, + amount, + liquidityPool, + Domain.ARBITRUM_ONE, + Provider.ARBITRUM_GATEWAY, + extraData, + {value: fee} + ); + await expect(tx) + .to.emit(repayer, "InitiateRepay") + .withArgs(dai.target, amount, liquidityPool.target, Domain.ARBITRUM_ONE, Provider.ARBITRUM_GATEWAY); + await expect(tx) + .to.emit(arbitrumGatewayRouter, "TransferRouted") + .withArgs(dai.target, repayer.target, liquidityPool.target, gatewayAddress); + expect(await dai.balanceOf(repayer)).to.equal(0n); + }); + + it("Should allow repayer to initiate Arbitrum Gateway WETH repay on fork", async function () { + const { + repayer, repayUser, liquidityPool, weth, arbitrumGatewayRouter, + } = await loadFixture(deployAll); + + const amount = 4n * ETH; + const maxGas = 10000000n; + const gasPriceBid = 60000000n; + const maxSubmissionCost = 100000000000000n; + const fee = 1000000000000000n; + await weth.connect(repayUser).deposit({value: amount}); + await weth.connect(repayUser).transfer(repayer, amount); + + const outputToken = networkConfig.ARBITRUM_ONE.Tokens.WETH; + + const data = AbiCoder.defaultAbiCoder().encode( + ["uint256", "bytes"], + [maxSubmissionCost, "0x"], + ); + const extraData = AbiCoder.defaultAbiCoder().encode( + ["address", "uint256", "uint256", "bytes"], + [outputToken, maxGas, gasPriceBid, data] + ); + + const gatewayAddress = await arbitrumGatewayRouter.getGateway(weth.target); + const tx = repayer.connect(repayUser).initiateRepay( + weth, + amount, + liquidityPool, + Domain.ARBITRUM_ONE, + Provider.ARBITRUM_GATEWAY, + extraData, + {value: fee} + ); + await expect(tx) + .to.emit(repayer, "InitiateRepay") + .withArgs(weth.target, amount, liquidityPool.target, Domain.ARBITRUM_ONE, Provider.ARBITRUM_GATEWAY); + await expect(tx) + .to.emit(arbitrumGatewayRouter, "TransferRouted") + .withArgs(weth.target, repayer.target, liquidityPool.target, gatewayAddress); + expect(await weth.balanceOf(repayer)).to.equal(0n); + }); + + it("Should revert Arbitrum Gateway repay on fork if output tokens don't match", async function () { + const { + repayer, repayUser, liquidityPool, usdc, USDC_DEC, + } = await loadFixture(deployAll); + + const amount = 4n * USDC_DEC; + const maxGas = 10000000n; + const gasPriceBid = 60000000n; + const maxSubmissionCost = 100000000000000n; + const fee = 1000000000000000n; + + assertAddress(process.env.USDC_OWNER_ETH_ADDRESS, "Env variables not configured (USDC_OWNER_ETH_ADDRESS missing)"); + const USDC_OWNER_ETH_ADDRESS = process.env.USDC_OWNER_ETH_ADDRESS; + const usdcOwner = await hre.ethers.getImpersonatedSigner(USDC_OWNER_ETH_ADDRESS); + await setBalance(USDC_OWNER_ETH_ADDRESS, 10n ** 18n); + + await usdc.connect(usdcOwner).transfer(repayer, 10n * USDC_DEC); + await usdc.connect(usdcOwner).transfer(repayer, amount); + + const outputToken = networkConfig.ARBITRUM_ONE.Tokens.USDC; + + const data = AbiCoder.defaultAbiCoder().encode( + ["uint256", "bytes"], + [maxSubmissionCost, "0x"], + ); + const extraData = AbiCoder.defaultAbiCoder().encode( + ["address", "uint256", "uint256", "bytes"], + [outputToken, maxGas, gasPriceBid, data] + ); + + await expect(repayer.connect(repayUser).initiateRepay( + usdc, + amount, + liquidityPool, + Domain.ARBITRUM_ONE, + Provider.ARBITRUM_GATEWAY, + extraData, + {value: fee} + )).to.be.revertedWithCustomError(repayer, "InvalidOutputToken()"); + }); }); diff --git a/test/Repayer.ts b/test/Repayer.ts index df2e265..c1cd50c 100644 --- a/test/Repayer.ts +++ b/test/Repayer.ts @@ -3,7 +3,7 @@ import { } from "@nomicfoundation/hardhat-toolbox/network-helpers"; import {expect} from "chai"; import hre from "hardhat"; -import {AbiCoder} from "ethers"; +import {AbiCoder, hexlify, toUtf8Bytes} from "ethers"; import {anyValue} from "@nomicfoundation/hardhat-chai-matchers/withArgs"; import { getCreateAddress, getContractAt, deploy, deployX, toBytes32, getBalance, @@ -16,7 +16,7 @@ import { TestUSDC, TransparentUpgradeableProxy, ProxyAdmin, TestLiquidityPool, Repayer, TestCCTPTokenMessenger, TestCCTPMessageTransmitter, TestAcrossV3SpokePool, TestStargate, MockStargateTreasurerTrue, MockStargateTreasurerFalse, - TestSuperchainStandardBridge, IWrappedNativeToken + TestSuperchainStandardBridge, IWrappedNativeToken, TestArbitrumGatewayRouter } from "../typechain-types"; import {networkConfig} from "../network.config"; @@ -72,6 +72,10 @@ describe("Repayer", function () { const baseBridge = ( await deploy("TestSuperchainStandardBridge", deployer, {}) ) as TestSuperchainStandardBridge; + const l2TokenAddress = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"; + const arbitrumGatewayRouter = ( + await deploy("TestArbitrumGatewayRouter", deployer, {}, usdc.target, l2TokenAddress) + ) as TestArbitrumGatewayRouter; const USDC_DEC = 10n ** (await usdc.decimals()); @@ -100,6 +104,7 @@ describe("Repayer", function () { stargateTreasurerTrue, optimismBridge, baseBridge, + arbitrumGatewayRouter ) ) as Repayer; const repayerInit = (await repayerImpl.initialize.populateTransaction( @@ -138,7 +143,7 @@ describe("Repayer", function () { USDC_DEC, eurc, EURC_DEC, eurcOwner, liquidityPool, liquidityPool2, repayer, repayerProxy, repayerAdmin, cctpTokenMessenger, cctpMessageTransmitter, REPAYER_ROLE, DEFAULT_ADMIN_ROLE, acrossV3SpokePool, weth, stargateTreasurerTrue, stargateTreasurerFalse, everclearFeeAdapter, forkNetworkConfig, optimismBridge, - baseBridge, setTokensUser, + baseBridge, setTokensUser, arbitrumGatewayRouter, l2TokenAddress, }; }; @@ -695,7 +700,7 @@ describe("Repayer", function () { it("Should allow repayer to initiate Across repay with SpokePool on fork", async function () { const {deployer, repayer, USDC_DEC, admin, repayUser, repayerAdmin, repayerProxy, liquidityPool, cctpTokenMessenger, cctpMessageTransmitter, weth, stargateTreasurerTrue, everclearFeeAdapter, - optimismBridge, baseBridge, setTokensUser, + optimismBridge, baseBridge, setTokensUser, arbitrumGatewayRouter, } = await loadFixture(deployAll); const acrossV3SpokePoolFork = await hre.ethers.getContractAt( @@ -725,6 +730,7 @@ describe("Repayer", function () { stargateTreasurerTrue, optimismBridge, baseBridge, + arbitrumGatewayRouter, ) ) as Repayer; @@ -1127,7 +1133,7 @@ describe("Repayer", function () { const { USDC_DEC, usdc, repayUser, liquidityPool, optimismBridge, cctpTokenMessenger, cctpMessageTransmitter, acrossV3SpokePool, everclearFeeAdapter, weth, stargateTreasurerTrue, admin, deployer, baseBridge, - setTokensUser, + setTokensUser, arbitrumGatewayRouter, } = await loadFixture(deployAll); const amount = 4n * USDC_DEC; const outputToken = networkConfig.OP_MAINNET.Tokens.USDC; @@ -1145,6 +1151,7 @@ describe("Repayer", function () { stargateTreasurerTrue, optimismBridge, baseBridge, + arbitrumGatewayRouter, ) ) as Repayer; const repayerInit = (await repayerImpl.initialize.populateTransaction( @@ -1194,7 +1201,7 @@ describe("Repayer", function () { const { USDC_DEC, usdc, repayUser, liquidityPool, optimismBridge, cctpTokenMessenger, cctpMessageTransmitter, acrossV3SpokePool, everclearFeeAdapter, weth, stargateTreasurerTrue, admin, deployer, baseBridge, - setTokensUser, + setTokensUser, arbitrumGatewayRouter, } = await loadFixture(deployAll); const amount = 4n * USDC_DEC; const outputToken = networkConfig.BASE.Tokens.USDC; @@ -1212,6 +1219,7 @@ describe("Repayer", function () { stargateTreasurerTrue, optimismBridge, baseBridge, + arbitrumGatewayRouter, ) ) as Repayer; const repayerInit = (await repayerImpl.initialize.populateTransaction( @@ -1261,7 +1269,7 @@ describe("Repayer", function () { const { USDC_DEC, usdc, repayUser, liquidityPool, optimismBridge, cctpTokenMessenger, cctpMessageTransmitter, acrossV3SpokePool, everclearFeeAdapter, weth, stargateTreasurerTrue, admin, deployer, baseBridge, - setTokensUser, + setTokensUser, arbitrumGatewayRouter, } = await loadFixture(deployAll); const repayerImpl = ( @@ -1276,6 +1284,7 @@ describe("Repayer", function () { stargateTreasurerTrue, optimismBridge, baseBridge, + arbitrumGatewayRouter, ) ) as Repayer; const repayerInit = (await repayerImpl.initialize.populateTransaction( @@ -1324,7 +1333,7 @@ describe("Repayer", function () { const { USDC_DEC, usdc, repayUser, liquidityPool, optimismBridge, cctpTokenMessenger, cctpMessageTransmitter, acrossV3SpokePool, everclearFeeAdapter, weth, stargateTreasurerTrue, admin, deployer, baseBridge, - setTokensUser, + setTokensUser, arbitrumGatewayRouter, } = await loadFixture(deployAll); const repayerImpl = ( @@ -1339,6 +1348,7 @@ describe("Repayer", function () { stargateTreasurerTrue, optimismBridge, baseBridge, + arbitrumGatewayRouter, ) ) as Repayer; const repayerInit = (await repayerImpl.initialize.populateTransaction( @@ -1388,7 +1398,7 @@ describe("Repayer", function () { const { USDC_DEC, usdc, repayUser, liquidityPool, optimismBridge, cctpTokenMessenger, cctpMessageTransmitter, acrossV3SpokePool, everclearFeeAdapter, weth, stargateTreasurerTrue, admin, deployer, baseBridge, - setTokensUser, + setTokensUser, arbitrumGatewayRouter, } = await loadFixture(deployAll); const repayerImpl = ( @@ -1403,6 +1413,7 @@ describe("Repayer", function () { stargateTreasurerTrue, optimismBridge, baseBridge, + arbitrumGatewayRouter, ) ) as Repayer; const repayerInit = (await repayerImpl.initialize.populateTransaction( @@ -1473,6 +1484,666 @@ describe("Repayer", function () { .to.be.revertedWithCustomError(repayer, "UnsupportedDomain"); }); + it("Should allow repayer to initiate Arbitrum Gateway repay with mock bridge", async function () { + const { + USDC_DEC, usdc, repayUser, liquidityPool, optimismBridge, cctpTokenMessenger, cctpMessageTransmitter, + acrossV3SpokePool, everclearFeeAdapter, weth, stargateTreasurerTrue, admin, deployer, baseBridge, + setTokensUser, l2TokenAddress, arbitrumGatewayRouter + } = await loadFixture(deployAll); + const amount = 4n * USDC_DEC; + const maxGas = 10000000n; + const gasPriceBid = 1000000000n; + const maxSubmissionCost = 100000000000000n; + + const repayerImpl = ( + await deployX("Repayer", deployer, "Repayer2", {}, + Domain.ETHEREUM, + usdc, + cctpTokenMessenger, + cctpMessageTransmitter, + acrossV3SpokePool, + everclearFeeAdapter, + weth, + stargateTreasurerTrue, + optimismBridge, + baseBridge, + arbitrumGatewayRouter, + ) + ) as Repayer; + const repayerInit = (await repayerImpl.initialize.populateTransaction( + admin, + repayUser, + setTokensUser, + [liquidityPool, liquidityPool], + [Domain.ETHEREUM, Domain.ARBITRUM_ONE], + [Provider.LOCAL, Provider.ARBITRUM_GATEWAY], + [true, true], + [{ + inputToken: usdc, + destinationTokens: [ + {destinationDomain: Domain.ARBITRUM_ONE, outputToken: addressToBytes32(l2TokenAddress)} + ] + }], + )).data; + const repayerProxy = (await deployX( + "TransparentUpgradeableProxy", deployer, "TransparentUpgradeableProxyRepayer2", {}, + repayerImpl, admin, repayerInit + )) as TransparentUpgradeableProxy; + const repayer = (await getContractAt("Repayer", repayerProxy, deployer)) as Repayer; + + await usdc.transfer(repayer, 10n * USDC_DEC); + + const data = AbiCoder.defaultAbiCoder().encode( + ["uint256", "bytes"], + [maxSubmissionCost, "0x"], + ); + const extraData = AbiCoder.defaultAbiCoder().encode( + ["address", "uint256", "uint256", "bytes"], + [l2TokenAddress, maxGas, gasPriceBid, data] + ); + + const tx = repayer.connect(repayUser).initiateRepay( + usdc, + amount, + liquidityPool, + Domain.ARBITRUM_ONE, + Provider.ARBITRUM_GATEWAY, + extraData + ); + await expect(tx) + .to.emit(repayer, "InitiateRepay") + .withArgs(usdc.target, amount, liquidityPool.target, Domain.ARBITRUM_ONE, Provider.ARBITRUM_GATEWAY); + await expect(tx) + .to.emit(repayer, "ArbitrumERC20TransferInitiated") + .withArgs(hexlify(toUtf8Bytes("GATEWAY_DATA"))); + await expect(tx) + .to.emit(arbitrumGatewayRouter, "TransferRouted") + .withArgs(usdc.target, repayer.target, liquidityPool.target, arbitrumGatewayRouter.target); + }); + + it("Should revert Arbitrum Gateway repay if output token doesn't match", async function () { + const { + USDC_DEC, usdc, repayUser, liquidityPool, optimismBridge, cctpTokenMessenger, cctpMessageTransmitter, + acrossV3SpokePool, everclearFeeAdapter, weth, stargateTreasurerTrue, admin, deployer, baseBridge, + setTokensUser, arbitrumGatewayRouter + } = await loadFixture(deployAll); + const amount = 4n * USDC_DEC; + const maxGas = 10000000n; + const gasPriceBid = 1000000000n; + const maxSubmissionCost = 100000000000000n; + + const repayerImpl = ( + await deployX("Repayer", deployer, "Repayer2", {}, + Domain.ETHEREUM, + usdc, + cctpTokenMessenger, + cctpMessageTransmitter, + acrossV3SpokePool, + everclearFeeAdapter, + weth, + stargateTreasurerTrue, + optimismBridge, + baseBridge, + arbitrumGatewayRouter, + ) + ) as Repayer; + const repayerInit = (await repayerImpl.initialize.populateTransaction( + admin, + repayUser, + setTokensUser, + [liquidityPool, liquidityPool], + [Domain.ETHEREUM, Domain.ARBITRUM_ONE], + [Provider.LOCAL, Provider.ARBITRUM_GATEWAY], + [true, true], + [{ + inputToken: usdc, + destinationTokens: [ + {destinationDomain: Domain.ARBITRUM_ONE, outputToken: addressToBytes32(weth.target)} + ] + }], + )).data; + const repayerProxy = (await deployX( + "TransparentUpgradeableProxy", deployer, "TransparentUpgradeableProxyRepayer2", {}, + repayerImpl, admin, repayerInit + )) as TransparentUpgradeableProxy; + const repayer = (await getContractAt("Repayer", repayerProxy, deployer)) as Repayer; + + await usdc.transfer(repayer, 10n * USDC_DEC); + + const data = AbiCoder.defaultAbiCoder().encode( + ["uint256", "bytes"], + [maxSubmissionCost, "0x"], + ); + const extraData = AbiCoder.defaultAbiCoder().encode( + ["address", "uint256", "uint256", "bytes"], + [weth.target, maxGas, gasPriceBid, data] + ); + + await expect(repayer.connect(repayUser).initiateRepay( + usdc, + amount, + liquidityPool, + Domain.ARBITRUM_ONE, + Provider.ARBITRUM_GATEWAY, + extraData + )).to.be.revertedWithCustomError(repayer, "InvalidOutputToken()"); + }); + + it("Should revert Arbitrum Gateway repay if call to Arbitrum Gateway reverts", async function () { + const { + USDC_DEC, usdc, repayUser, liquidityPool, optimismBridge, cctpTokenMessenger, cctpMessageTransmitter, + acrossV3SpokePool, everclearFeeAdapter, weth, stargateTreasurerTrue, admin, deployer, baseBridge, + setTokensUser, l2TokenAddress, arbitrumGatewayRouter + } = await loadFixture(deployAll); + + // Deploy repayer configured to use Arbitrum Gateway Router + const repayerImpl = ( + await deployX("Repayer", deployer, "Repayer2", {}, + Domain.ETHEREUM, + usdc, + cctpTokenMessenger, + cctpMessageTransmitter, + acrossV3SpokePool, + everclearFeeAdapter, + weth, + stargateTreasurerTrue, + optimismBridge, + baseBridge, + arbitrumGatewayRouter, + ) + ) as Repayer; + const repayerInit = (await repayerImpl.initialize.populateTransaction( + admin, + repayUser, + setTokensUser, + [liquidityPool, liquidityPool], + [Domain.ETHEREUM, Domain.ARBITRUM_ONE], + [Provider.LOCAL, Provider.ARBITRUM_GATEWAY], + [true, true], + [{ + inputToken: usdc, + destinationTokens: [ + {destinationDomain: Domain.ARBITRUM_ONE, outputToken: addressToBytes32(l2TokenAddress)} + ] + }], + )).data; + const repayerProxy = (await deployX( + "TransparentUpgradeableProxy", deployer, "TransparentUpgradeableProxyRepayer2", {}, + repayerImpl, admin, repayerInit + )) as TransparentUpgradeableProxy; + const repayer = (await getContractAt("Repayer", repayerProxy, deployer)) as Repayer; + + await usdc.transfer(repayer, 10n * USDC_DEC); + + // Use amount 2000 to trigger the mock router revert + const amount = 2000n; + const maxGas = 10000000n; + const gasPriceBid = 1000000000n; + const maxSubmissionCost = 100000000000000n; + + const data = AbiCoder.defaultAbiCoder().encode( + ["uint256", "bytes"], + [maxSubmissionCost, "0x"], + ); + const extraData = AbiCoder.defaultAbiCoder().encode( + ["address", "uint256", "uint256", "bytes"], + [l2TokenAddress, maxGas, gasPriceBid, data] + ); + + await expect(repayer.connect(repayUser).initiateRepay( + usdc, + amount, + liquidityPool, + Domain.ARBITRUM_ONE, + Provider.ARBITRUM_GATEWAY, + extraData + )).to.be.reverted; + }); + + it("Should initiate Arbitrum Gateway repay with wrapped native currency", async function () { + const { + usdc, repayUser, liquidityPool, optimismBridge, cctpTokenMessenger, cctpMessageTransmitter, + acrossV3SpokePool, everclearFeeAdapter, weth, stargateTreasurerTrue, admin, deployer, baseBridge, + setTokensUser, l2TokenAddress, + } = await loadFixture(deployAll); + const amount = 100000n; + const maxGas = 10000000n; + const gasPriceBid = 1000000000n; + const maxSubmissionCost = 100000000000000n; + + const arbitrumGatewayRouter = ( + await deploy("TestArbitrumGatewayRouter", deployer, {}, weth.target, l2TokenAddress) + ) as TestArbitrumGatewayRouter; + + const repayerImpl = ( + await deployX("Repayer", deployer, "Repayer2", {}, + Domain.ETHEREUM, + usdc, + cctpTokenMessenger, + cctpMessageTransmitter, + acrossV3SpokePool, + everclearFeeAdapter, + weth, + stargateTreasurerTrue, + optimismBridge, + baseBridge, + arbitrumGatewayRouter, + ) + ) as Repayer; + const repayerInit = (await repayerImpl.initialize.populateTransaction( + admin, + repayUser, + setTokensUser, + [liquidityPool, liquidityPool], + [Domain.ETHEREUM, Domain.ARBITRUM_ONE], + [Provider.LOCAL, Provider.ARBITRUM_GATEWAY], + [true, true], + [{ + inputToken: weth, + destinationTokens: [ + {destinationDomain: Domain.ARBITRUM_ONE, outputToken: addressToBytes32(l2TokenAddress)} + ] + }], + )).data; + const repayerProxy = (await deployX( + "TransparentUpgradeableProxy", deployer, "TransparentUpgradeableProxyRepayer2", {}, + repayerImpl, admin, repayerInit + )) as TransparentUpgradeableProxy; + const repayer = (await getContractAt("Repayer", repayerProxy, deployer)) as Repayer; + + await weth.connect(repayUser).deposit({value: amount}); + await weth.connect(repayUser).transfer(repayer, amount); + + const data = AbiCoder.defaultAbiCoder().encode( + ["uint256", "bytes"], + [maxSubmissionCost, "0x"], + ); + const extraData = AbiCoder.defaultAbiCoder().encode( + ["address", "uint256", "uint256", "bytes"], + [l2TokenAddress, maxGas, gasPriceBid, data] + ); + const tx = repayer.connect(repayUser).initiateRepay( + weth, + amount, + liquidityPool, + Domain.ARBITRUM_ONE, + Provider.ARBITRUM_GATEWAY, + extraData + ); + await expect(tx) + .to.emit(repayer, "InitiateRepay") + .withArgs(weth.target, amount, liquidityPool.target, Domain.ARBITRUM_ONE, Provider.ARBITRUM_GATEWAY); + await expect(tx) + .to.emit(arbitrumGatewayRouter, "TransferRouted") + .withArgs(weth.target, repayer.target, liquidityPool.target, arbitrumGatewayRouter.target); + }); + + it("Should revert Arbitrum Gateway repay if output token doesn't match the gateway token", async function () { + const { + USDC_DEC, usdc, repayUser, liquidityPool, optimismBridge, cctpTokenMessenger, cctpMessageTransmitter, + acrossV3SpokePool, everclearFeeAdapter, weth, stargateTreasurerTrue, admin, deployer, baseBridge, + setTokensUser, l2TokenAddress, arbitrumGatewayRouter + } = await loadFixture(deployAll); + + // Deploy repayer configured to use Arbitrum Gateway Router + const repayerImpl = ( + await deployX("Repayer", deployer, "Repayer2", {}, + Domain.ETHEREUM, + usdc, + cctpTokenMessenger, + cctpMessageTransmitter, + acrossV3SpokePool, + everclearFeeAdapter, + weth, + stargateTreasurerTrue, + optimismBridge, + baseBridge, + arbitrumGatewayRouter, + ) + ) as Repayer; + + const wrongOutputToken = weth.target; + const repayerInit = (await repayerImpl.initialize.populateTransaction( + admin, + repayUser, + setTokensUser, + [liquidityPool, liquidityPool], + [Domain.ETHEREUM, Domain.ARBITRUM_ONE], + [Provider.LOCAL, Provider.ARBITRUM_GATEWAY], + [true, true], + [{ + inputToken: usdc, + destinationTokens: [ + {destinationDomain: Domain.ARBITRUM_ONE, outputToken: addressToBytes32(wrongOutputToken)} + ] + }], + )).data; + const repayerProxy = (await deployX( + "TransparentUpgradeableProxy", deployer, "TransparentUpgradeableProxyRepayer2", {}, + repayerImpl, admin, repayerInit + )) as TransparentUpgradeableProxy; + const repayer = (await getContractAt("Repayer", repayerProxy, deployer)) as Repayer; + + await usdc.transfer(repayer, 10n * USDC_DEC); + + // Use amount 2000 to trigger the mock router revert + const amount = 2000n; + const maxGas = 10000000n; + const gasPriceBid = 1000000000n; + const maxSubmissionCost = 100000000000000n; + + const data = AbiCoder.defaultAbiCoder().encode( + ["uint256", "bytes"], + [maxSubmissionCost, "0x"], + ); + const extraData = AbiCoder.defaultAbiCoder().encode( + ["address", "uint256", "uint256", "bytes"], + [l2TokenAddress, maxGas, gasPriceBid, data] + ); + + await expect(repayer.connect(repayUser).initiateRepay( + usdc, + amount, + liquidityPool, + Domain.ARBITRUM_ONE, + Provider.ARBITRUM_GATEWAY, + extraData + )).to.be.revertedWithCustomError(repayer, "InvalidOutputToken"); + }); + + it("Should revert Arbitrum Gateway repay if output token is not allowed", async function () { + const { + USDC_DEC, usdc, repayUser, liquidityPool, optimismBridge, cctpTokenMessenger, cctpMessageTransmitter, + acrossV3SpokePool, everclearFeeAdapter, weth, stargateTreasurerTrue, admin, deployer, baseBridge, + setTokensUser, arbitrumGatewayRouter + } = await loadFixture(deployAll); + + const repayerImpl = ( + await deployX("Repayer", deployer, "Repayer2", {}, + Domain.ETHEREUM, + usdc, + cctpTokenMessenger, + cctpMessageTransmitter, + acrossV3SpokePool, + everclearFeeAdapter, + weth, + stargateTreasurerTrue, + optimismBridge, + baseBridge, + arbitrumGatewayRouter + ) + ) as Repayer; + const repayerInit = (await repayerImpl.initialize.populateTransaction( + admin, + repayUser, + setTokensUser, + [liquidityPool, liquidityPool], + [Domain.ETHEREUM, Domain.ARBITRUM_ONE], + [Provider.LOCAL, Provider.ARBITRUM_GATEWAY], + [true, true], + [], + )).data; + const repayerProxy = (await deployX( + "TransparentUpgradeableProxy", deployer, "TransparentUpgradeableProxyRepayer2", {}, + repayerImpl, admin, repayerInit + )) as TransparentUpgradeableProxy; + const repayer = (await getContractAt("Repayer", repayerProxy, deployer)) as Repayer; + + await usdc.transfer(repayer, 10n * USDC_DEC); + + const amount = 4n * USDC_DEC; + const outputToken = ZERO_ADDRESS; + const minGasLimit = 100000n; + const maxSubmissionCost = 100000000000000n; + const data = AbiCoder.defaultAbiCoder().encode( + ["uint256", "bytes"], + [maxSubmissionCost, "0x"], + ); + const extraData = AbiCoder.defaultAbiCoder().encode( + ["address", "uint32", "bytes"], + [outputToken, minGasLimit, data], + ); + const tx = repayer.connect(repayUser).initiateRepay( + usdc, + amount, + liquidityPool, + Domain.ARBITRUM_ONE, + Provider.ARBITRUM_GATEWAY, + extraData + ); + await expect(tx) + .to.be.revertedWithCustomError(repayer, "InvalidOutputToken()"); + }); + + it("Should NOT allow repayer to initiate Arbitrum Gateway repay on invalid route", async function () { + const { + USDC_DEC, usdc, repayUser, liquidityPool, optimismBridge, cctpTokenMessenger, cctpMessageTransmitter, + acrossV3SpokePool, everclearFeeAdapter, weth, stargateTreasurerTrue, admin, deployer, baseBridge, + setTokensUser, arbitrumGatewayRouter, l2TokenAddress + } = await loadFixture(deployAll); + + const repayerImpl = ( + await deployX("Repayer", deployer, "Repayer2", {}, + Domain.ETHEREUM, + usdc, + cctpTokenMessenger, + cctpMessageTransmitter, + acrossV3SpokePool, + everclearFeeAdapter, + weth, + stargateTreasurerTrue, + optimismBridge, + baseBridge, + arbitrumGatewayRouter + ) + ) as Repayer; + const repayerInit = (await repayerImpl.initialize.populateTransaction( + admin, + repayUser, + setTokensUser, + [liquidityPool], + [Domain.ETHEREUM], + [Provider.LOCAL], + [true], + [], + )).data; + const repayerProxy = (await deployX( + "TransparentUpgradeableProxy", deployer, "TransparentUpgradeableProxyRepayer2", {}, + repayerImpl, admin, repayerInit + )) as TransparentUpgradeableProxy; + const repayer = (await getContractAt("Repayer", repayerProxy, deployer)) as Repayer; + + const amount = 4n * USDC_DEC; + + await usdc.transfer(repayer, 10n * USDC_DEC); + const maxGas = 10000000n; + const gasPriceBid = 1000000000n; + const maxSubmissionCost = 100000000000000n; + const data = AbiCoder.defaultAbiCoder().encode( + ["uint256", "bytes"], + [maxSubmissionCost, "0x"], + ); + const extraData = AbiCoder.defaultAbiCoder().encode( + ["address", "uint256", "uint256", "bytes"], + [l2TokenAddress, maxGas, gasPriceBid, data] + ); + const tx = repayer.connect(repayUser).initiateRepay( + usdc, + amount, + liquidityPool, + Domain.ARBITRUM_ONE, + Provider.ARBITRUM_GATEWAY, + extraData + ); + await expect(tx) + .to.be.revertedWithCustomError(repayer, "RouteDenied"); + }); + + it("Should NOT allow repayer to initiate Arbitrum Gateway repay if local domain is not ETHEREUM", async function () { + const {admin, USDC_DEC, usdc, repayUser, liquidityPool, repayer, l2TokenAddress} = await loadFixture(deployAll); + + await repayer.connect(admin).setRoute( + [liquidityPool], + [Domain.ARBITRUM_ONE], + [Provider.ARBITRUM_GATEWAY], + [true], + ALLOWED + ); + + const amount = 4n * USDC_DEC; + + await usdc.transfer(repayer, 10n * USDC_DEC); + const maxGas = 10000000n; + const gasPriceBid = 1000000000n; + const maxSubmissionCost = 100000000000000n; + const data = AbiCoder.defaultAbiCoder().encode( + ["uint256", "bytes"], + [maxSubmissionCost, "0x"], + ); + const extraData = AbiCoder.defaultAbiCoder().encode( + ["address", "uint256", "uint256", "bytes"], + [l2TokenAddress, maxGas, gasPriceBid, data] + ); + const tx = repayer.connect(repayUser).initiateRepay( + usdc, + amount, + liquidityPool, + Domain.ARBITRUM_ONE, + Provider.ARBITRUM_GATEWAY, + extraData + ); + await expect(tx) + .to.be.revertedWithCustomError(repayer, "UnsupportedDomain"); + }); + + it("Should NOT initiate Arbitrum Gateway repay if destination domain is not ARBITRUM_ONE", async function () { + const {USDC_DEC, usdc, repayUser, liquidityPool, optimismBridge, cctpTokenMessenger, cctpMessageTransmitter, + acrossV3SpokePool, everclearFeeAdapter, weth, stargateTreasurerTrue, admin, deployer, baseBridge, + setTokensUser, arbitrumGatewayRouter, l2TokenAddress} = await loadFixture(deployAll); + + const repayerImpl = ( + await deployX("Repayer", deployer, "Repayer2", {}, + Domain.ETHEREUM, + usdc, + cctpTokenMessenger, + cctpMessageTransmitter, + acrossV3SpokePool, + everclearFeeAdapter, + weth, + stargateTreasurerTrue, + optimismBridge, + baseBridge, + arbitrumGatewayRouter + ) + ) as Repayer; + const repayerInit = (await repayerImpl.initialize.populateTransaction( + admin, + repayUser, + setTokensUser, + [liquidityPool, liquidityPool], + [Domain.ETHEREUM, Domain.BASE], + [Provider.LOCAL, Provider.ARBITRUM_GATEWAY], + [true, true], + [], + )).data; + const repayerProxy = (await deployX( + "TransparentUpgradeableProxy", deployer, "TransparentUpgradeableProxyRepayer2", {}, + repayerImpl, admin, repayerInit + )) as TransparentUpgradeableProxy; + const repayer = (await getContractAt("Repayer", repayerProxy, deployer)) as Repayer; + + const amount = 4n * USDC_DEC; + + await usdc.transfer(repayer, 10n * USDC_DEC); + const maxGas = 10000000n; + const gasPriceBid = 1000000000n; + const maxSubmissionCost = 100000000000000n; + const data = AbiCoder.defaultAbiCoder().encode( + ["uint256", "bytes"], + [maxSubmissionCost, "0x"], + ); + const extraData = AbiCoder.defaultAbiCoder().encode( + ["address", "uint256", "uint256", "bytes"], + [l2TokenAddress, maxGas, gasPriceBid, data] + ); + const tx = repayer.connect(repayUser).initiateRepay( + usdc, + amount, + liquidityPool, + Domain.BASE, + Provider.ARBITRUM_GATEWAY, + extraData + ); + await expect(tx) + .to.be.revertedWithCustomError(repayer, "UnsupportedDomain"); + }); + + it("Should revert Arbitrum Gateway repay if router address is 0", async function () { + const { + USDC_DEC, usdc, repayUser, liquidityPool, optimismBridge, cctpTokenMessenger, cctpMessageTransmitter, + acrossV3SpokePool, everclearFeeAdapter, weth, stargateTreasurerTrue, admin, deployer, baseBridge, + setTokensUser, l2TokenAddress + } = await loadFixture(deployAll); + + const repayerImpl = ( + await deployX("Repayer", deployer, "Repayer2", {}, + Domain.ETHEREUM, + usdc, + cctpTokenMessenger, + cctpMessageTransmitter, + acrossV3SpokePool, + everclearFeeAdapter, + weth, + stargateTreasurerTrue, + optimismBridge, + baseBridge, + ZERO_ADDRESS + ) + ) as Repayer; + const repayerInit = (await repayerImpl.initialize.populateTransaction( + admin, + repayUser, + setTokensUser, + [liquidityPool, liquidityPool], + [Domain.ETHEREUM, Domain.ARBITRUM_ONE], + [Provider.LOCAL, Provider.ARBITRUM_GATEWAY], + [true, true], + [], + )).data; + const repayerProxy = (await deployX( + "TransparentUpgradeableProxy", deployer, "TransparentUpgradeableProxyRepayer2", {}, + repayerImpl, admin, repayerInit + )) as TransparentUpgradeableProxy; + const repayer = (await getContractAt("Repayer", repayerProxy, deployer)) as Repayer; + + await usdc.transfer(repayer, 10n * USDC_DEC); + + const amount = 4n * USDC_DEC; + + await usdc.transfer(repayer, 10n * USDC_DEC); + const maxGas = 10000000n; + const gasPriceBid = 1000000000n; + const maxSubmissionCost = 100000000000000n; + const data = AbiCoder.defaultAbiCoder().encode( + ["uint256", "bytes"], + [maxSubmissionCost, "0x"], + ); + const extraData = AbiCoder.defaultAbiCoder().encode( + ["address", "uint256", "uint256", "bytes"], + [l2TokenAddress, maxGas, gasPriceBid, data] + ); + const tx = repayer.connect(repayUser).initiateRepay( + usdc, + amount, + liquidityPool, + Domain.ARBITRUM_ONE, + Provider.ARBITRUM_GATEWAY, + extraData + ); + await expect(tx) + .to.be.revertedWithCustomError(repayer, "ZeroAddress"); + }); + it("Should allow repayer to initiate repay of a different token", async function () { const {repayer, eurc, EURC_DEC, eurcOwner, repayUser, liquidityPool } = await loadFixture(deployAll); @@ -1750,7 +2421,7 @@ describe("Repayer", function () { it("Should revert Stargate repay if the pool is not registered", async function () { const {repayer, USDC_DEC, usdc, admin, repayUser, liquidityPool, deployer, cctpTokenMessenger, cctpMessageTransmitter, acrossV3SpokePool, weth, stargateTreasurerFalse, repayerAdmin, repayerProxy, - everclearFeeAdapter, optimismBridge, baseBridge, + everclearFeeAdapter, optimismBridge, baseBridge, arbitrumGatewayRouter, } = await loadFixture(deployAll); await usdc.transfer(repayer, 10n * USDC_DEC); @@ -1776,6 +2447,7 @@ describe("Repayer", function () { stargateTreasurerFalse, optimismBridge, baseBridge, + arbitrumGatewayRouter, ) ) as Repayer; @@ -1883,6 +2555,7 @@ describe("Repayer", function () { const { repayer, USDC_DEC, admin, repayUser, liquidityPool, deployer, cctpTokenMessenger, cctpMessageTransmitter, acrossV3SpokePool, weth, repayerAdmin, repayerProxy, everclearFeeAdapter, optimismBridge, baseBridge, + arbitrumGatewayRouter, } = await loadFixture(deployAll); const stargatePoolUsdcAddress = "0x27a16dc786820B16E5c9028b75B99F6f604b5d26"; @@ -1914,6 +2587,7 @@ describe("Repayer", function () { stargateTreasurer, optimismBridge, baseBridge, + arbitrumGatewayRouter, ) ) as Repayer; @@ -2052,7 +2726,7 @@ describe("Repayer", function () { it("Should unwrap enough native tokens on initiate repay", async function () { const { repayer, repayUser, liquidityPool, optimismBridge, usdc, cctpTokenMessenger, - cctpMessageTransmitter, repayerAdmin, admin, repayerProxy, deployer, baseBridge, + cctpMessageTransmitter, repayerAdmin, admin, repayerProxy, deployer, baseBridge, arbitrumGatewayRouter, } = await loadFixture(deployAll); const wrappedAmount = 10n * ETH; @@ -2081,6 +2755,7 @@ describe("Repayer", function () { ZERO_ADDRESS, optimismBridge, baseBridge, + arbitrumGatewayRouter, ) ) as Repayer; From a99aa0b1f6ae30b9a7bd7ef3df81df7b2b72c4ad Mon Sep 17 00:00:00 2001 From: viatrix Date: Fri, 12 Dec 2025 00:13:41 +0200 Subject: [PATCH 02/19] Update scripts and coverage --- .env.example | 2 ++ coverage-baseline.json | 8 ++++---- network.config.ts | 1 + scripts/deploy.ts | 4 ++++ scripts/deployRepayer.ts | 4 ++++ scripts/deployStandaloneRepayer.ts | 4 ++++ scripts/upgradeRepayer.ts | 4 ++++ 7 files changed, 23 insertions(+), 4 deletions(-) diff --git a/.env.example b/.env.example index db70506..a45c154 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/coverage-baseline.json b/coverage-baseline.json index 0778ddc..74a045e 100644 --- a/coverage-baseline.json +++ b/coverage-baseline.json @@ -1,6 +1,6 @@ { - "lines": "96.88", - "functions": "98.57", - "branches": "87.76", - "statements": "96.88" + "lines": "96.93", + "functions": "98.58", + "branches": "87.79", + "statements": "96.93" } \ No newline at end of file diff --git a/network.config.ts b/network.config.ts index 93b0db8..45ba89b 100644 --- a/network.config.ts +++ b/network.config.ts @@ -1510,6 +1510,7 @@ export interface StandaloneRepayerConfig { EverclearFeeAdapter?: string; OptimismStandardBridge?: string; BaseStandardBridge?: string; + ArbitrumGatewayRouter?: string; // Repayer tokens are used from the general network config. WrappedNativeToken: string; RepayerRoutes: RepayerRoutesConfig; diff --git a/scripts/deploy.ts b/scripts/deploy.ts index 633acd8..e35d666 100644 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -124,6 +124,9 @@ export async function main() { if (!config.BaseStandardBridge) { config.BaseStandardBridge = ZERO_ADDRESS; } + if (!config.ArbitrumGatewayRouter) { + config.ArbitrumGatewayRouter = ZERO_ADDRESS; + } let mainPool: LiquidityPool | undefined = undefined; let aavePoolLongTerm: LiquidityPoolAaveLongTerm; @@ -412,6 +415,7 @@ export async function main() { config.StargateTreasurer, config.OptimismStandardBridge, config.BaseStandardBridge, + config.ArbitrumGatewayRouter, ], [ config.Admin, diff --git a/scripts/deployRepayer.ts b/scripts/deployRepayer.ts index 2bff6f2..0ecfc3d 100644 --- a/scripts/deployRepayer.ts +++ b/scripts/deployRepayer.ts @@ -73,6 +73,9 @@ export async function main() { if (!config.BaseStandardBridge) { config.BaseStandardBridge = ZERO_ADDRESS; } + if (!config.ArbitrumGatewayRouter) { + config.ArbitrumGatewayRouter = ZERO_ADDRESS; + } const inputOutputTokens = getInputOutputTokens(network, config); const repayerVersion = config.IsTest ? "TestRepayer" : "Repayer"; @@ -93,6 +96,7 @@ export async function main() { config.StargateTreasurer, config.OptimismStandardBridge, config.BaseStandardBridge, + config.ArbitrumGatewayRouter, ], [ config.Admin, diff --git a/scripts/deployStandaloneRepayer.ts b/scripts/deployStandaloneRepayer.ts index 715fb25..e8c245c 100644 --- a/scripts/deployStandaloneRepayer.ts +++ b/scripts/deployStandaloneRepayer.ts @@ -80,6 +80,9 @@ export async function main() { if (!config.BaseStandardBridge) { config.BaseStandardBridge = ZERO_ADDRESS; } + if (!config.ArbitrumGatewayRouter) { + config.ArbitrumGatewayRouter = ZERO_ADDRESS; + } const inputOutputTokens = getInputOutputTokens(network, networkConfig[network]); const repayerVersion = config.IsTest ? "TestRepayer" : "Repayer"; @@ -100,6 +103,7 @@ export async function main() { config.StargateTreasurer, config.OptimismStandardBridge, config.BaseStandardBridge, + config.ArbitrumGatewayRouter, ], [ deployer, diff --git a/scripts/upgradeRepayer.ts b/scripts/upgradeRepayer.ts index 3343c3e..a5af97b 100644 --- a/scripts/upgradeRepayer.ts +++ b/scripts/upgradeRepayer.ts @@ -48,6 +48,9 @@ export async function main() { if (!config.BaseStandardBridge) { config.BaseStandardBridge = ZERO_ADDRESS; } + if (!config.ArbitrumGatewayRouter) { + config.ArbitrumGatewayRouter = ZERO_ADDRESS; + } const repayerAddress = await getDeployProxyXAddress("Repayer"); const repayerVersion = config.IsTest ? "TestRepayer" : "Repayer"; @@ -68,6 +71,7 @@ export async function main() { config.StargateTreasurer, config.OptimismStandardBridge, config.BaseStandardBridge, + config.ArbitrumGatewayRouter, ], "Repayer", ); From 229201e47858c197ed7e81cd6c95338df03cc6f0 Mon Sep 17 00:00:00 2001 From: viatrix Date: Sat, 13 Dec 2025 18:42:17 +0200 Subject: [PATCH 03/19] Fixes after review --- contracts/utils/ArbitrumGatewayAdapter.sol | 4 ++-- specific-fork-test/ethereum/Repayer.ts | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/contracts/utils/ArbitrumGatewayAdapter.sol b/contracts/utils/ArbitrumGatewayAdapter.sol index a7b62b2..4e651c8 100644 --- a/contracts/utils/ArbitrumGatewayAdapter.sol +++ b/contracts/utils/ArbitrumGatewayAdapter.sol @@ -39,10 +39,10 @@ abstract contract ArbitrumGatewayAdapter is AdapterHelper { _validateOutputToken(_addressToBytes32(outputToken), destinationDomain, outputTokens); // Get output token from the gateway - address gatewayOutputToken = ARBITRUM_GATEWAY_ROUTER.calculateL2TokenAddress(address(token)); + address gatewayOutputToken = router.calculateL2TokenAddress(address(token)); // Check that output tokens match require(gatewayOutputToken == outputToken, InvalidOutputToken()); - address gateway = ARBITRUM_GATEWAY_ROUTER.getGateway(address(token)); + address gateway = router.getGateway(address(token)); token.forceApprove(gateway, amount); bytes memory gatewayData = router.outboundTransfer{value: msg.value}( address(token), diff --git a/specific-fork-test/ethereum/Repayer.ts b/specific-fork-test/ethereum/Repayer.ts index 236bfa3..2b49fb5 100644 --- a/specific-fork-test/ethereum/Repayer.ts +++ b/specific-fork-test/ethereum/Repayer.ts @@ -29,8 +29,10 @@ describe("Repayer", function () { const DEPOSIT_PROFIT_ROLE = toBytes32("DEPOSIT_PROFIT_ROLE"); const usdc = await hre.ethers.getContractAt("ERC20", forkNetworkConfig.Tokens.USDC); - const dai = await hre.ethers.getContractAt("ERC20", forkNetworkConfig.Tokens.DAI!); - const wbtc = await hre.ethers.getContractAt("ERC20", forkNetworkConfig.Tokens.WBTC!); + assertAddress(forkNetworkConfig.Tokens.DAI, "DAI address is missing"); + const dai = await hre.ethers.getContractAt("ERC20", forkNetworkConfig.Tokens.DAI); + assertAddress(forkNetworkConfig.Tokens.WBTC, "WBTC address is missing"); + const wbtc = await hre.ethers.getContractAt("ERC20", forkNetworkConfig.Tokens.WBTC); const liquidityPool = (await deploy( "TestLiquidityPool", deployer, From 34e98773b3efafbb89af56c09817c15fd1ecc55b Mon Sep 17 00:00:00 2001 From: viatrix Date: Tue, 16 Dec 2025 10:49:44 +0200 Subject: [PATCH 04/19] Remove duplicated test --- specific-fork-test/ethereum/Repayer.ts | 47 -------------------------- 1 file changed, 47 deletions(-) diff --git a/specific-fork-test/ethereum/Repayer.ts b/specific-fork-test/ethereum/Repayer.ts index 2b49fb5..385edaa 100644 --- a/specific-fork-test/ethereum/Repayer.ts +++ b/specific-fork-test/ethereum/Repayer.ts @@ -428,53 +428,6 @@ describe("Repayer", function () { expect(await wbtc.balanceOf(repayer)).to.equal(0n); }); - it("Should allow repayer to initiate Arbitrum Gateway DAI repay on fork", async function () { - const { - repayer, repayUser, liquidityPool, arbitrumGatewayRouter, dai, DAI_DEC - } = await loadFixture(deployAll); - - assertAddress(process.env.DAI_OWNER_ETH_ADDRESS, "Env variables not configured (DAI_OWNER_ETH_ADDRESS missing)"); - const DAI_OWNER_ETH_ADDRESS = process.env.DAI_OWNER_ETH_ADDRESS; - const daiOwner = await hre.ethers.getImpersonatedSigner(DAI_OWNER_ETH_ADDRESS); - await setBalance(DAI_OWNER_ETH_ADDRESS, 10n ** 18n); - - const amount = 4n * DAI_DEC; - const maxGas = 10000000n; - const gasPriceBid = 60000000n; - const maxSubmissionCost = 100000000000000n; - const fee = 1000000000000000n; - await dai.connect(daiOwner).transfer(repayer, amount); - - const outputToken = networkConfig.ARBITRUM_ONE.Tokens.DAI; - - const data = AbiCoder.defaultAbiCoder().encode( - ["uint256", "bytes"], - [maxSubmissionCost, "0x"], - ); - const extraData = AbiCoder.defaultAbiCoder().encode( - ["address", "uint256", "uint256", "bytes"], - [outputToken, maxGas, gasPriceBid, data] - ); - - const gatewayAddress = await arbitrumGatewayRouter.getGateway(dai.target); - const tx = repayer.connect(repayUser).initiateRepay( - dai, - amount, - liquidityPool, - Domain.ARBITRUM_ONE, - Provider.ARBITRUM_GATEWAY, - extraData, - {value: fee} - ); - await expect(tx) - .to.emit(repayer, "InitiateRepay") - .withArgs(dai.target, amount, liquidityPool.target, Domain.ARBITRUM_ONE, Provider.ARBITRUM_GATEWAY); - await expect(tx) - .to.emit(arbitrumGatewayRouter, "TransferRouted") - .withArgs(dai.target, repayer.target, liquidityPool.target, gatewayAddress); - expect(await dai.balanceOf(repayer)).to.equal(0n); - }); - it("Should allow repayer to initiate Arbitrum Gateway WETH repay on fork", async function () { const { repayer, repayUser, liquidityPool, weth, arbitrumGatewayRouter, From 688b200426bb96c4096a6fe75553c286311026d0 Mon Sep 17 00:00:00 2001 From: LiviuD Date: Tue, 16 Dec 2025 16:11:07 +0200 Subject: [PATCH 05/19] updated deterministic hardhat tests --- .env.example | 16 ++++++++++++++++ COVERAGE.md | 42 ++++++++++++++++++++++++++++++++++++++---- coverage-baseline.json | 2 +- hardhat.config.ts | 9 ++++++++- scripts/get-blocks.mjs | 35 +++++++++++++++++++++++++++++++++++ 5 files changed, 98 insertions(+), 6 deletions(-) create mode 100644 scripts/get-blocks.mjs diff --git a/.env.example b/.env.example index a45c154..86eeeff 100644 --- a/.env.example +++ b/.env.example @@ -40,6 +40,22 @@ 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=39550474 +FORK_BLOCK_NUMBER_ETHEREUM=24024515 +FORK_BLOCK_NUMBER_ARBITRUM_ONE=411254516 +FORK_BLOCK_NUMBER_OP_MAINNET=127000000 +FORK_BLOCK_NUMBER_POLYGON_MAINNET=80390425 +FORK_BLOCK_NUMBER_AVALANCHE=73848966 +FORK_BLOCK_NUMBER_BSC=71852875 +FORK_BLOCK_NUMBER_LINEA=26756433 +FORK_BLOCK_NUMBER_UNICHAIN=1000000 + USDC_OWNER_ADDRESS=0x498581fF718922c3f8e6A244956aF099B2652b2b GHO_OWNER_ADDRESS=0x498581fF718922c3f8e6A244956aF099B2652b2b EURC_OWNER_ADDRESS=0x498581fF718922c3f8e6A244956aF099B2652b2b diff --git a/COVERAGE.md b/COVERAGE.md index f6634eb..4f72f74 100644 --- a/COVERAGE.md +++ b/COVERAGE.md @@ -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) @@ -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 diff --git a/coverage-baseline.json b/coverage-baseline.json index 74a045e..6ac40b0 100644 --- a/coverage-baseline.json +++ b/coverage-baseline.json @@ -1,6 +1,6 @@ { "lines": "96.93", "functions": "98.58", - "branches": "87.79", + "branches": "87.63", "statements": "96.93" } \ No newline at end of file diff --git a/hardhat.config.ts b/hardhat.config.ts index 9c3b3ed..f47c5ff 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -699,7 +699,14 @@ const config: HardhatUserConfig = { url: isSet(process.env.DRY_RUN) || isSet(process.env.FORK_TEST) ? process.env[`${process.env.DRY_RUN || process.env.FORK_TEST}_RPC`]! : (process.env.FORK_PROVIDER || process.env.BASE_RPC || "https://base-mainnet.public.blastapi.io"), - blockNumber: process.env.FORK_BLOCK_NUMBER ? parseInt(process.env.FORK_BLOCK_NUMBER) : undefined, + blockNumber: (() => { + // Determine which chain is being forked + const chain = (process.env.DRY_RUN || process.env.FORK_TEST || 'BASE').toUpperCase(); + // Look up the per-chain fork block number + const blockVar = `FORK_BLOCK_NUMBER_${chain}`; + const blockNumber = process.env[blockVar]; + return blockNumber ? parseInt(blockNumber) : undefined; + })(), }, accounts: isSet(process.env.DRY_RUN) ? [{privateKey: process.env.PRIVATE_KEY!, balance: "1000000000000000000"}] diff --git a/scripts/get-blocks.mjs b/scripts/get-blocks.mjs new file mode 100644 index 0000000..c509fcb --- /dev/null +++ b/scripts/get-blocks.mjs @@ -0,0 +1,35 @@ +import { ethers } from 'ethers'; + +const chains = { + 'BASE': process.env.BASE_RPC || 'https://base-mainnet.public.blastapi.io', + 'ETHEREUM': process.env.ETHEREUM_RPC || 'https://eth-mainnet.public.blastapi.io', + 'ARBITRUM_ONE': process.env.ARBITRUM_ONE_RPC || 'https://arbitrum-one.public.blastapi.io', + 'OP_MAINNET': process.env.OP_MAINNET_RPC || 'https://public-op-mainnet.fastnode.io', + 'POLYGON_MAINNET': process.env.POLYGON_MAINNET_RPC || 'https://polygon-bor-rpc.publicnode.com', + 'AVALANCHE': process.env.AVALANCHE_RPC || 'https://avalanche-c-chain-rpc.publicnode.com', + 'BSC': process.env.BSC_RPC || 'https://bsc-mainnet.public.blastapi.io', + 'LINEA': process.env.LINEA_RPC || 'https://linea-rpc.publicnode.com', +}; + +async function getBlockNumber(name, url) { + try { + const provider = new ethers.JsonRpcProvider(url); + const blockNumber = await provider.getBlockNumber(); + // Subtract 1000 blocks for safety margin + const safeBlock = blockNumber - 1000; + console.log(`FORK_BLOCK_NUMBER_${name}=${safeBlock}`); + return safeBlock; + } catch (error) { + console.error(`# Error fetching ${name}: ${error.message}`); + return null; + } +} + +async function main() { + console.log('# Fetching current block numbers...'); + for (const [name, url] of Object.entries(chains)) { + await getBlockNumber(name, url); + } +} + +main().catch(console.error); From ca22cbd01de19a50808838dfe5a9de38f4da6c3b Mon Sep 17 00:00:00 2001 From: LiviuD Date: Tue, 16 Dec 2025 16:20:55 +0200 Subject: [PATCH 06/19] fixed lint issues --- hardhat.config.ts | 2 +- scripts/get-blocks.mjs | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/hardhat.config.ts b/hardhat.config.ts index f47c5ff..d712355 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -701,7 +701,7 @@ const config: HardhatUserConfig = { : (process.env.FORK_PROVIDER || process.env.BASE_RPC || "https://base-mainnet.public.blastapi.io"), blockNumber: (() => { // Determine which chain is being forked - const chain = (process.env.DRY_RUN || process.env.FORK_TEST || 'BASE').toUpperCase(); + const chain = (process.env.DRY_RUN || process.env.FORK_TEST || "BASE").toUpperCase(); // Look up the per-chain fork block number const blockVar = `FORK_BLOCK_NUMBER_${chain}`; const blockNumber = process.env[blockVar]; diff --git a/scripts/get-blocks.mjs b/scripts/get-blocks.mjs index c509fcb..84dda9b 100644 --- a/scripts/get-blocks.mjs +++ b/scripts/get-blocks.mjs @@ -1,14 +1,14 @@ -import { ethers } from 'ethers'; +import {ethers} from "ethers"; const chains = { - 'BASE': process.env.BASE_RPC || 'https://base-mainnet.public.blastapi.io', - 'ETHEREUM': process.env.ETHEREUM_RPC || 'https://eth-mainnet.public.blastapi.io', - 'ARBITRUM_ONE': process.env.ARBITRUM_ONE_RPC || 'https://arbitrum-one.public.blastapi.io', - 'OP_MAINNET': process.env.OP_MAINNET_RPC || 'https://public-op-mainnet.fastnode.io', - 'POLYGON_MAINNET': process.env.POLYGON_MAINNET_RPC || 'https://polygon-bor-rpc.publicnode.com', - 'AVALANCHE': process.env.AVALANCHE_RPC || 'https://avalanche-c-chain-rpc.publicnode.com', - 'BSC': process.env.BSC_RPC || 'https://bsc-mainnet.public.blastapi.io', - 'LINEA': process.env.LINEA_RPC || 'https://linea-rpc.publicnode.com', + "BASE": process.env.BASE_RPC || "https://base-mainnet.public.blastapi.io", + "ETHEREUM": process.env.ETHEREUM_RPC || "https://eth-mainnet.public.blastapi.io", + "ARBITRUM_ONE": process.env.ARBITRUM_ONE_RPC || "https://arbitrum-one.public.blastapi.io", + "OP_MAINNET": process.env.OP_MAINNET_RPC || "https://public-op-mainnet.fastnode.io", + "POLYGON_MAINNET": process.env.POLYGON_MAINNET_RPC || "https://polygon-bor-rpc.publicnode.com", + "AVALANCHE": process.env.AVALANCHE_RPC || "https://avalanche-c-chain-rpc.publicnode.com", + "BSC": process.env.BSC_RPC || "https://bsc-mainnet.public.blastapi.io", + "LINEA": process.env.LINEA_RPC || "https://linea-rpc.publicnode.com", }; async function getBlockNumber(name, url) { @@ -26,7 +26,7 @@ async function getBlockNumber(name, url) { } async function main() { - console.log('# Fetching current block numbers...'); + console.log("# Fetching current block numbers..."); for (const [name, url] of Object.entries(chains)) { await getBlockNumber(name, url); } From e7ffbd8b3b13cdd1637008fd229e3449c869ba22 Mon Sep 17 00:00:00 2001 From: LiviuD Date: Wed, 17 Dec 2025 14:05:18 +0200 Subject: [PATCH 07/19] push change --- .env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 86eeeff..67fc336 100644 --- a/.env.example +++ b/.env.example @@ -50,7 +50,7 @@ FORK_BLOCK_NUMBER_BASE=39550474 FORK_BLOCK_NUMBER_ETHEREUM=24024515 FORK_BLOCK_NUMBER_ARBITRUM_ONE=411254516 FORK_BLOCK_NUMBER_OP_MAINNET=127000000 -FORK_BLOCK_NUMBER_POLYGON_MAINNET=80390425 +FORK_BLOCK_NUMBER_POLYGON_MAINNET=80390425 FORK_BLOCK_NUMBER_AVALANCHE=73848966 FORK_BLOCK_NUMBER_BSC=71852875 FORK_BLOCK_NUMBER_LINEA=26756433 From 110547bbe94d9346b66ecdc6c34bad5054ab8f29 Mon Sep 17 00:00:00 2001 From: LiviuD Date: Wed, 17 Dec 2025 15:32:12 +0200 Subject: [PATCH 08/19] updated debug behavior --- .env.example | 16 ++++++++-------- scripts/get-blocks.mjs | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.env.example b/.env.example index 67fc336..cb6a490 100644 --- a/.env.example +++ b/.env.example @@ -46,14 +46,14 @@ FORK_PROVIDER=https://base-mainnet.public.blastapi.io # 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=39550474 -FORK_BLOCK_NUMBER_ETHEREUM=24024515 -FORK_BLOCK_NUMBER_ARBITRUM_ONE=411254516 -FORK_BLOCK_NUMBER_OP_MAINNET=127000000 -FORK_BLOCK_NUMBER_POLYGON_MAINNET=80390425 -FORK_BLOCK_NUMBER_AVALANCHE=73848966 -FORK_BLOCK_NUMBER_BSC=71852875 -FORK_BLOCK_NUMBER_LINEA=26756433 +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 diff --git a/scripts/get-blocks.mjs b/scripts/get-blocks.mjs index 84dda9b..02628b5 100644 --- a/scripts/get-blocks.mjs +++ b/scripts/get-blocks.mjs @@ -15,8 +15,8 @@ async function getBlockNumber(name, url) { try { const provider = new ethers.JsonRpcProvider(url); const blockNumber = await provider.getBlockNumber(); - // Subtract 1000 blocks for safety margin - const safeBlock = blockNumber - 1000; + // Subtract 100 blocks for minimal safety margin (contracts are recent) + const safeBlock = blockNumber - 100; console.log(`FORK_BLOCK_NUMBER_${name}=${safeBlock}`); return safeBlock; } catch (error) { From 52dbfc290cd03f2e988bba8e7d27fb59d1cde1c7 Mon Sep 17 00:00:00 2001 From: Oleksii Matiiasevych Date: Wed, 17 Dec 2025 22:50:49 +0700 Subject: [PATCH 09/19] Update Everclear test --- test/Repayer.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/test/Repayer.ts b/test/Repayer.ts index c1cd50c..d0bbf35 100644 --- a/test/Repayer.ts +++ b/test/Repayer.ts @@ -1021,14 +1021,20 @@ describe("Repayer", function () { maxFee: "200", }) })).json()).data; + const newIntentSelector = "0xae9b2bad"; // API returns selector for a variety of newIntent that takes 'address' as resipient. // We are using version that expects a 'bytes32' instead. Encoding other data remains the same. const apiTx = everclearFeeAdapter.interface.decodeFunctionData("newIntent", newIntentSelector + apiData.substr(10)); + const extraData = AbiCoder.defaultAbiCoder().encode( ["bytes32", "uint256", "uint48", "tuple(uint256, uint256, bytes)"], [apiTx[3], apiTx[5], apiTx[6], apiTx[8]] ); + const apiAmountIn = apiTx[4]; + const apiFee = apiTx[8][0]; + const apiAmountWithFee = apiAmountIn + apiFee; + expect(apiAmountWithFee).to.be.lessThanOrEqual(amount); await repayer.connect(setTokensUser).setInputOutputTokens( [{ inputToken: weth, @@ -1040,7 +1046,7 @@ describe("Repayer", function () { ); const tx = repayer.connect(repayUser).initiateRepay( weth, - amount, + apiAmountWithFee, liquidityPool, Domain.ETHEREUM, Provider.EVERCLEAR, @@ -1049,13 +1055,13 @@ describe("Repayer", function () { await expect(tx) .to.emit(repayer, "InitiateRepay") - .withArgs(weth.target, amount, liquidityPool.target, Domain.ETHEREUM, Provider.EVERCLEAR); + .withArgs(weth.target, apiAmountWithFee, liquidityPool.target, Domain.ETHEREUM, Provider.EVERCLEAR); await expect(tx) .to.emit(weth, "Transfer") - .withArgs(repayer.target, everclearFeeAdapter.target, amount); + .withArgs(repayer.target, everclearFeeAdapter.target, apiAmountWithFee); await expect(tx) .to.emit(everclearFeeAdapter, "IntentWithFeesAdded"); - expect(await weth.balanceOf(repayer)).to.equal(6n * ETH); + expect(await weth.balanceOf(repayer)).to.equal(10n * ETH - apiAmountWithFee); expect(await getBalance(repayer)).to.equal(0n); }); From bdff146e1be6dd444ad3f8720aa8a2a2a5e21161 Mon Sep 17 00:00:00 2001 From: LiviuD Date: Wed, 17 Dec 2025 18:15:23 +0200 Subject: [PATCH 10/19] updated baseline and added ubuntu 22 to github coverage action --- .github/workflows/coverage.yml | 2 +- coverage-baseline.json | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index d9a6461..479843d 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -6,7 +6,7 @@ on: jobs: coverage: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Checkout code diff --git a/coverage-baseline.json b/coverage-baseline.json index 6ac40b0..fd36c4f 100644 --- a/coverage-baseline.json +++ b/coverage-baseline.json @@ -1,6 +1,6 @@ { - "lines": "96.93", + "lines": "97.04", "functions": "98.58", - "branches": "87.63", - "statements": "96.93" + "branches": "88.46", + "statements": "97.04" } \ No newline at end of file From 4379e3f1e470b86919efc6cb55cbc0b3134cdcb8 Mon Sep 17 00:00:00 2001 From: LiviuD Date: Wed, 17 Dec 2025 18:34:58 +0200 Subject: [PATCH 11/19] normalized files --- .gitattributes | 29 +++++++++++++++++++++++++++++ .github/workflows/coverage.yml | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..715b01b --- /dev/null +++ b/.gitattributes @@ -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 + diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 479843d..538b6bd 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -22,7 +22,16 @@ jobs: run: npm ci - name: Setup environment variables - run: cp .env.example .env + run: | + if [ ! -f .env.example ]; then + echo "❌ ERROR: .env.example file is missing!" + echo "This file must exist with FORK_BLOCK_NUMBER_* variables for deterministic coverage." + exit 1 + fi + cp .env.example .env + echo "✅ Copied .env.example to .env" + echo "📋 Fork block numbers configured:" + grep "FORK_BLOCK_NUMBER" .env | head -10 || echo " ⚠️ No FORK_BLOCK_NUMBER variables found in .env.example" - name: Get baseline from main branch run: | @@ -75,6 +84,27 @@ jobs: echo "🔍 COVERAGE VALIDATION" echo "==================================================" + # Diagnostic: Show environment setup + echo "" + echo "📋 Environment Diagnostics:" + echo " Node version: $(node --version)" + echo " NPM version: $(npm --version)" + if [ -f .env ]; then + echo " .env file: EXISTS" + echo " Fork block numbers in .env:" + grep "FORK_BLOCK_NUMBER" .env | head -5 || echo " (none found)" + else + echo " .env file: MISSING" + fi + if [ -f .env.example ]; then + echo " .env.example file: EXISTS" + echo " Fork block numbers in .env.example:" + grep "FORK_BLOCK_NUMBER" .env.example | head -5 || echo " (none found)" + else + echo " .env.example file: MISSING ⚠️" + fi + echo "" + # Parse CI-generated coverage using dedicated script CI_LINES=$(npx ts-node --files scripts/get-coverage-percentage.ts) @@ -98,6 +128,7 @@ jobs: echo "" echo " Expected: $PR_LINES% (from your committed coverage-baseline.json)" echo " Actual: $CI_LINES% (from fresh CI coverage run)" + echo " Difference: $(awk "BEGIN {printf \"%.2f\", $PR_LINES - $CI_LINES}")%" echo "" echo "💡 This means either:" echo " 1. You forgot to run 'npm run coverage:update-baseline' locally" From 895684db438e6e029f420593953f5f74408f3029 Mon Sep 17 00:00:00 2001 From: LiviuD Date: Wed, 17 Dec 2025 18:42:52 +0200 Subject: [PATCH 12/19] updated .env.example to use latest block and nor forked block --- .env.example | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.env.example b/.env.example index cb6a490..5adbd6f 100644 --- a/.env.example +++ b/.env.example @@ -46,15 +46,15 @@ FORK_PROVIDER=https://base-mainnet.public.blastapi.io # 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 +#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 From 4e459899a0727af3f8d0af37a1741c4a97543c0d Mon Sep 17 00:00:00 2001 From: LiviuD Date: Wed, 17 Dec 2025 19:31:15 +0200 Subject: [PATCH 13/19] print github action env versions --- .github/workflows/coverage.yml | 23 +++++++++++++++++++++++ scripts/check-coverage.ts | 4 +++- scripts/get-coverage-percentage.ts | 3 ++- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 538b6bd..5b538f2 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -21,6 +21,29 @@ jobs: - name: Install dependencies run: npm ci + - name: Print environment versions + run: | + echo "==================================================" + echo "🔍 ENVIRONMENT VERSIONS (for debugging coverage differences)" + echo "==================================================" + echo "" + echo "Node.js version:" + node -v + echo "" + echo "NPM version:" + npm -v + echo "" + echo "Hardhat version:" + npx hardhat --version + echo "" + echo "Solidity compiler version:" + npx solcjs --version 2>/dev/null || echo " (will be downloaded by Hardhat if needed)" + echo "" + echo "Key package versions:" + npm ls hardhat solidity-coverage @nomicfoundation/hardhat-toolbox --depth=0 + echo "" + echo "==================================================" + - name: Setup environment variables run: | if [ ! -f .env.example ]; then diff --git a/scripts/check-coverage.ts b/scripts/check-coverage.ts index 3c2bf27..94a2834 100644 --- a/scripts/check-coverage.ts +++ b/scripts/check-coverage.ts @@ -14,7 +14,9 @@ interface CoverageData { * Parses coverage from lcov.info file */ function parseLcovCoverage(lcovPath: string): CoverageData { - const content = fs.readFileSync(lcovPath, "utf8"); + // Normalize line endings to handle both CRLF (Windows) and LF (Unix) + // This ensures consistent parsing regardless of how lcov.info was generated + const content = fs.readFileSync(lcovPath, "utf8").replace(/\r\n/g, "\n"); let linesFound = 0; let linesHit = 0; diff --git a/scripts/get-coverage-percentage.ts b/scripts/get-coverage-percentage.ts index fb33150..e9ed7a5 100644 --- a/scripts/get-coverage-percentage.ts +++ b/scripts/get-coverage-percentage.ts @@ -17,7 +17,8 @@ if (!fs.existsSync(lcovPath)) { } // Read and parse lcov file -const content = fs.readFileSync(lcovPath, "utf8"); +// Normalize line endings to handle both CRLF (Windows) and LF (Unix) +const content = fs.readFileSync(lcovPath, "utf8").replace(/\r\n/g, "\n"); const lines = content.split("\n"); let linesFound = 0; From b8e4e88dc3deeec4c16b850253a4e4c283fc3c02 Mon Sep 17 00:00:00 2001 From: Liviu Damian Date: Wed, 17 Dec 2025 20:40:58 +0200 Subject: [PATCH 14/19] generated coverage-baseline on ubuntu and aded 0.2 percentage tollerance --- coverage-baseline.json | 6 ++-- scripts/check-coverage.ts | 66 +++++++++++++++++++++++---------------- 2 files changed, 42 insertions(+), 30 deletions(-) diff --git a/coverage-baseline.json b/coverage-baseline.json index fd36c4f..6ac40b0 100644 --- a/coverage-baseline.json +++ b/coverage-baseline.json @@ -1,6 +1,6 @@ { - "lines": "97.04", + "lines": "96.93", "functions": "98.58", - "branches": "88.46", - "statements": "97.04" + "branches": "87.63", + "statements": "96.93" } \ No newline at end of file diff --git a/scripts/check-coverage.ts b/scripts/check-coverage.ts index 94a2834..b626e66 100644 --- a/scripts/check-coverage.ts +++ b/scripts/check-coverage.ts @@ -10,12 +10,13 @@ interface CoverageData { statements: string; } +// Allowed coverage drift (percent) +const COVERAGE_TOLERANCE = 0.2; + /** * Parses coverage from lcov.info file */ function parseLcovCoverage(lcovPath: string): CoverageData { - // Normalize line endings to handle both CRLF (Windows) and LF (Unix) - // This ensures consistent parsing regardless of how lcov.info was generated const content = fs.readFileSync(lcovPath, "utf8").replace(/\r\n/g, "\n"); let linesFound = 0; @@ -43,23 +44,21 @@ function parseLcovCoverage(lcovPath: string): CoverageData { } return { - lines: linesFound > 0 ? (linesHit / linesFound * 100).toFixed(2) : "0", - functions: functionsFound > 0 ? (functionsHit / functionsFound * 100).toFixed(2) : "0", - branches: branchesFound > 0 ? (branchesHit / branchesFound * 100).toFixed(2) : "0", - statements: linesFound > 0 ? (linesHit / linesFound * 100).toFixed(2) : "0" + lines: linesFound > 0 ? ((linesHit / linesFound) * 100).toFixed(2) : "0", + functions: functionsFound > 0 ? ((functionsHit / functionsFound) * 100).toFixed(2) : "0", + branches: branchesFound > 0 ? ((branchesHit / branchesFound) * 100).toFixed(2) : "0", + statements: linesFound > 0 ? ((linesHit / linesFound) * 100).toFixed(2) : "0", }; } // Main const lcovPath = path.join(__dirname, "..", "coverage", "lcov.info"); -// Check if custom baseline path provided (for CI to compare against main) const baselineArg = process.argv.find(arg => arg.startsWith("--baseline=")); const baselinePath = baselineArg ? baselineArg.split("=")[1] : path.join(__dirname, "..", "coverage-baseline.json"); -// Check if we're updating baseline const isUpdatingBaseline = process.argv.includes("--update-baseline"); if (!fs.existsSync(lcovPath)) { @@ -69,7 +68,7 @@ if (!fs.existsSync(lcovPath)) { const current = parseLcovCoverage(lcovPath); -// If updating baseline, save and exit +// Update baseline mode if (isUpdatingBaseline) { fs.writeFileSync(baselinePath, JSON.stringify(current, null, 2)); console.log("\n✅ Coverage baseline updated:"); @@ -81,7 +80,13 @@ if (isUpdatingBaseline) { } // Load baseline -let baseline: CoverageData = {lines: "0", functions: "0", branches: "0", statements: "0"}; +let baseline: CoverageData = { + lines: "0", + functions: "0", + branches: "0", + statements: "0", +}; + if (fs.existsSync(baselinePath)) { baseline = JSON.parse(fs.readFileSync(baselinePath, "utf8")) as CoverageData; } @@ -95,27 +100,34 @@ console.log(`Branches: ${baseline.branches}% → ${current.branches}%`); console.log(`Statements: ${baseline.statements}% → ${current.statements}%`); console.log("─".repeat(50)); -// Check for drops -const drops: string[] = []; -if (parseFloat(current.lines) < parseFloat(baseline.lines)) { - drops.push(`Lines dropped: ${baseline.lines}% → ${current.lines}%`); -} -if (parseFloat(current.functions) < parseFloat(baseline.functions)) { - drops.push(`Functions dropped: ${baseline.functions}% → ${current.functions}%`); -} -if (parseFloat(current.branches) < parseFloat(baseline.branches)) { - drops.push(`Branches dropped: ${baseline.branches}% → ${current.branches}%`); -} -if (parseFloat(current.statements) < parseFloat(baseline.statements)) { - drops.push(`Statements dropped: ${baseline.statements}% → ${current.statements}%`); +// Tolerant comparison +function checkDrop(metric: keyof CoverageData): string | null { + const base = parseFloat(baseline[metric]); + const curr = parseFloat(current[metric]); + const diff = curr - base; + + if (diff < -COVERAGE_TOLERANCE) { + return `${metric} dropped: ${base}% → ${curr}% (Δ ${diff.toFixed(2)}%)`; + } + + return null; } +const drops = [ + checkDrop("lines"), + checkDrop("functions"), + checkDrop("branches"), + checkDrop("statements"), +].filter(Boolean) as string[]; + if (drops.length > 0) { - console.log("\n❌ Coverage decreased:\n"); - drops.forEach((drop: string) => console.log(` • ${drop}`)); - console.log("\n💡 Please add tests to maintain or improve coverage.\n"); + console.log("\n❌ Coverage decreased beyond tolerance:\n"); + drops.forEach(d => console.log(` • ${d}`)); + console.log(`\n💡 Allowed tolerance: ±${COVERAGE_TOLERANCE}%\n`); process.exit(1); } -console.log("\n✅ Coverage maintained or improved!\n"); +console.log( + `\n✅ Coverage maintained within tolerance (±${COVERAGE_TOLERANCE}%)\n` +); process.exit(0); From 7ad368787e9e71a817708b8a909d6639e039f5c9 Mon Sep 17 00:00:00 2001 From: Liviu Damian Date: Wed, 17 Dec 2025 20:52:06 +0200 Subject: [PATCH 15/19] updated woflwo coverage script usage --- .github/workflows/coverage.yml | 171 ++------------------------------- 1 file changed, 10 insertions(+), 161 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 5b538f2..19d8cee 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -23,176 +23,25 @@ jobs: - name: Print environment versions run: | - echo "==================================================" - echo "🔍 ENVIRONMENT VERSIONS (for debugging coverage differences)" - echo "==================================================" - echo "" - echo "Node.js version:" - node -v - echo "" - echo "NPM version:" - npm -v - echo "" - echo "Hardhat version:" + echo "Node.js: $(node -v)" + echo "NPM: $(npm -v)" npx hardhat --version - echo "" - echo "Solidity compiler version:" - npx solcjs --version 2>/dev/null || echo " (will be downloaded by Hardhat if needed)" - echo "" - echo "Key package versions:" - npm ls hardhat solidity-coverage @nomicfoundation/hardhat-toolbox --depth=0 - echo "" - echo "==================================================" + npx solcjs --version 2>/dev/null || echo "(will be downloaded by Hardhat if needed)" - name: Setup environment variables run: | - if [ ! -f .env.example ]; then - echo "❌ ERROR: .env.example file is missing!" - echo "This file must exist with FORK_BLOCK_NUMBER_* variables for deterministic coverage." - exit 1 - fi cp .env.example .env - echo "✅ Copied .env.example to .env" - echo "📋 Fork block numbers configured:" - grep "FORK_BLOCK_NUMBER" .env | head -10 || echo " ⚠️ No FORK_BLOCK_NUMBER variables found in .env.example" + echo "Fork block numbers configured:" + grep "FORK_BLOCK_NUMBER" .env || echo " (none found)" - - name: Get baseline from main branch + - name: Run coverage and verify 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 - - - 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 "Running coverage..." npm run coverage - echo "✅ Coverage completed successfully" - - - name: Validate coverage - run: | - echo "==================================================" - echo "🔍 COVERAGE VALIDATION" - echo "==================================================" - - # Diagnostic: Show environment setup - echo "" - echo "📋 Environment Diagnostics:" - echo " Node version: $(node --version)" - echo " NPM version: $(npm --version)" - if [ -f .env ]; then - echo " .env file: EXISTS" - echo " Fork block numbers in .env:" - grep "FORK_BLOCK_NUMBER" .env | head -5 || echo " (none found)" - else - echo " .env file: MISSING" - fi - if [ -f .env.example ]; then - echo " .env.example file: EXISTS" - echo " Fork block numbers in .env.example:" - grep "FORK_BLOCK_NUMBER" .env.example | head -5 || echo " (none found)" - else - echo " .env.example file: MISSING ⚠️" - fi - 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 " Difference: $(awk "BEGIN {printf \"%.2f\", $PR_LINES - $CI_LINES}")%" - 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..." + npx ts-node --files scripts/verify-coverage.ts - name: Upload coverage report (optional) if: always() From 81912c24c1d7be92730c630aaee135d736b9af2c Mon Sep 17 00:00:00 2001 From: Liviu Damian Date: Wed, 17 Dec 2025 21:04:04 +0200 Subject: [PATCH 16/19] fixed coverage script --- .github/workflows/coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 19d8cee..6cebc29 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -41,7 +41,7 @@ jobs: npm run coverage echo "Verifying coverage against baseline..." - npx ts-node --files scripts/verify-coverage.ts + npm run coverage:check - name: Upload coverage report (optional) if: always() From 96bf04fc393389f1ca875bb9715c325c19aa86fc Mon Sep 17 00:00:00 2001 From: Oleksii Matiiasevych Date: Thu, 18 Dec 2025 12:40:01 +0700 Subject: [PATCH 17/19] Debug coverage --- .nvmrc | 2 +- contracts/Deps.sol | 1 + package-lock.json | 97 +++++++++++++++++++++++----------------------- package.json | 4 +- 4 files changed, 53 insertions(+), 51 deletions(-) diff --git a/.nvmrc b/.nvmrc index 2c02202..c6a66a6 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v22.13.0 +v22.21.1 diff --git a/contracts/Deps.sol b/contracts/Deps.sol index 7eba42a..748229c 100644 --- a/contracts/Deps.sol +++ b/contracts/Deps.sol @@ -3,3 +3,4 @@ pragma solidity 0.8.28; /* solhint-disable no-unused-import */ import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {console} from "hardhat/console.sol"; diff --git a/package-lock.json b/package-lock.json index 0022cfc..b4b4787 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,10 +20,10 @@ "@nomicfoundation/hardhat-verify": "^2.0.14", "dotenv": "^16.4.7", "eslint": "^9.17.0", - "hardhat": "^2.26.1", + "hardhat": "^2.28.0", "hardhat-ignore-warnings": "^0.2.12", "solhint": "^5.0.4", - "solidity-coverage": "^0.8.16", + "solidity-coverage": "^0.8.17", "typescript": "^5.7.3", "typescript-eslint": "^8.19.1" } @@ -1408,92 +1408,92 @@ } }, "node_modules/@nomicfoundation/edr": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr/-/edr-0.11.3.tgz", - "integrity": "sha512-kqILRkAd455Sd6v8mfP3C1/0tCOynJWY+Ir+k/9Boocu2kObCrsFgG+ZWB7fSBVdd9cPVSNrnhWS+V+PEo637g==", + "version": "0.12.0-next.17", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr/-/edr-0.12.0-next.17.tgz", + "integrity": "sha512-Y8Kwqd5JpBmI/Kst6NJ/bZ81FeJea9J6WEwoSRTZnEvwfqW9dk9PI8zJs2UJpOACL1fXEPvN+doETbxT9EhwXA==", "dev": true, "license": "MIT", "dependencies": { - "@nomicfoundation/edr-darwin-arm64": "0.11.3", - "@nomicfoundation/edr-darwin-x64": "0.11.3", - "@nomicfoundation/edr-linux-arm64-gnu": "0.11.3", - "@nomicfoundation/edr-linux-arm64-musl": "0.11.3", - "@nomicfoundation/edr-linux-x64-gnu": "0.11.3", - "@nomicfoundation/edr-linux-x64-musl": "0.11.3", - "@nomicfoundation/edr-win32-x64-msvc": "0.11.3" + "@nomicfoundation/edr-darwin-arm64": "0.12.0-next.17", + "@nomicfoundation/edr-darwin-x64": "0.12.0-next.17", + "@nomicfoundation/edr-linux-arm64-gnu": "0.12.0-next.17", + "@nomicfoundation/edr-linux-arm64-musl": "0.12.0-next.17", + "@nomicfoundation/edr-linux-x64-gnu": "0.12.0-next.17", + "@nomicfoundation/edr-linux-x64-musl": "0.12.0-next.17", + "@nomicfoundation/edr-win32-x64-msvc": "0.12.0-next.17" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@nomicfoundation/edr-darwin-arm64": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-darwin-arm64/-/edr-darwin-arm64-0.11.3.tgz", - "integrity": "sha512-w0tksbdtSxz9nuzHKsfx4c2mwaD0+l5qKL2R290QdnN9gi9AV62p9DHkOgfBdyg6/a6ZlnQqnISi7C9avk/6VA==", + "version": "0.12.0-next.17", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-darwin-arm64/-/edr-darwin-arm64-0.12.0-next.17.tgz", + "integrity": "sha512-gI9/9ysLeAid0+VSTBeutxOJ0/Rrh00niGkGL9+4lR577igDY+v55XGN0oBMST49ILS0f12J6ZY90LG8sxPXmQ==", "dev": true, "license": "MIT", "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@nomicfoundation/edr-darwin-x64": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-darwin-x64/-/edr-darwin-x64-0.11.3.tgz", - "integrity": "sha512-QR4jAFrPbOcrO7O2z2ESg+eUeIZPe2bPIlQYgiJ04ltbSGW27FblOzdd5+S3RoOD/dsZGKAvvy6dadBEl0NgoA==", + "version": "0.12.0-next.17", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-darwin-x64/-/edr-darwin-x64-0.12.0-next.17.tgz", + "integrity": "sha512-zSZtwf584RkIyb8awELDt7ctskogH0p4pmqOC4vhykc8ODOv2XLuG1IgeE4WgYhWGZOufbCtgLfpJQrWqN6mmw==", "dev": true, "license": "MIT", "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@nomicfoundation/edr-linux-arm64-gnu": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-arm64-gnu/-/edr-linux-arm64-gnu-0.11.3.tgz", - "integrity": "sha512-Ktjv89RZZiUmOFPspuSBVJ61mBZQ2+HuLmV67InNlh9TSUec/iDjGIwAn59dx0bF/LOSrM7qg5od3KKac4LJDQ==", + "version": "0.12.0-next.17", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-arm64-gnu/-/edr-linux-arm64-gnu-0.12.0-next.17.tgz", + "integrity": "sha512-WjdfgV6B7gT5Q0NXtSIWyeK8gzaJX5HK6/jclYVHarWuEtS1LFgePYgMjK8rmm7IRTkM9RsE/PCuQEP1nrSsuA==", "dev": true, "license": "MIT", "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@nomicfoundation/edr-linux-arm64-musl": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-arm64-musl/-/edr-linux-arm64-musl-0.11.3.tgz", - "integrity": "sha512-B3sLJx1rL2E9pfdD4mApiwOZSrX0a/KQSBWdlq1uAhFKqkl00yZaY4LejgZndsJAa4iKGQJlGnw4HCGeVt0+jA==", + "version": "0.12.0-next.17", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-arm64-musl/-/edr-linux-arm64-musl-0.12.0-next.17.tgz", + "integrity": "sha512-26rObKhhCDb9JkZbToyr7JVZo4tSVAFvzoJSJVmvpOl0LOHrfFsgVQu2n/8cNkwMAqulPubKL2E0jdnmEoZjWA==", "dev": true, "license": "MIT", "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@nomicfoundation/edr-linux-x64-gnu": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-x64-gnu/-/edr-linux-x64-gnu-0.11.3.tgz", - "integrity": "sha512-D/4cFKDXH6UYyKPu6J3Y8TzW11UzeQI0+wS9QcJzjlrrfKj0ENW7g9VihD1O2FvXkdkTjcCZYb6ai8MMTCsaVw==", + "version": "0.12.0-next.17", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-x64-gnu/-/edr-linux-x64-gnu-0.12.0-next.17.tgz", + "integrity": "sha512-dPkHScIf/CU6h6k3k4HNUnQyQcVSLKanviHCAcs5HkviiJPxvVtOMMvtNBxoIvKZRxGFxf2eutcqQW4ZV1wRQQ==", "dev": true, "license": "MIT", "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@nomicfoundation/edr-linux-x64-musl": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-x64-musl/-/edr-linux-x64-musl-0.11.3.tgz", - "integrity": "sha512-ergXuIb4nIvmf+TqyiDX5tsE49311DrBky6+jNLgsGDTBaN1GS3OFwFS8I6Ri/GGn6xOaT8sKu3q7/m+WdlFzg==", + "version": "0.12.0-next.17", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-x64-musl/-/edr-linux-x64-musl-0.12.0-next.17.tgz", + "integrity": "sha512-5Ixe/bpyWZxC3AjIb8EomAOK44ajemBVx/lZRHZiWSBlwQpbSWriYAtKjKcReQQPwuYVjnFpAD2AtuCvseIjHw==", "dev": true, "license": "MIT", "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@nomicfoundation/edr-win32-x64-msvc": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-win32-x64-msvc/-/edr-win32-x64-msvc-0.11.3.tgz", - "integrity": "sha512-snvEf+WB3OV0wj2A7kQ+ZQqBquMcrozSLXcdnMdEl7Tmn+KDCbmFKBt3Tk0X3qOU4RKQpLPnTxdM07TJNVtung==", + "version": "0.12.0-next.17", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-win32-x64-msvc/-/edr-win32-x64-msvc-0.12.0-next.17.tgz", + "integrity": "sha512-29YlvdgofSdXG1mUzIuH4kMXu1lmVc1hvYWUGWEH59L+LaakdhfJ/Wu5izeclKkrTh729Amtk/Hk1m29kFOO8A==", "dev": true, "license": "MIT", "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@nomicfoundation/hardhat-chai-matchers": { @@ -5819,15 +5819,15 @@ } }, "node_modules/hardhat": { - "version": "2.26.1", - "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.26.1.tgz", - "integrity": "sha512-CXWuUaTtehxiHPCdlitntctfeYRgujmXkNX5gnrD5jdA6HhRQt+WWBZE/gHXbE29y/wDmmUL2d652rI0ctjqjw==", + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.28.0.tgz", + "integrity": "sha512-A3yBISI18EcnY2IR7Ny2xZF33Q3qH01yrWapeWbyGOiJm/386SasWjbHRHYgUlZ3YWJETIMh7wYfMUaXrofTDQ==", "dev": true, "license": "MIT", "dependencies": { "@ethereumjs/util": "^9.1.0", "@ethersproject/abi": "^5.1.2", - "@nomicfoundation/edr": "^0.11.3", + "@nomicfoundation/edr": "0.12.0-next.17", "@nomicfoundation/solidity-analyzer": "^0.1.0", "@sentry/node": "^5.18.1", "adm-zip": "^0.4.16", @@ -9313,10 +9313,11 @@ } }, "node_modules/solidity-coverage": { - "version": "0.8.16", - "resolved": "https://registry.npmjs.org/solidity-coverage/-/solidity-coverage-0.8.16.tgz", - "integrity": "sha512-qKqgm8TPpcnCK0HCDLJrjbOA2tQNEJY4dHX/LSSQ9iwYFS973MwjtgYn2Iv3vfCEQJTj5xtm4cuUMzlJsJSMbg==", + "version": "0.8.17", + "resolved": "https://registry.npmjs.org/solidity-coverage/-/solidity-coverage-0.8.17.tgz", + "integrity": "sha512-5P8vnB6qVX9tt1MfuONtCTEaEGO/O4WuEidPHIAJjx4sktHHKhO3rFvnE0q8L30nWJPTrcqGQMT7jpE29B2qow==", "dev": true, + "license": "ISC", "dependencies": { "@ethersproject/abi": "^5.0.9", "@solidity-parser/parser": "^0.20.1", diff --git a/package.json b/package.json index 3181ac7..17dd044 100644 --- a/package.json +++ b/package.json @@ -166,10 +166,10 @@ "@nomicfoundation/hardhat-verify": "^2.0.14", "dotenv": "^16.4.7", "eslint": "^9.17.0", - "hardhat": "^2.26.1", + "hardhat": "^2.28.0", "hardhat-ignore-warnings": "^0.2.12", "solhint": "^5.0.4", - "solidity-coverage": "^0.8.16", + "solidity-coverage": "^0.8.17", "typescript": "^5.7.3", "typescript-eslint": "^8.19.1" }, From 9f1eaf3a051bd40d5ef3dd47bf02376d3417412c Mon Sep 17 00:00:00 2001 From: Oleksii Matiiasevych Date: Thu, 18 Dec 2025 12:41:14 +0700 Subject: [PATCH 18/19] Update baseline --- coverage-baseline.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/coverage-baseline.json b/coverage-baseline.json index 6ac40b0..af8d26a 100644 --- a/coverage-baseline.json +++ b/coverage-baseline.json @@ -1,6 +1,6 @@ { - "lines": "96.93", + "lines": "96.82", "functions": "98.58", - "branches": "87.63", - "statements": "96.93" + "branches": "87.79", + "statements": "96.82" } \ No newline at end of file From aedcebe9f2ba16198c68466272238d5bdb34856f Mon Sep 17 00:00:00 2001 From: Oleksii Matiiasevych Date: Thu, 18 Dec 2025 13:39:47 +0700 Subject: [PATCH 19/19] Trying other things --- contracts/Deps.sol | 1 + coverage-baseline.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/contracts/Deps.sol b/contracts/Deps.sol index 748229c..495f551 100644 --- a/contracts/Deps.sol +++ b/contracts/Deps.sol @@ -3,4 +3,5 @@ pragma solidity 0.8.28; /* 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"; diff --git a/coverage-baseline.json b/coverage-baseline.json index af8d26a..74a045e 100644 --- a/coverage-baseline.json +++ b/coverage-baseline.json @@ -1,6 +1,6 @@ { - "lines": "96.82", + "lines": "96.93", "functions": "98.58", "branches": "87.79", - "statements": "96.82" + "statements": "96.93" } \ No newline at end of file diff --git a/package.json b/package.json index 17dd044..a4b74e6 100644 --- a/package.json +++ b/package.json @@ -141,7 +141,7 @@ "test:deploy": "ts-node --files ./scripts/deploy.ts", "test:ethereum": "FORK_TEST=ETHEREUM hardhat test --typecheck ./specific-fork-test/ethereum/*.ts", "test:scripts": "SCRIPT_ENV=CI DEPLOY_ID=CI ts-node --files ./scripts/test.ts", - "coverage": "hardhat coverage", + "coverage": "SOLIDITY_COVERAGE=true hardhat coverage", "coverage:check": "ts-node --files scripts/check-coverage.ts", "coverage:update-baseline": "ts-node --files scripts/check-coverage.ts --update-baseline" },