Skip to content
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

feat: L3 withdrawal helpers #605

Merged
merged 36 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
757e802
refactor: export _bridgeTokensToHubPool for Arbitrum to separate cont…
bmzig Sep 13, 2024
1a49346
feat: add Intermediate_TokenRetriever contract for L3-L1 withdrawals
bmzig Sep 13, 2024
ac13aac
generalize bridge adapter contract
bmzig Sep 16, 2024
3131eed
add functionality for retrieveMany. Refactor WithdrawalAdapter
bmzig Sep 16, 2024
f9c0d62
feat: add L2 forwarder interface
bmzig Sep 17, 2024
f6b2b30
Merge branch 'master' into bz/withdrawalAdapter
bmzig Sep 17, 2024
d3f624b
apply suggested changes
bmzig Sep 17, 2024
cea0337
sync with upstream changes
bmzig Sep 18, 2024
0f68634
explicit imports
bmzig Sep 18, 2024
a0a371d
add a withdrawal adapter for OVM chains
bmzig Sep 18, 2024
0de9303
refactor and rename
bmzig Sep 19, 2024
0371348
Merge branch 'master' into bz/l2ForwarderInterface
bmzig Sep 20, 2024
942f741
WIP remove spoke pool dependency
bmzig Sep 23, 2024
3cbf3dd
Merge branch 'bz/l2ForwarderInterface' into bz/withdrawalAdapter
bmzig Sep 23, 2024
fd94012
clean up interfaces
bmzig Sep 23, 2024
b95cb01
Merge branch 'bz/l2ForwarderInterface' into bz/withdrawalAdapter
bmzig Sep 23, 2024
4c41098
arbitrum withdrawal adapter refactoring
bmzig Sep 23, 2024
f2e3c41
simplify forwarder base and make it a proxy
bmzig Sep 23, 2024
f251354
disable implementation initialization
bmzig Sep 23, 2024
050c784
Merge branch 'bz/l2ForwarderInterface' into bz/withdrawalAdapter
bmzig Sep 23, 2024
982eff7
update tests for new arbitrum bridge checks
bmzig Sep 23, 2024
546f9ff
change variable names
bmzig Sep 24, 2024
d1a6143
Merge branch 'master' into bz/withdrawalAdapter
bmzig Oct 2, 2024
4db2572
refactor of withdrawal adapter format
bmzig Oct 2, 2024
db64f5f
remove unused test imports
bmzig Oct 2, 2024
6dc1482
Merge branch 'master' into bz/withdrawalAdapter
bmzig Oct 3, 2024
1882c5f
Merge branch 'master' into bz/withdrawalAdapter
bmzig Oct 3, 2024
da1bc9a
rename and add test
bmzig Oct 3, 2024
2056784
add natspec comment and change name to crossDomainAdmin
bmzig Oct 4, 2024
450cfe5
add vm.assume to test to address fuzz where l1Token = invalidToken
bmzig Oct 4, 2024
bcaa0f4
make the helper into a proxy
bmzig Oct 4, 2024
96a4e1b
move init functions to chain-specific adapters
bmzig Oct 4, 2024
167b8a9
address comments
bmzig Oct 4, 2024
3470649
inherit errors
bmzig Oct 4, 2024
47c8537
change _minGasLimit from a u256 to a u32
bmzig Oct 7, 2024
5ab22c3
update test interfaces
bmzig Oct 7, 2024
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: 1 addition & 1 deletion contracts/Ovm_SpokePool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ interface IL2ERC20Bridge {
address _remoteToken,
address _to,
uint256 _amount,
uint256 _minGasLimit,
uint32 _minGasLimit,
bytes calldata _extraData
) external;
}
Expand Down
92 changes: 92 additions & 0 deletions contracts/chain-adapters/l2/Arbitrum_WithdrawalHelper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// SPDX-License-Identifier: BUSL-1.1

// Arbitrum only supports v0.8.19
// See https://docs.arbitrum.io/for-devs/concepts/differences-between-arbitrum-ethereum/solidity-support#differences-from-solidity-on-ethereum
pragma solidity ^0.8.19;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { ArbitrumL2ERC20GatewayLike } from "../../interfaces/ArbitrumBridge.sol";
import { WithdrawalHelperBase } from "./WithdrawalHelperBase.sol";
import { ITokenMessenger } from "../../external/interfaces/CCTPInterfaces.sol";
import { CrossDomainAddressUtils } from "../../libraries/CrossDomainAddressUtils.sol";

