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/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..4e651c8 --- /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 = router.calculateL2TokenAddress(address(token)); + // Check that output tokens match + require(gatewayOutputToken == outputToken, InvalidOutputToken()); + address 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/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 84076ac..e163639 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", @@ -256,7 +259,13 @@ export const networkConfig: NetworksConfig = { Provider.SUPERCHAIN_STANDARD_BRIDGE, Provider.STARGATE, ], - [Network.ARBITRUM_ONE]: [Provider.CCTP, Provider.ACROSS, Provider.EVERCLEAR, Provider.STARGATE], + [Network.ARBITRUM_ONE]: [ + Provider.CCTP, + Provider.ACROSS, + Provider.EVERCLEAR, + Provider.STARGATE, + Provider.ARBITRUM_GATEWAY + ], [Network.BASE]: [ Provider.CCTP, Provider.ACROSS, @@ -275,7 +284,7 @@ export const networkConfig: NetworksConfig = { Provider.EVERCLEAR, Provider.SUPERCHAIN_STANDARD_BRIDGE ], - [Network.ARBITRUM_ONE]: [Provider.CCTP, Provider.ACROSS, Provider.EVERCLEAR], + [Network.ARBITRUM_ONE]: [Provider.CCTP, Provider.ACROSS, Provider.EVERCLEAR, Provider.ARBITRUM_GATEWAY], [Network.BASE]: [Provider.CCTP, Provider.ACROSS, Provider.EVERCLEAR, Provider.SUPERCHAIN_STANDARD_BRIDGE], }, }, @@ -310,6 +319,7 @@ export const networkConfig: NetworksConfig = { EverclearFeeAdapter: "0xd0185bfb8107c5b2336bC73cE3fdd9Bfb504540e", OptimismStandardBridge: "0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1", BaseStandardBridge: "0x3154Cf16ccdb4C6d922629664174b904d80F2C35", + ArbitrumGatewayRouter: "0x72Ce9c846789fdB6fC1f34aC4AD25Dd9ef7031ef", Tokens: { USDC: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", USDT: "0xdAC17F958D2ee523a2206206994597C13D831ec7", @@ -349,7 +359,7 @@ export const networkConfig: NetworksConfig = { [LiquidityPoolAaveUSDCV4]: { SupportsAllTokens: true, Domains: { - [Network.ARBITRUM_ONE]: [Provider.CCTP, Provider.ACROSS, Provider.EVERCLEAR], + [Network.ARBITRUM_ONE]: [Provider.CCTP, Provider.ACROSS, Provider.EVERCLEAR, Provider.ARBITRUM_GATEWAY], [Network.BASE]: [Provider.CCTP, Provider.ACROSS, Provider.EVERCLEAR, Provider.SUPERCHAIN_STANDARD_BRIDGE], [Network.OP_MAINNET]: [ Provider.CCTP, @@ -362,7 +372,7 @@ export const networkConfig: NetworksConfig = { [LiquidityPoolUSDCV4]: { SupportsAllTokens: false, Domains: { - [Network.ARBITRUM_ONE]: [Provider.CCTP, Provider.ACROSS, Provider.EVERCLEAR], + [Network.ARBITRUM_ONE]: [Provider.CCTP, Provider.ACROSS, Provider.EVERCLEAR, Provider.ARBITRUM_GATEWAY], [Network.BASE]: [ Provider.CCTP, Provider.ACROSS, @@ -380,7 +390,7 @@ export const networkConfig: NetworksConfig = { [LiquidityPoolAaveUSDCLongTermV3]: { SupportsAllTokens: true, Domains: { - [Network.ARBITRUM_ONE]: [Provider.CCTP, Provider.ACROSS, Provider.EVERCLEAR], + [Network.ARBITRUM_ONE]: [Provider.CCTP, Provider.ACROSS, Provider.EVERCLEAR, Provider.ARBITRUM_GATEWAY], }, }, }, @@ -1506,6 +1516,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/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/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", ); diff --git a/specific-fork-test/ethereum/Repayer.ts b/specific-fork-test/ethereum/Repayer.ts index eb21a20..385edaa 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,10 @@ describe("Repayer", function () { const DEPOSIT_PROFIT_ROLE = toBytes32("DEPOSIT_PROFIT_ROLE"); const usdc = await hre.ethers.getContractAt("ERC20", forkNetworkConfig.Tokens.USDC); + 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, @@ -69,10 +73,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 +96,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 +126,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 +166,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 +333,182 @@ 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 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..d0bbf35 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; @@ -1015,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, @@ -1034,7 +1046,7 @@ describe("Repayer", function () { ); const tx = repayer.connect(repayUser).initiateRepay( weth, - amount, + apiAmountWithFee, liquidityPool, Domain.ETHEREUM, Provider.EVERCLEAR, @@ -1043,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); }); @@ -1127,7 +1139,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 +1157,7 @@ describe("Repayer", function () { stargateTreasurerTrue, optimismBridge, baseBridge, + arbitrumGatewayRouter, ) ) as Repayer; const repayerInit = (await repayerImpl.initialize.populateTransaction( @@ -1194,7 +1207,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 +1225,7 @@ describe("Repayer", function () { stargateTreasurerTrue, optimismBridge, baseBridge, + arbitrumGatewayRouter, ) ) as Repayer; const repayerInit = (await repayerImpl.initialize.populateTransaction( @@ -1261,7 +1275,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 +1290,7 @@ describe("Repayer", function () { stargateTreasurerTrue, optimismBridge, baseBridge, + arbitrumGatewayRouter, ) ) as Repayer; const repayerInit = (await repayerImpl.initialize.populateTransaction( @@ -1324,7 +1339,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 +1354,7 @@ describe("Repayer", function () { stargateTreasurerTrue, optimismBridge, baseBridge, + arbitrumGatewayRouter, ) ) as Repayer; const repayerInit = (await repayerImpl.initialize.populateTransaction( @@ -1388,7 +1404,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 +1419,7 @@ describe("Repayer", function () { stargateTreasurerTrue, optimismBridge, baseBridge, + arbitrumGatewayRouter, ) ) as Repayer; const repayerInit = (await repayerImpl.initialize.populateTransaction( @@ -1473,6 +1490,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 +2427,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 +2453,7 @@ describe("Repayer", function () { stargateTreasurerFalse, optimismBridge, baseBridge, + arbitrumGatewayRouter, ) ) as Repayer; @@ -1883,6 +2561,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 +2593,7 @@ describe("Repayer", function () { stargateTreasurer, optimismBridge, baseBridge, + arbitrumGatewayRouter, ) ) as Repayer; @@ -2052,7 +2732,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 +2761,7 @@ describe("Repayer", function () { ZERO_ADDRESS, optimismBridge, baseBridge, + arbitrumGatewayRouter, ) ) as Repayer;