Skip to content

Add Optimism Standard Bridge repayment #75

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ PRIVATE_KEY=

VERIFY=false

BASE_SEPOLIA_RPC=
BASE_SEPOLIA_RPC=https://sepolia.base.org
ETHEREUM_SEPOLIA_RPC=
ARBITRUM_SEPOLIA_RPC=
OP_SEPOLIA_RPC=
BASE_RPC=
ETHEREUM_RPC=
ARBITRUM_ONE_RPC=
OP_MAINNET_RPC=
ARBITRUM_SEPOLIA_RPC=https://sepolia-rollup.arbitrum.io/rpc
OP_SEPOLIA_RPC=https://sepolia.optimism.io
BASE_RPC=https://base-mainnet.public.blastapi.io
ETHEREUM_RPC=https://eth-mainnet.public.blastapi.io
ARBITRUM_ONE_RPC=https://arbitrum-one.public.blastapi.io
OP_MAINNET_RPC=https://optimism-mainnet.public.blastapi.io

ETHERSCAN_BASE_SEPOLIA=
ETHERSCAN_ETHEREUM_SEPOLIA=
Expand All @@ -40,3 +40,4 @@ USDC_OWNER_ADDRESS=0x498581fF718922c3f8e6A244956aF099B2652b2b
GHO_OWNER_ADDRESS=0x498581fF718922c3f8e6A244956aF099B2652b2b
EURC_OWNER_ADDRESS=0x498581fF718922c3f8e6A244956aF099B2652b2b
PRIME_OWNER_ADDRESS=0x75a44A70cCb0E886E25084Be14bD45af57915451
USDC_OWNER_ETH_ADDRESS=0x40ec5B33f54e0E8A33A975908C5BA1c14e5BbbDf
2 changes: 2 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,7 @@ jobs:
run: npm run compile
- name: Hardhat Tests
run: npm run test
- name: Hardhat Fork Tests
run: npm run test:ethereum
- name: Script Tests
run: npm run test:scripts
23 changes: 20 additions & 3 deletions contracts/Repayer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,23 @@ import {CCTPAdapter} from "./utils/CCTPAdapter.sol";
import {AcrossAdapter} from "./utils/AcrossAdapter.sol";
import {StargateAdapter} from "./utils/StargateAdapter.sol";
import {EverclearAdapter} from "./utils/EverclearAdapter.sol";
import {OptimismStandardBridgeAdapter} from "./utils/OptimismStandardBridgeAdapter.sol";
import {ERC7201Helper} from "./utils/ERC7201Helper.sol";