/**
* @title Arbitrum_WithdrawalHelper
* @notice This contract interfaces with L2-L1 token bridges and withdraws tokens to a single address on L1.
* @dev This contract should be deployed on Arbitrum L2s which only use CCTP or the canonical Arbitrum gateway router to withdraw tokens.
* @custom:security-contact [email protected]
*/
contract Arbitrum_WithdrawalHelper is WithdrawalHelperBase {
using SafeERC20 for IERC20;

// Error which triggers when the supplied L1 token does not match the Arbitrum gateway router's expected L2 token.
error InvalidTokenMapping();

/*
* @notice Constructs the Arbitrum_WithdrawalHelper.
* @param _l2Usdc Address of native USDC on the L2.
* @param _cctpTokenMessenger Address of the CCTP token messenger contract on L2.
* @param _destinationCircleDomainId Circle's assigned CCTP domain ID for the destination network. For Ethereum, this is 0.
* @param _l2GatewayRouter Address of the Arbitrum l2 gateway router contract.
* @param _tokenRecipient L1 Address which will unconditionally receive tokens withdrawn from this contract.
*/
constructor(
IERC20 _l2Usdc,
ITokenMessenger _cctpTokenMessenger,
uint32 _destinationCircleDomainId,
address _l2GatewayRouter,
address _tokenRecipient
)
WithdrawalHelperBase(
_l2Usdc,
_cctpTokenMessenger,
_destinationCircleDomainId,
_l2GatewayRouter,
_tokenRecipient
)
{}

/**
* @notice Initializes the withdrawal helper contract.
* @param _crossDomainAdmin L1 address of the contract which can send root bundles/messages to this forwarder contract.
*/
function initialize(address _crossDomainAdmin) public initializer {
__WithdrawalHelper_init(_crossDomainAdmin);
}

/*
* @notice Calls CCTP or the Arbitrum gateway router to withdraw tokens back to the TOKEN_RECIPIENT L1 address.
* @param l1Token Address of the L1 token to receive.
* @param l2Token Address of the L2 token to send back.
* @param amountToReturn Amount of l2Token to send back.
*/
function withdrawToken(
address l1Token,
address l2Token,
uint256 amountToReturn
) public override {
// If the l2TokenAddress is UDSC, we need to use the CCTP bridge.
if (l2Token == address(usdcToken) && _isCCTPEnabled()) {
_transferUsdc(TOKEN_RECIPIENT, amountToReturn);
} else {
// Otherwise, we use the Arbitrum ERC20 Gateway router.
ArbitrumL2ERC20GatewayLike tokenBridge = ArbitrumL2ERC20GatewayLike(L2_TOKEN_GATEWAY);
// If the gateway router's expected L2 token address does not match then revert. This check does not actually
// impact whether the bridge will succeed, since the ERC20 gateway router only requires the L1 token address, but
// it is added here to potentially catch scenarios where there was a mistake in the calldata.
if (tokenBridge.calculateL2TokenAddress(l1Token) != l2Token) revert InvalidTokenMapping();
//slither-disable-next-line unused-return
tokenBridge.outboundTransfer(
l1Token, // _l1Token. Address of the L1 token to bridge over.
TOKEN_RECIPIENT, // _to. Withdraw, over the bridge, to the recipient.
amountToReturn, // _amount.
"" // _data. We don't need to send any data for the bridging action.
);
}
}

function _requireAdminSender() internal view override {
if (msg.sender != CrossDomainAddressUtils.applyL1ToL2Alias(crossDomainAdmin)) revert NotCrossDomainAdmin();
}
}
171 changes: 171 additions & 0 deletions contracts/chain-adapters/l2/Ovm_WithdrawalHelper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// SPDX-License-Identifier: BUSL-1.1

pragma solidity ^0.8.0;

