Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,5 @@ EURC_OWNER_ADDRESS=0x498581fF718922c3f8e6A244956aF099B2652b2b
WETH_OWNER_ADDRESS=0x498581fF718922c3f8e6A244956aF099B2652b2b
PRIME_OWNER_ADDRESS=0x75a44A70cCb0E886E25084Be14bD45af57915451
USDC_OWNER_ETH_ADDRESS=0x40ec5B33f54e0E8A33A975908C5BA1c14e5BbbDf
DAI_OWNER_ETH_ADDRESS=0x40ec5B33f54e0E8A33A975908C5BA1c14e5BbbDf
WBTC_OWNER_ETH_ADDRESS=0x40ec5B33f54e0E8A33A975908C5BA1c14e5BbbDf
19 changes: 17 additions & 2 deletions contracts/Repayer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {AcrossAdapter} from "./utils/AcrossAdapter.sol";
import {StargateAdapter} from "./utils/StargateAdapter.sol";
import {EverclearAdapter} from "./utils/EverclearAdapter.sol";
import {SuperchainStandardBridgeAdapter} from "./utils/SuperchainStandardBridgeAdapter.sol";
import {ArbitrumGatewayAdapter} from "./utils/ArbitrumGatewayAdapter.sol";
import {ERC7201Helper} from "./utils/ERC7201Helper.sol";

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

pragma solidity 0.8.28;

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

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

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

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

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

enum PoolType {
Expand Down
42 changes: 42 additions & 0 deletions contracts/testing/TestArbitrum.sol
Original file line number Diff line number Diff line change
@@ -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";
}
}
6 changes: 4 additions & 2 deletions contracts/testing/TestRepayer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ contract TestRepayer is Repayer {
address wrappedNativeToken,
address stargateTreasurer,
address optimismBridge,
address baseBridge
address baseBridge,
address arbitrumGatewayRouter
) Repayer(
localDomain,
assets,
Expand All @@ -25,7 +26,8 @@ contract TestRepayer is Repayer {
wrappedNativeToken,
stargateTreasurer,
optimismBridge,
baseBridge
baseBridge,
arbitrumGatewayRouter
) {}

function domainCCTP(Domain destinationDomain) public pure override returns (uint32) {
Expand Down
57 changes: 57 additions & 0 deletions contracts/utils/ArbitrumGatewayAdapter.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
8 changes: 4 additions & 4 deletions coverage-baseline.json
Original file line number Diff line number Diff line change
@@ -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"
}
5 changes: 5 additions & 0 deletions network.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export enum Provider {
EVERCLEAR = "EVERCLEAR",
STARGATE = "STARGATE",
SUPERCHAIN_STANDARD_BRIDGE = "SUPERCHAIN_STANDARD_BRIDGE",
ARBITRUM_GATEWAY = "ARBITRUM_GATEWAY",
}

export enum Token {
Expand Down Expand Up @@ -164,6 +165,7 @@ export interface NetworkConfig {
EverclearFeeAdapter?: string;
OptimismStandardBridge?: string;
BaseStandardBridge?: string;
ArbitrumGatewayRouter?: string;
Tokens: {
[Token.USDC]: string;
[Token.USDT]?: string;
Expand Down Expand Up @@ -213,6 +215,7 @@ export const networkConfig: NetworksConfig = {
EverclearFeeAdapter: "0xd0185bfb8107c5b2336bC73cE3fdd9Bfb504540e",
OptimismStandardBridge: "0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1",
BaseStandardBridge: "0x3154Cf16ccdb4C6d922629664174b904d80F2C35",
ArbitrumGatewayRouter: "0x72Ce9c846789fdB6fC1f34aC4AD25Dd9ef7031ef",
Tokens: {
USDC: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
USDT: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
Expand Down Expand Up @@ -310,6 +313,7 @@ export const networkConfig: NetworksConfig = {
EverclearFeeAdapter: "0xd0185bfb8107c5b2336bC73cE3fdd9Bfb504540e",
OptimismStandardBridge: "0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1",
BaseStandardBridge: "0x3154Cf16ccdb4C6d922629664174b904d80F2C35",
ArbitrumGatewayRouter: "0x72Ce9c846789fdB6fC1f34aC4AD25Dd9ef7031ef",
Tokens: {
USDC: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
USDT: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
Expand Down Expand Up @@ -1506,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;
Expand Down
2 changes: 2 additions & 0 deletions scripts/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export const ProviderSolidity = {
STARGATE: 3n,
EVERCLEAR: 4n,
SUPERCHAIN_STANDARD_BRIDGE: 5n,
ARBITRUM_GATEWAY: 6n,
};

export const DomainSolidity = {
Expand Down Expand Up @@ -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 } = {
Expand Down
4 changes: 4 additions & 0 deletions scripts/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -412,6 +415,7 @@ export async function main() {
config.StargateTreasurer,
config.OptimismStandardBridge,
config.BaseStandardBridge,
config.ArbitrumGatewayRouter,
],
[
config.Admin,
Expand Down
4 changes: 4 additions & 0 deletions scripts/deployRepayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -93,6 +96,7 @@ export async function main() {
config.StargateTreasurer,
config.OptimismStandardBridge,
config.BaseStandardBridge,
config.ArbitrumGatewayRouter,
],
[
config.Admin,
Expand Down
4 changes: 4 additions & 0 deletions scripts/deployStandaloneRepayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -100,6 +103,7 @@ export async function main() {
config.StargateTreasurer,
config.OptimismStandardBridge,
config.BaseStandardBridge,
config.ArbitrumGatewayRouter,
],
[
deployer,
Expand Down
4 changes: 4 additions & 0 deletions scripts/upgradeRepayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -68,6 +71,7 @@ export async function main() {
config.StargateTreasurer,
config.OptimismStandardBridge,
config.BaseStandardBridge,
config.ArbitrumGatewayRouter,
],
"Repayer",
);
Expand Down
Loading
Loading