/// @title Performs repayment to Liquidity Pools on same/different chains.
/// Routes, which is a destination pool/domain and a bridging provider, have to be approved by admin.
/// REPAYER_ROLE is needed to finalize/init rebalancing process.
/// @notice Upgradeable.
/// @author Tanya Bushenyova <[email protected]>
contract Repayer is IRepayer, AccessControlUpgradeable, CCTPAdapter, AcrossAdapter, StargateAdapter, EverclearAdapter {
contract Repayer is
IRepayer,
AccessControlUpgradeable,
CCTPAdapter,
AcrossAdapter,
StargateAdapter,
EverclearAdapter,
OptimismStandardBridgeAdapter
{
using SafeERC20 for IERC20;
using BitMaps for BitMaps.BitMap;
using EnumerableSet for EnumerableSet.AddressSet;
Expand Down Expand Up @@ -71,12 +80,14 @@ contract Repayer is IRepayer, AccessControlUpgradeable, CCTPAdapter, AcrossAdapt
address acrossSpokePool,
address everclearFeeAdapter,
address wrappedNativeToken,
address stargateTreasurer
address stargateTreasurer,
address optimismBridge
)
CCTPAdapter(cctpTokenMessenger, cctpMessageTransmitter)
AcrossAdapter(acrossSpokePool)
StargateAdapter(stargateTreasurer)
EverclearAdapter(everclearFeeAdapter)
OptimismStandardBridgeAdapter(optimismBridge, wrappedNativeToken)
{
ERC7201Helper.validateStorageLocation(
STORAGE_LOCATION,
Expand Down Expand Up @@ -131,6 +142,7 @@ contract Repayer is IRepayer, AccessControlUpgradeable, CCTPAdapter, AcrossAdapt
uint256 thisBalance = address(this).balance - msg.value;
if (thisBalance > 0) WRAPPED_NATIVE_TOKEN.deposit{value: thisBalance}();
}

require(token.balanceOf(address(this)) >= amount, InsufficientBalance());
require(isRouteAllowed(destinationPool, destinationDomain, provider), RouteDenied());

Expand All @@ -151,7 +163,7 @@ contract Repayer is IRepayer, AccessControlUpgradeable, CCTPAdapter, AcrossAdapt
} else
if (provider == Provider.CCTP) {
require(token == ASSETS, InvalidToken());
initiateTransferCCTP(ASSETS, amount, destinationPool, destinationDomain);
initiateTransferCCTP(token, amount, destinationPool, destinationDomain);
} else
if (provider == Provider.ACROSS) {
initiateTransferAcross(token, amount, destinationPool, destinationDomain, extraData);
Expand All @@ -161,6 +173,11 @@ contract Repayer is IRepayer, AccessControlUpgradeable, CCTPAdapter, AcrossAdapt
} else
if (provider == Provider.STARGATE) {
initiateTransferStargate(token, amount, destinationPool, destinationDomain, extraData, _msgSender());
} else
if (provider == Provider.OPTIMISM_STANDARD_BRIDGE) {
initiateTransferOptimismStandardBridge(
token, amount, destinationPool, destinationDomain, extraData, DOMAIN
);
} else {
// Unreachable atm, but could become so when more providers are added to enum.
revert UnsupportedProvider();
Expand Down
64 changes: 64 additions & 0 deletions contracts/interfaces/IOptimism.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

interface IOptimismStandardBridge {

/// @notice Emitted when an ERC20 bridge is initiated to the other chain.
/// @param localToken Address of the ERC20 on this chain.
/// @param remoteToken Address of the ERC20 on the remote chain.
/// @param from Address of the sender.
/// @param to Address of the receiver.
/// @param amount Amount of the ERC20 sent.
/// @param extraData Extra data sent with the transaction.
event ERC20BridgeInitiated(
address indexed localToken,
address indexed remoteToken,
address indexed from,
address to,
uint256 amount,
bytes extraData
);

/// @notice Emitted when an ETH bridge is initiated to the other chain.
/// @param from Address of the sender.
/// @param to Address of the receiver.
/// @param amount Amount of ETH sent.
/// @param extraData Extra data sent with the transaction.
event ETHBridgeInitiated(address indexed from, address indexed to, uint256 amount, bytes extraData);

/// @notice Sends ERC20 tokens to a receiver's address on the other chain. Note that if the
/// ERC20 token on the other chain does not recognize the local token as the correct
/// pair token, the ERC20 bridge will fail and the tokens will be returned to sender on
/// this chain.
/// @param _localToken Address of the ERC20 on this chain.
/// @param _remoteToken Address of the corresponding token on the remote chain.
/// @param _to Address of the receiver.
/// @param _amount Amount of local tokens to deposit.
/// @param _minGasLimit Minimum amount of gas that the bridge can be relayed with.
/// @param _extraData Extra data to be sent with the transaction. Note that the recipient will
/// not be triggered with this data, but it will be emitted and can be used
/// to identify the transaction.
function bridgeERC20To(
address _localToken,
address _remoteToken,
address _to,
uint256 _amount,
uint32 _minGasLimit,
bytes calldata _extraData
)
external;

/// @notice Sends ETH to a receiver's address on the other chain. Note that if ETH is sent to a
/// smart contract and the call fails, the ETH will be temporarily locked in the
/// StandardBridge on the other chain until the call is replayed. If the call cannot be
/// replayed with any amount of gas (call always reverts), then the ETH will be
/// permanently locked in the StandardBridge on the other chain. ETH will also
/// be locked if the receiver is the other bridge, because finalizeBridgeETH will revert
/// in that case.
/// @param _to Address of the receiver.
/// @param _minGasLimit Minimum amount of gas that the bridge can be relayed with.
/// @param _extraData Extra data to be sent with the transaction. Note that the recipient will
/// not be triggered with this data, but it will be emitted and can be used
/// to identify the transaction.
function bridgeETHTo(address _to, uint32 _minGasLimit, bytes calldata _extraData) external payable;
}
3 changes: 2 additions & 1 deletion contracts/interfaces/IRoute.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ interface IRoute {
CCTP,
ACROSS,
STARGATE,
EVERCLEAR
EVERCLEAR,
OPTIMISM_STANDARD_BRIDGE
}

enum PoolType {
Expand Down
1 change: 1 addition & 0 deletions contracts/interfaces/IWrappedNativeToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ interface IWrappedNativeToken is IERC20 {
event Deposit(address indexed to, uint256 amount);

function deposit() external payable;
function withdraw(uint256 amount) external;
}
41 changes: 41 additions & 0 deletions contracts/testing/TestOptimism.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity 0.8.28;

import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IOptimismStandardBridge} from "../interfaces/IOptimism.sol";

contract TestOptimismStandardBridge is IOptimismStandardBridge {
error OptimismBridgeWrongRemoteToken();
error OptimismBridgeWrongMinGasLimit();

function bridgeERC20To(
address _localToken,
address _remoteToken,
address _to,
uint256 _amount,
uint32 /*_minGasLimit*/,
bytes calldata _extraData
) external override {
require(
_localToken != _remoteToken,
OptimismBridgeWrongRemoteToken()
); // To simulate revert.
SafeERC20.safeTransferFrom(IERC20(_localToken), msg.sender, address(this), _amount);
emit ERC20BridgeInitiated(
_localToken,
_remoteToken,
address(this),
_to,
_amount,
_extraData
);
}

function bridgeETHTo(address _to, uint32 _minGasLimit, bytes calldata _extraData) external payable override {
require(
_minGasLimit > 0,
OptimismBridgeWrongMinGasLimit()
); // To simulate revert.
emit ETHBridgeInitiated(address(this), _to, msg.value, _extraData);
}
}
6 changes: 4 additions & 2 deletions contracts/testing/TestRepayer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ contract TestRepayer is Repayer {
address acrossSpokePool,
address everclearFeeAdapter,
address wrappedNativeToken,
address stargateTreasurer
address stargateTreasurer,
address optimismBridge
) Repayer(
localDomain,
assets,
Expand All @@ -21,7 +22,8 @@ contract TestRepayer is Repayer {
acrossSpokePool,
everclearFeeAdapter,
wrappedNativeToken,
stargateTreasurer
stargateTreasurer,
optimismBridge
) {}

function domainCCTP(Domain destinationDomain) public pure override returns (uint32) {
Expand Down
10 changes: 10 additions & 0 deletions contracts/testing/TestWETH.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,21 @@ import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract TestWETH is ERC20 {
event Deposit(address indexed to, uint256 amount);
event Withdrawal(address indexed from, uint256 amount);

error WithdrawalFailed();

constructor() ERC20("Wrapped Ether", "WETH") {}

function deposit() external payable {
_mint(msg.sender, msg.value);
emit Deposit(msg.sender, msg.value);
}

function withdraw(uint256 amount) external {
_burn(msg.sender, amount);
(bool success, ) = payable(msg.sender).call{value: amount}("");
require(success, WithdrawalFailed());
emit Withdrawal(msg.sender, amount);
}
}
6 changes: 6 additions & 0 deletions contracts/utils/Constants.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity 0.8.28;

import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";

IERC20 constant NATIVE_TOKEN = IERC20(address(0));
55 changes: 55 additions & 0 deletions contracts/utils/OptimismStandardBridgeAdapter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity 0.8.28;

import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IOptimismStandardBridge} from ".././interfaces/IOptimism.sol";
import {IWrappedNativeToken} from ".././interfaces/IWrappedNativeToken.sol";
import {AdapterHelper} from "./AdapterHelper.sol";

abstract contract OptimismStandardBridgeAdapter is AdapterHelper {
using SafeERC20 for IERC20;

IOptimismStandardBridge immutable public OPTIMISM_STANDARD_BRIDGE;
IWrappedNativeToken immutable private WRAPPED_NATIVE_TOKEN;

constructor(
address optimismStandardBridge,
address wrappedNativeToken
) {
// No check for address(0) to allow deployment on chains where Optimism Standard Bridge is not available
OPTIMISM_STANDARD_BRIDGE = IOptimismStandardBridge(optimismStandardBridge);
WRAPPED_NATIVE_TOKEN = IWrappedNativeToken(wrappedNativeToken);
}

function initiateTransferOptimismStandardBridge(
IERC20 token,
uint256 amount,
address destinationPool,
Domain destinationDomain,
bytes calldata extraData,
Domain localDomain
) internal {
// We are only interested in fast L1->L2 bridging, because the reverse is slow.
require(
localDomain == Domain.ETHEREUM && destinationDomain == Domain.OP_MAINNET,
UnsupportedDomain()
);
require(address(OPTIMISM_STANDARD_BRIDGE) != address(0), ZeroAddress());
(address outputToken, uint32 minGasLimit) = abi.decode(extraData, (address, uint32));
if (token == WRAPPED_NATIVE_TOKEN) {
WRAPPED_NATIVE_TOKEN.withdraw(amount);
OPTIMISM_STANDARD_BRIDGE.bridgeETHTo{value: amount}(destinationPool, minGasLimit, "");
return;
}

token.forceApprove(address(OPTIMISM_STANDARD_BRIDGE), amount);
OPTIMISM_STANDARD_BRIDGE.bridgeERC20To(
address(token),
outputToken,
destinationPool,
amount,
minGasLimit,
"" // message
);
}
}
4 changes: 2 additions & 2 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,8 +297,8 @@ const config: HardhatUserConfig = {
},
hardhat: {
forking: {
url: isSet(process.env.DRY_RUN)
? process.env[`${process.env.DRY_RUN}_RPC`]!
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"),
},
accounts: isSet(process.env.DRY_RUN)
Expand Down
6 changes: 5 additions & 1 deletion network.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export enum Provider {
CCTP = "CCTP",
ACROSS = "ACROSS",
EVERCLEAR = "EVERCLEAR",
OPTIMISM_STANDARD_BRIDGE = "OPTIMISM_STANDARD_BRIDGE",
};

interface CCTPConfig {
Expand Down Expand Up @@ -84,6 +85,7 @@ export interface NetworkConfig {
AcrossV3SpokePool?: string;
StargateTreasurer?: string;
EverclearFeeAdapter?: string;
OptimismStandardBridge?: string;
USDC: string;
WrappedNativeToken: string;
RebalancerRoutes?: RebalancerRoutesConfig;
Expand Down Expand Up @@ -116,6 +118,7 @@ export const networkConfig: NetworksConfig = {
AcrossV3SpokePool: "0x5c7BCd6E7De5423a257D81B442095A1a6ced35C5",
StargateTreasurer: "0x1041D127b2d4BC700F0F563883bC689502606918",
EverclearFeeAdapter: "0x15a7cA97D1ed168fB34a4055CEFa2E2f9Bdb6C75",
OptimismStandardBridge: "0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1",
USDC: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
WrappedNativeToken: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
IsTest: false,
Expand Down Expand Up @@ -163,7 +166,8 @@ export const networkConfig: NetworksConfig = {
AcrossV3SpokePool: "0x6f26Bf09B1C792e3228e5467807a900A503c0281",
StargateTreasurer: "0x644abb1e17291b4403966119d15Ab081e4a487e9",
EverclearFeeAdapter: "0x15a7cA97D1ed168fB34a4055CEFa2E2f9Bdb6C75",
USDC: "0x0b2c639c533813f4aa9d7837caf62653d097ff85",
OptimismStandardBridge: "0x4200000000000000000000000000000000000010",
USDC: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
WrappedNativeToken: "0x4200000000000000000000000000000000000006",
IsTest: false,
Admin: "0x4eA9E682BA79bC403523c9e8D98A05EaF3810636",
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"lint:solidity": "solhint 'contracts/**/*.sol'",
"lint:ts": "eslint",
"test": "hardhat test --typecheck",
"test:ethereum": "FORK_TEST=ETHEREUM hardhat test --typecheck ./specific-fork-test/ethereum/*.ts",
"test:deploy": "ts-node --files ./scripts/deploy.ts",
"test:scripts": "SCRIPT_ENV=CI DEPLOY_ID=CI ts-node --files ./scripts/test.ts"
},
Expand Down
1 change: 1 addition & 0 deletions scripts/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const ProviderSolidity = {
ACROSS: 2n,
STARGATE: 3n,
EVERCLEAR: 4n,
OPTIMISM_STANDARD_BRIDGE: 5n,
};

export const DomainSolidity = {
Expand Down
Loading