import { WithdrawalHelperBase } from "./WithdrawalHelperBase.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { WETH9Interface } from "../../external/interfaces/WETH9Interface.sol";
import { ITokenMessenger } from "../../external/interfaces/CCTPInterfaces.sol";
import { Lib_PredeployAddresses } from "@eth-optimism/contracts/libraries/constants/Lib_PredeployAddresses.sol";
import { LibOptimismUpgradeable } from "@openzeppelin/contracts-upgradeable/crosschain/optimism/LibOptimismUpgradeable.sol";
import { IL2ERC20Bridge } from "../../Ovm_SpokePool.sol";

/**
* @notice Minimal interface for the Ovm_SpokePool contract. This interface is called to pull state from the network's
* spoke pool contract to be used by this withdrawal adapter.
*/
interface IOvm_SpokePool {
// Returns the address of the token bridge for the input l2 token.
function tokenBridges(address token) external view returns (address);

// Returns the address of the l1 token set in the spoke pool for the input l2 token.
function remoteL1Tokens(address token) external view returns (address);

// Returns the address for the representation of ETH on the l2.
function l2Eth() external view returns (address);

// Returns the address of the wrapped native token for the L2.
function wrappedNativeToken() external view returns (WETH9Interface);

// Returns the amount of gas the contract allocates for a token withdrawal.
function l1Gas() external view returns (uint32);
}

/**
* @title Ovm_WithdrawalAdapter
* @notice This contract interfaces with L2-L1 token bridges and withdraws tokens to a single address on L1.
* @dev This contract should be deployed on OpStack L2s which both have a Ovm_SpokePool contract deployed to the L2
* network AND only use token bridges defined in the Ovm_SpokePool. A notable exception to this requirement is Optimism,
* which has a special SNX bridge (and thus this adapter will NOT work for Optimism).
* @custom:security-contact [email protected]
*/
contract Ovm_WithdrawalHelper is WithdrawalHelperBase {
using SafeERC20 for IERC20;

// Address for the wrapped native token on this chain. For Ovm standard bridges, we need to unwrap
// this token before initiating the withdrawal. Normally, it is 0x42..006, but there are instances
// where this address is different.
WETH9Interface public immutable wrappedNativeToken;
// Address of the corresponding spoke pool on L2. This is to piggyback off of the spoke pool's supported
// token routes/defined token bridges.
IOvm_SpokePool public immutable spokePool;
// Address of native ETH on the l2. For OpStack chains, this address is used to indicate a native ETH withdrawal.
// In general, this address is 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000.
address public immutable l2Eth;
// Address of the messenger contract on L2. This is by default defined in Lib_PredeployAddresses.
address public constant MESSENGER = Lib_PredeployAddresses.L2_CROSS_DOMAIN_MESSENGER;

/*
* @notice Constructs the Ovm_WithdrawalAdapter.
* @param _l2Usdc Address of native USDC on the L2.
* @param _cctpTokenMessenger Address of the CCTP token messenger contract on L2.
* @param _destinationCircleDomainId Circle's assigned CCTP domain ID for the destination network. For Ethereum, this
* is 0.
* @param _l2Gateway Address of the Optimism ERC20 L2 standard bridge contract.
* @param _tokenRecipient The L1 address which will unconditionally receive tokens from withdrawals by this contract.
* @param _crossDomainAdmin Address of the admin on L1. This address is the only one which may tell this contract to send tokens to an
* L2 address.
* @param _spokePool The contract address of the Ovm_SpokePool which is deployed on this L2 network.
*/
constructor(
IERC20 _l2Usdc,
ITokenMessenger _cctpTokenMessenger,
uint32 _destinationCircleDomainId,
address _l2Gateway,
address _tokenRecipient,
IOvm_SpokePool _spokePool
) WithdrawalHelperBase(_l2Usdc, _cctpTokenMessenger, _destinationCircleDomainId, _l2Gateway, _tokenRecipient) {
spokePool = _spokePool;

// These addresses should only change network-by-network, or after a bridge upgrade, so we define them once in the constructor.
wrappedNativeToken = spokePool.wrappedNativeToken();
l2Eth = spokePool.l2Eth();
}

/**
* @notice Initializes the withdrawal helper contract.
* @param _crossDomainAdmin L1 address of the contract which can send root bundles/messages to this forwarder contract.
*/
function initialize(address _crossDomainAdmin) public initializer {
__WithdrawalHelper_init(_crossDomainAdmin);
}

/*
* @notice Calls CCTP or the Optimism token gateway to withdraw tokens back to the recipient.
* @param l2Token address of the l2Token to send back.
* @param amountToReturn amount of l2Token to send back.
* @dev The l1Token parameter is unused since we obtain the l1Token to receive by querying the state of the Ovm_SpokePool deployed
* to this network.
* @dev This function is a copy of the `_bridgeTokensToHubPool` function found on the Ovm_SpokePool contract here:
* https://github.com/across-protocol/contracts/blob/65191dbcded95c8fe050e0f95eb7848e3784e61f/contracts/Ovm_SpokePool.sol#L148.
* New lines of code correspond to instances where this contract queries state from the spoke pool, such as determining
* the appropriate token bridge for the withdrawal or finding the remoteL1Token to withdraw.
*/
function withdrawToken(
address,
address l2Token,
uint256 amountToReturn
) public override {
// Fetch the current l1Gas defined in the Ovm_SpokePool.
uint32 l1Gas = spokePool.l1Gas();
// If the token being bridged is WETH then we need to first unwrap it to ETH and then send ETH over the
// canonical bridge. On Optimism, this is address 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000.
if (l2Token == address(wrappedNativeToken)) {
WETH9Interface(l2Token).withdraw(amountToReturn); // Unwrap into ETH.
l2Token = l2Eth; // Set the l2Token to ETH.
IL2ERC20Bridge(Lib_PredeployAddresses.L2_STANDARD_BRIDGE).withdrawTo{ value: amountToReturn }(
l2Token, // _l2Token. Address of the L2 token to bridge over.
TOKEN_RECIPIENT, // _to. Withdraw, over the bridge, to the l1 pool contract.
amountToReturn, // _amount.
l1Gas, // _l1Gas. Unused, but included for potential forward compatibility considerations
"" // _data. We don't need to send any data for the bridging action.
);
}
// If the token is USDC && CCTP bridge is enabled, then bridge USDC via CCTP.
else if (l2Token == address(usdcToken) && _isCCTPEnabled()) {
_transferUsdc(TOKEN_RECIPIENT, amountToReturn);
}
// Note we'll default to withdrawTo instead of bridgeERC20To unless the remoteL1Tokens mapping is set for
// the l2Token. withdrawTo should be used to bridge back non-native L2 tokens
// (i.e. non-native L2 tokens have a canonical L1 token). If we should bridge "native L2" tokens then
// we'd need to call bridgeERC20To and give allowance to the tokenBridge to spend l2Token from this contract.
// Therefore for native tokens we should set ensure that remoteL1Tokens is set for the l2Token.
else {
IL2ERC20Bridge tokenBridge = IL2ERC20Bridge(
spokePool.tokenBridges(l2Token) == address(0)
? Lib_PredeployAddresses.L2_STANDARD_BRIDGE
: spokePool.tokenBridges(l2Token)
);
address remoteL1Token = spokePool.remoteL1Tokens(l2Token);
if (remoteL1Token != address(0)) {
// If there is a mapping for this L2 token to an L1 token, then use the L1 token address and
// call bridgeERC20To.
IERC20(l2Token).safeIncreaseAllowance(address(tokenBridge), amountToReturn);
tokenBridge.bridgeERC20To(
l2Token, // _l2Token. Address of the L2 token to bridge over.
remoteL1Token, // Remote token to be received on L1 side. If the
// remoteL1Token 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.
TOKEN_RECIPIENT, // _to
amountToReturn, // _amount
l1Gas, // _l1Gas
"" // _data
);
} else {
tokenBridge.withdrawTo(
l2Token, // _l2Token. Address of the L2 token to bridge over.
TOKEN_RECIPIENT, // _to. Withdraw, over the bridge, to the l1 pool contract.
amountToReturn, // _amount.
l1Gas, // _l1Gas. Unused, but included for potential forward compatibility considerations
"" // _data. We don't need to send any data for the bridging action.
);
}
}
}

function _requireAdminSender() internal view override {
if (LibOptimismUpgradeable.crossChainSender(MESSENGER) != crossDomainAdmin) revert NotCrossDomainAdmin();
}
}
Loading
Loading