diff --git a/packages/contracts-bedrock/src/L1/IPartialUsdc.sol b/packages/contracts-bedrock/src/L1/IPartialUsdc.sol new file mode 100644 index 000000000000..4506685a4ec9 --- /dev/null +++ b/packages/contracts-bedrock/src/L1/IPartialUsdc.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: APACHE-2.0 +pragma solidity 0.8.15; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/// @notice The parts of the USDC interface we need. +interface IPartialUsdc is IERC20 { + function mint(address _to, uint256 _amount) external; + function burn(uint256 _amount) external; +} diff --git a/packages/contracts-bedrock/src/L1/L1UsdcBridge.sol b/packages/contracts-bedrock/src/L1/L1UsdcBridge.sol index 8e8cf598e647..8fb4394d9b67 100644 --- a/packages/contracts-bedrock/src/L1/L1UsdcBridge.sol +++ b/packages/contracts-bedrock/src/L1/L1UsdcBridge.sol @@ -5,35 +5,16 @@ import { Predeploys } from "src/libraries/Predeploys.sol"; import { UsdcBridge } from "src/universal/UsdcBridge.sol"; import { ISemver } from "src/universal/ISemver.sol"; import { CrossDomainMessenger } from "src/universal/CrossDomainMessenger.sol"; -import { SuperchainConfig } from "src/L1/SuperchainConfig.sol"; -import { Constants } from "src/libraries/Constants.sol"; +import { IPartialUsdc } from "src/L1/IPartialUsdc.sol"; /// @custom:proxied /// @title L1UsdcBridge -/// @notice The L1UsdcBridge is responsible for transfering ETH and ERC20 tokens between L1 and +/// @notice The L1UsdcBridge is responsible for transfering USDC tokens between L1 and /// L2. In the case that an ERC20 token is native to L1, it will be escrowed within this -/// contract. If the ERC20 token is native to L2, it will be burnt. Before Bedrock, ETH was -/// stored within this contract. After Bedrock, ETH is instead stored inside the -/// OptimismPortal contract. -/// NOTE: this contract is not intended to support all variations of ERC20 tokens. Examples -/// of some token types that may not be properly supported by this contract include, but are -/// not limited to: tokens with transfer fees, rebasing tokens, and tokens with blocklists. +/// contract. If the ERC20 token is native to L2, it will be burnt. contract L1UsdcBridge is UsdcBridge, ISemver { - /// @custom:legacy - /// @notice Emitted whenever a deposit of ETH from L1 into L2 is initiated. - /// @param from Address of the depositor. - /// @param to Address of the recipient on L2. - /// @param amount Amount of ETH deposited. - /// @param extraData Extra data attached to the deposit. - event ETHDepositInitiated(address indexed from, address indexed to, uint256 amount, bytes extraData); - - /// @custom:legacy - /// @notice Emitted whenever a withdrawal of ETH from L2 to L1 is finalized. - /// @param from Address of the withdrawer. - /// @param to Address of the recipient on L1. - /// @param amount Amount of ETH withdrawn. - /// @param extraData Extra data attached to the withdrawal. - event ETHWithdrawalFinalized(address indexed from, address indexed to, uint256 amount, bytes extraData); + /// @notice The address that is allowed to burn the held USDC. + address public burner; /// @custom:legacy /// @notice Emitted whenever an ERC20 deposit is initiated. @@ -73,58 +54,49 @@ contract L1UsdcBridge is UsdcBridge, ISemver { /// @custom:semver 2.1.0 string public constant version = "2.1.0"; - /// @notice Address of the SuperchainConfig contract. - SuperchainConfig public superchainConfig; - /// @notice Constructs the L1UsdcBridge contract. - constructor() UsdcBridge() { - initialize({ _messenger: CrossDomainMessenger(address(0)), _superchainConfig: SuperchainConfig(address(0)) }); - } - - /// @notice Initializer. - /// @param _messenger Contract for the CrossDomainMessenger on this network. - /// @param _superchainConfig Contract for the SuperchainConfig on this network. - function initialize(CrossDomainMessenger _messenger, SuperchainConfig _superchainConfig) public initializer { - superchainConfig = _superchainConfig; + constructor() UsdcBridge() { } + + /// @notice Initializes the contract. + /// @param _messenger The cross domain messenger proxy. + /// @param _otherBridge The L2 bridge address. + /// @param _l1Usdc The ERC20 address on the L1. + /// @param _l2Usdc The ERC20 address on the L2. + /// @param _owner The initial owner of this contract. + function initialize( + CrossDomainMessenger _messenger, + UsdcBridge _otherBridge, + address _l1Usdc, + address _l2Usdc, + address _owner + ) + public + initializer + { __UsdcBridge_init({ _messenger: _messenger, - _otherBridge: UsdcBridge(payable(Predeploys.L2_STANDARD_BRIDGE)) + _otherBridge: _otherBridge, + _l1Usdc: _l1Usdc, + _l2Usdc: _l2Usdc, + _owner: _owner }); } - /// @inheritdoc UsdcBridge - function paused() public view override returns (bool) { - return superchainConfig.paused(); - } - - /// @notice Allows EOAs to bridge ETH by sending directly to the bridge. - receive() external payable override onlyEOA { - _initiateETHDeposit(msg.sender, msg.sender, RECEIVE_DEFAULT_GAS_LIMIT, bytes("")); + /// @notice Whitelists an account that will be able to burn the held usdc. + /// This function is callable only once. + function whitelistBurner(address _burner) external onlyOwner { + require(_burner != address(0), "Burner address can not be zero"); + require(burner == address(0), "Burner address already set"); + burner = _burner; } - /// @custom:legacy - /// @notice Deposits some amount of ETH into the sender's account on L2. - /// @param _minGasLimit Minimum gas limit for the deposit message on L2. - /// @param _extraData Optional data to forward to L2. - /// Data supplied here will not be used to execute any code on L2 and is - /// only emitted as extra data for the convenience of off-chain tooling. - function depositETH(uint32 _minGasLimit, bytes calldata _extraData) external payable onlyEOA { - _initiateETHDeposit(msg.sender, msg.sender, _minGasLimit, _extraData); - } + /// @notice Burns all the l1 token held by this contract. + function burnLockedUSDC() external { + require(msg.sender == burner, "Not whitelisted"); - /// @custom:legacy - /// @notice Deposits some amount of ETH into a target account on L2. - /// Note that if ETH is sent to a contract on L2 and the call fails, then that ETH will - /// be locked in the L2UsdcBridge. ETH may be recoverable if the call can be - /// successfully replayed by increasing the amount of gas supplied to the call. If the - /// call will fail for any amount of gas, then the ETH will be locked permanently. - /// @param _to Address of the recipient on L2. - /// @param _minGasLimit Minimum gas limit for the deposit message on L2. - /// @param _extraData Optional data to forward to L2. - /// Data supplied here will not be used to execute any code on L2 and is - /// only emitted as extra data for the convenience of off-chain tooling. - function depositETHTo(address _to, uint32 _minGasLimit, bytes calldata _extraData) external payable { - _initiateETHDeposit(msg.sender, _to, _minGasLimit, _extraData); + IPartialUsdc token = IPartialUsdc(l1Usdc); + uint256 balance = token.balanceOf(address(this)); + token.burn(balance); } /// @custom:legacy @@ -174,24 +146,6 @@ contract L1UsdcBridge is UsdcBridge, ISemver { _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, _to, _amount, _minGasLimit, _extraData); } - /// @custom:legacy - /// @notice Finalizes a withdrawal of ETH from L2. - /// @param _from Address of the withdrawer on L2. - /// @param _to Address of the recipient on L1. - /// @param _amount Amount of ETH to withdraw. - /// @param _extraData Optional data forwarded from L2. - function finalizeETHWithdrawal( - address _from, - address _to, - uint256 _amount, - bytes calldata _extraData - ) - external - payable - { - finalizeBridgeETH(_from, _to, _amount, _extraData); - } - /// @custom:legacy /// @notice Finalizes a withdrawal of ERC20 tokens from L2. /// @param _l1Token Address of the token on L1. @@ -220,15 +174,6 @@ contract L1UsdcBridge is UsdcBridge, ISemver { return address(otherBridge); } - /// @notice Internal function for initiating an ETH deposit. - /// @param _from Address of the sender on L1. - /// @param _to Address of the recipient on L2. - /// @param _minGasLimit Minimum gas limit for the deposit message on L2. - /// @param _extraData Optional data to forward to L2. - function _initiateETHDeposit(address _from, address _to, uint32 _minGasLimit, bytes memory _extraData) internal { - _initiateBridgeETH(_from, _to, msg.value, _minGasLimit, _extraData); - } - /// @notice Internal function for initiating an ERC20 deposit. /// @param _l1Token Address of the L1 token being deposited. /// @param _l2Token Address of the corresponding token on L2. @@ -251,38 +196,6 @@ contract L1UsdcBridge is UsdcBridge, ISemver { _initiateBridgeERC20(_l1Token, _l2Token, _from, _to, _amount, _minGasLimit, _extraData); } - /// @inheritdoc UsdcBridge - /// @notice Emits the legacy ETHDepositInitiated event followed by the ETHBridgeInitiated event. - /// This is necessary for backwards compatibility with the legacy bridge. - function _emitETHBridgeInitiated( - address _from, - address _to, - uint256 _amount, - bytes memory _extraData - ) - internal - override - { - emit ETHDepositInitiated(_from, _to, _amount, _extraData); - super._emitETHBridgeInitiated(_from, _to, _amount, _extraData); - } - - /// @inheritdoc UsdcBridge - /// @notice Emits the legacy ERC20DepositInitiated event followed by the ERC20BridgeInitiated - /// event. This is necessary for backwards compatibility with the legacy bridge. - function _emitETHBridgeFinalized( - address _from, - address _to, - uint256 _amount, - bytes memory _extraData - ) - internal - override - { - emit ETHWithdrawalFinalized(_from, _to, _amount, _extraData); - super._emitETHBridgeFinalized(_from, _to, _amount, _extraData); - } - /// @inheritdoc UsdcBridge /// @notice Emits the legacy ERC20WithdrawalFinalized event followed by the ERC20BridgeFinalized /// event. This is necessary for backwards compatibility with the legacy bridge. diff --git a/packages/contracts-bedrock/src/L2/L2UsdcBridge.sol b/packages/contracts-bedrock/src/L2/L2UsdcBridge.sol index dc6cc5efe5ea..28eb99b13717 100644 --- a/packages/contracts-bedrock/src/L2/L2UsdcBridge.sol +++ b/packages/contracts-bedrock/src/L2/L2UsdcBridge.sol @@ -4,16 +4,13 @@ pragma solidity 0.8.15; import { Predeploys } from "src/libraries/Predeploys.sol"; import { UsdcBridge } from "src/universal/UsdcBridge.sol"; import { ISemver } from "src/universal/ISemver.sol"; -import { OptimismMintableERC20 } from "src/universal/OptimismMintableERC20.sol"; import { CrossDomainMessenger } from "src/universal/CrossDomainMessenger.sol"; import { Constants } from "src/libraries/Constants.sol"; /// @custom:proxied -/// @custom:predeploy 0x4200000000000000000000000000000000000010 /// @title L2UsdcBridge -/// @notice The L2UsdcBridge is responsible for transfering ETH and ERC20 tokens between L1 and -/// L2. In the case that an ERC20 token is native to L2, it will be escrowed within this -/// contract. If the ERC20 token is native to L1, it will be burnt. +/// @notice The L2UsdcBridge is responsible for transfering USDC tokens between L1 and +/// L2. /// NOTE: this contract is not intended to support all variations of ERC20 tokens. Examples /// of some token types that may not be properly supported by this contract include, but are /// not limited to: tokens with transfer fees, rebasing tokens, and tokens with blocklists. @@ -56,30 +53,26 @@ contract L2UsdcBridge is UsdcBridge, ISemver { string public constant version = "1.8.0"; /// @notice Constructs the L2UsdcBridge contract. - constructor() UsdcBridge() { - initialize({ _otherBridge: UsdcBridge(payable(address(0))) }); - } - - /// @notice Initializer. - /// @param _otherBridge Contract for the corresponding bridge on the other chain. - function initialize(UsdcBridge _otherBridge) public initializer { + constructor() UsdcBridge() { } + + /// @notice Initializes the contract. + /// @param _otherBridge The L1 bridge address. + /// @param _l1Usdc The ERC20 address on the L1. + /// @param _l2Usdc The ERC20 address on the L2. + /// @param _owner The initial owner of this contract. + function initialize(UsdcBridge _otherBridge, address _l1Usdc, address _l2Usdc, address _owner) public initializer { __UsdcBridge_init({ _messenger: CrossDomainMessenger(Predeploys.L2_CROSS_DOMAIN_MESSENGER), - _otherBridge: _otherBridge + _otherBridge: _otherBridge, + _l1Usdc: _l1Usdc, + _l2Usdc: _l2Usdc, + _owner: _owner }); } - /// @notice Allows EOAs to bridge ETH by sending directly to the bridge. - receive() external payable override onlyEOA { - _initiateWithdrawal( - Predeploys.LEGACY_ERC20_ETH, msg.sender, msg.sender, msg.value, RECEIVE_DEFAULT_GAS_LIMIT, bytes("") - ); - } - /// @custom:legacy /// @notice Initiates a withdrawal from L2 to L1. - /// This function only works with OptimismMintableERC20 tokens or ether. Use the - /// `bridgeERC20` function to bridge native L2 tokens to L1. + /// This function only works with Usdc /// @param _l2Token Address of the L2 token to withdraw. /// @param _amount Amount of the L2 token to withdraw. /// @param _minGasLimit Minimum gas limit to use for the transaction. @@ -100,12 +93,7 @@ contract L2UsdcBridge is UsdcBridge, ISemver { /// @custom:legacy /// @notice Initiates a withdrawal from L2 to L1 to a target account on L1. - /// Note that if ETH is sent to a contract on L1 and the call fails, then that ETH will - /// be locked in the L1UsdcBridge. ETH may be recoverable if the call can be - /// successfully replayed by increasing the amount of gas supplied to the call. If the - /// call will fail for any amount of gas, then the ETH will be locked permanently. - /// This function only works with OptimismMintableERC20 tokens or ether. Use the - /// `bridgeERC20To` function to bridge native L2 tokens to L1. + /// This function only works for usdc. /// @param _l2Token Address of the L2 token to withdraw. /// @param _to Recipient account on L1. /// @param _amount Amount of the L2 token to withdraw. @@ -126,8 +114,7 @@ contract L2UsdcBridge is UsdcBridge, ISemver { } /// @custom:legacy - /// @notice Finalizes a deposit from L1 to L2. To finalize a deposit of ether, use address(0) - /// and the l1Token and the Legacy ERC20 ether predeploy address as the l2Token. + /// @notice Finalizes a deposit from L1 to L2. /// @param _l1Token Address of the L1 token to deposit. /// @param _l2Token Address of the corresponding L2 token. /// @param _from Address of the depositor. @@ -146,8 +133,8 @@ contract L2UsdcBridge is UsdcBridge, ISemver { payable virtual { - if (_l1Token == address(0) && _l2Token == Predeploys.LEGACY_ERC20_ETH) { - finalizeBridgeETH(_from, _to, _amount, _extraData); + if (!_isL1Usdc(_l1Token) || !_isL2Usdc(_l2Token)) { + revert("Only USDC allowed"); } else { finalizeBridgeERC20(_l2Token, _l1Token, _from, _to, _amount, _extraData); } @@ -178,46 +165,13 @@ contract L2UsdcBridge is UsdcBridge, ISemver { ) internal { - if (_l2Token == Predeploys.LEGACY_ERC20_ETH) { - _initiateBridgeETH(_from, _to, _amount, _minGasLimit, _extraData); + if (!_isL2Usdc(_l2Token)) { + revert("Only USDC allowed"); } else { - address l1Token = OptimismMintableERC20(_l2Token).l1Token(); - _initiateBridgeERC20(_l2Token, l1Token, _from, _to, _amount, _minGasLimit, _extraData); + _initiateBridgeERC20(_l2Token, l1Usdc, _from, _to, _amount, _minGasLimit, _extraData); } } - /// @notice Emits the legacy WithdrawalInitiated event followed by the ETHBridgeInitiated event. - /// This is necessary for backwards compatibility with the legacy bridge. - /// @inheritdoc UsdcBridge - function _emitETHBridgeInitiated( - address _from, - address _to, - uint256 _amount, - bytes memory _extraData - ) - internal - override - { - emit WithdrawalInitiated(address(0), Predeploys.LEGACY_ERC20_ETH, _from, _to, _amount, _extraData); - super._emitETHBridgeInitiated(_from, _to, _amount, _extraData); - } - - /// @notice Emits the legacy DepositFinalized event followed by the ETHBridgeFinalized event. - /// This is necessary for backwards compatibility with the legacy bridge. - /// @inheritdoc UsdcBridge - function _emitETHBridgeFinalized( - address _from, - address _to, - uint256 _amount, - bytes memory _extraData - ) - internal - override - { - emit DepositFinalized(address(0), Predeploys.LEGACY_ERC20_ETH, _from, _to, _amount, _extraData); - super._emitETHBridgeFinalized(_from, _to, _amount, _extraData); - } - /// @notice Emits the legacy WithdrawalInitiated event followed by the ERC20BridgeInitiated /// event. This is necessary for backwards compatibility with the legacy bridge. /// @inheritdoc UsdcBridge diff --git a/packages/contracts-bedrock/src/L2/UsdcManager.sol b/packages/contracts-bedrock/src/L2/UsdcManager.sol new file mode 100644 index 000000000000..266a2f221f25 --- /dev/null +++ b/packages/contracts-bedrock/src/L2/UsdcManager.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.15; + +import "@openzeppelin/contracts/access/Ownable.sol"; + +interface IUsdcProxy { + function changeAdmin(address newAdmin) external; +} + +interface IUsdcImpl { + function transferOwnership(address newOwner) external; +} + +interface IMasterMinter { + function configureController(address, address) external; + function configureMinter(uint256) external; + function removeMinter() external returns (bool); +} + +contract UsdcManager is Ownable { + address whitelistedTakeoverOrigin; + address tokenProxyAddress; + address masterMinterAddress; + address bridgeAddress; + + constructor(address initialOwner) Ownable() { + transferOwnership(initialOwner); + } + + /// @notice Initializes the contract with the given addresses. Also configured the bridge + /// as a minter. Note that this contract is expected to have been assigned + /// the ownership of the USDC proxy, implementation, and master minter roles. + /// @param _bridgeAddress The L2 bridge address. + /// @param _masterMinterAddress The MasterMinter address. + /// @param _tokenProxyAddress The address of the USDC token proxy. + function initialize( + address _bridgeAddress, + address _masterMinterAddress, + address _tokenProxyAddress + ) + public + onlyOwner + { + tokenProxyAddress = _tokenProxyAddress; + bridgeAddress = _bridgeAddress; + masterMinterAddress = _masterMinterAddress; + IMasterMinter(masterMinterAddress).configureController(address(this), bridgeAddress); + IMasterMinter(masterMinterAddress).configureMinter(type(uint256).max); + } + + /// @notice Allows the given address to take over the USDC roles. Note that this + /// function can only be called once. + /// @param _whitelistedTakeoverOrigin Address to be whitelisted. + function allowTakeover(address _whitelistedTakeoverOrigin) public onlyOwner { + require(whitelistedTakeoverOrigin == address(0), "Whitelist address already set"); + whitelistedTakeoverOrigin = _whitelistedTakeoverOrigin; + } + + /// @notice Transfers USDC roles to a pre-whitelisted account and removes minting + /// privileges from the bridge. + /// @param owner Address to transfer the roles to. + function transferUSDCRoles(address owner) external { + require(msg.sender == whitelistedTakeoverOrigin, "Unauthorized transfer"); + + // Change proxy admin + IUsdcProxy(tokenProxyAddress).changeAdmin(owner); + + // remove minter + IMasterMinter(masterMinterAddress).removeMinter(); + + // Transfer implementation owner + IUsdcImpl(tokenProxyAddress).transferOwnership(owner); + } +} diff --git a/packages/contracts-bedrock/src/libraries/Pausable.sol b/packages/contracts-bedrock/src/libraries/Pausable.sol new file mode 100644 index 000000000000..6c3da70c5ef2 --- /dev/null +++ b/packages/contracts-bedrock/src/libraries/Pausable.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/Pausable.sol) + +pragma solidity ^0.8.15; + +import { Context } from "@openzeppelin/contracts/utils/Context.sol"; + +/** + * @dev Contract module which allows children to implement an emergency stop + * mechanism that can be triggered by an authorized account. + * + * This module is used through inheritance. It will make available the + * modifiers `whenNotPaused` and `whenPaused`, which can be applied to + * the functions of your contract. Note that they will not be pausable by + * simply including this module, only once the modifiers are put in place. + */ +abstract contract Pausable is Context { + bool private _paused; + + /** + * @dev Emitted when the pause is triggered by `account`. + */ + event Paused(address account); + + /** + * @dev Emitted when the pause is lifted by `account`. + */ + event Unpaused(address account); + + /** + * @dev The operation failed because the contract is paused. + */ + error EnforcedPause(); + + /** + * @dev The operation failed because the contract is not paused. + */ + error ExpectedPause(); + + /** + * @dev Initializes the contract in unpaused state. + */ + constructor() { + _paused = false; + } + + /** + * @dev Modifier to make a function callable only when the contract is not paused. + * + * Requirements: + * + * - The contract must not be paused. + */ + modifier whenNotPaused() { + _requireNotPaused(); + _; + } + + /** + * @dev Modifier to make a function callable only when the contract is paused. + * + * Requirements: + * + * - The contract must be paused. + */ + modifier whenPaused() { + _requirePaused(); + _; + } + + /** + * @dev Returns true if the contract is paused, and false otherwise. + */ + function paused() public view virtual returns (bool) { + return _paused; + } + + /** + * @dev Throws if the contract is paused. + */ + function _requireNotPaused() internal view virtual { + if (paused()) { + revert EnforcedPause(); + } + } + + /** + * @dev Throws if the contract is not paused. + */ + function _requirePaused() internal view virtual { + if (!paused()) { + revert ExpectedPause(); + } + } + + /** + * @dev Triggers stopped state. + * + * Requirements: + * + * - The contract must not be paused. + */ + function _pause() internal virtual whenNotPaused { + _paused = true; + emit Paused(_msgSender()); + } + + /** + * @dev Returns to normal state. + * + * Requirements: + * + * - The contract must be paused. + */ + function _unpause() internal virtual whenPaused { + _paused = false; + emit Unpaused(_msgSender()); + } +} diff --git a/packages/contracts-bedrock/src/universal/UsdcBridge.sol b/packages/contracts-bedrock/src/universal/UsdcBridge.sol index 6a50142b5b3e..d83482e6efb1 100644 --- a/packages/contracts-bedrock/src/universal/UsdcBridge.sol +++ b/packages/contracts-bedrock/src/universal/UsdcBridge.sol @@ -6,17 +6,18 @@ import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC16 import { Address } from "@openzeppelin/contracts/utils/Address.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { SafeCall } from "src/libraries/SafeCall.sol"; -import { IOptimismMintableERC20, ILegacyMintableERC20 } from "src/universal/IOptimismMintableERC20.sol"; import { CrossDomainMessenger } from "src/universal/CrossDomainMessenger.sol"; -import { OptimismMintableERC20 } from "src/universal/OptimismMintableERC20.sol"; import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import { Pausable } from "src/libraries/Pausable.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { IPartialUsdc } from "src/L1/IPartialUsdc.sol"; /// @custom:upgradeable /// @title UsdcBridge /// @notice UsdcBridge is a base contract for the L1 and L2 standard ERC20 bridges. It handles /// the core bridging logic, including escrowing tokens that are native to the local chain /// and minting/burning tokens that are native to the remote chain. -abstract contract UsdcBridge is Initializable { +abstract contract UsdcBridge is Initializable, Pausable, Ownable { using SafeERC20 for IERC20; /// @notice The L2 gas limit set when eth is depoisited using the receive() function. @@ -43,24 +44,16 @@ abstract contract UsdcBridge is Initializable { /// @custom:network-specific UsdcBridge public otherBridge; + /// @notice Address of the token on L1. + address public l1Usdc; + + /// @notice Address of the token on L2. + address public l2Usdc; + /// @notice Reserve extra slots (to a total of 50) in the storage layout for future upgrades. - /// A gap size of 45 was chosen here, so that the first slot used in a child contract + /// A gap size of 43 was chosen here, so that the first slot used in a child contract /// would be a multiple of 50. - uint256[45] private __gap; - - /// @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 Emitted when an ETH bridge is finalized on this 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 ETHBridgeFinalized(address indexed from, address indexed to, uint256 amount, bytes extraData); + uint256[43] private __gap; /// @notice Emitted when an ERC20 bridge is initiated to the other chain. /// @param localToken Address of the ERC20 on this chain. @@ -116,18 +109,38 @@ abstract contract UsdcBridge is Initializable { /// @param _otherBridge Contract for the other UsdcBridge contract. function __UsdcBridge_init( CrossDomainMessenger _messenger, - UsdcBridge _otherBridge + UsdcBridge _otherBridge, + address _l1Usdc, + address _l2Usdc, + address _owner ) internal onlyInitializing { messenger = _messenger; otherBridge = _otherBridge; + l1Usdc = _l1Usdc; + l2Usdc = _l2Usdc; + _transferOwnership(_owner); + } + + /// @notice Checks if the given token is the correct l1 token. + /// @param _token The token to check. + function _isL2Usdc(address _token) internal view returns (bool) { + return _token == l2Usdc; + } + + /// @notice Checks if the given token is the correct l2 token. + /// @param _token The token to check. + function _isL1Usdc(address _token) internal view returns (bool) { + return _token == l1Usdc; } /// @notice Allows EOAs to bridge ETH by sending directly to the bridge. /// Must be implemented by contracts that inherit. - receive() external payable virtual; + receive() external payable { + revert("Eth transfers not supported"); + } /// @notice Getter for messenger contract. /// Public getter is legacy and will be removed in the future. Use `messenger` instead. @@ -145,37 +158,18 @@ abstract contract UsdcBridge is Initializable { return otherBridge; } - /// @notice This function should return true if the contract is paused. - /// On L1 this function will check the SuperchainConfig for its paused status. - /// On L2 this function should be a no-op. - /// @return Whether or not the contract is paused. - function paused() public view virtual returns (bool) { - return false; - } - - /// @notice Sends ETH to the sender's address on the other chain. - /// @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 bridgeETH(uint32 _minGasLimit, bytes calldata _extraData) public payable onlyEOA { - _initiateBridgeETH(msg.sender, msg.sender, msg.value, _minGasLimit, _extraData); + /** + * @dev Function to pause contract. This calls the Pausable contract. + */ + function pause() external onlyOwner { + super._pause(); } - /// @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 - /// UsdcBridge 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 UsdcBridge 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) public payable { - _initiateBridgeETH(msg.sender, _to, msg.value, _minGasLimit, _extraData); + /** + * @dev Function to unpause contract. This calls the Pausable contract. + */ + function unpause() external onlyOwner { + super._unpause(); } /// @notice Sends ERC20 tokens to the sender's address on the other chain. Note that if the @@ -229,37 +223,6 @@ abstract contract UsdcBridge is Initializable { _initiateBridgeERC20(_localToken, _remoteToken, msg.sender, _to, _amount, _minGasLimit, _extraData); } - /// @notice Finalizes an ETH bridge on this chain. Can only be triggered by the other - /// UsdcBridge contract on the remote chain. - /// @param _from Address of the sender. - /// @param _to Address of the receiver. - /// @param _amount Amount of ETH being bridged. - /// @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 finalizeBridgeETH( - address _from, - address _to, - uint256 _amount, - bytes calldata _extraData - ) - public - payable - onlyOtherBridge - { - require(paused() == false, "UsdcBridge: paused"); - require(msg.value == _amount, "UsdcBridge: amount sent does not match amount required"); - require(_to != address(this), "UsdcBridge: cannot send to self"); - require(_to != address(messenger), "UsdcBridge: cannot send to messenger"); - - // Emit the correct events. By default this will be _amount, but child - // contracts may override this function in order to emit legacy events as well. - _emitETHBridgeFinalized(_from, _to, _amount, _extraData); - - bool success = SafeCall.call(_to, gasleft(), _amount, hex""); - require(success, "UsdcBridge: ETH transfer failed"); - } - /// @notice Finalizes an ERC20 bridge on this chain. Can only be triggered by the other /// UsdcBridge contract on the remote chain. /// @param _localToken Address of the ERC20 on this chain. @@ -281,17 +244,15 @@ abstract contract UsdcBridge is Initializable { public onlyOtherBridge { - require(paused() == false, "UsdcBridge: paused"); - if (_isOptimismMintableERC20(_localToken)) { - require( - _isCorrectTokenPair(_localToken, _remoteToken), - "UsdcBridge: wrong remote token for Optimism Mintable ERC20 local token" - ); - - OptimismMintableERC20(_localToken).mint(_to, _amount); - } else { + if (_isL2Usdc(_localToken) && _isL1Usdc(_remoteToken)) { + // L1 --> L2 + IPartialUsdc(_localToken).mint(_to, _amount); + } else if (_isL1Usdc(_localToken) && _isL2Usdc(_remoteToken)) { + // L2 --> L1 deposits[_localToken][_remoteToken] = deposits[_localToken][_remoteToken] - _amount; IERC20(_localToken).safeTransfer(_to, _amount); + } else { + revert("Invalid token pair"); } // Emit the correct events. By default this will be ERC20BridgeFinalized, but child @@ -299,36 +260,6 @@ abstract contract UsdcBridge is Initializable { _emitERC20BridgeFinalized(_localToken, _remoteToken, _from, _to, _amount, _extraData); } - /// @notice Initiates a bridge of ETH through the CrossDomainMessenger. - /// @param _from Address of the sender. - /// @param _to Address of the receiver. - /// @param _amount Amount of ETH being bridged. - /// @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 _initiateBridgeETH( - address _from, - address _to, - uint256 _amount, - uint32 _minGasLimit, - bytes memory _extraData - ) - internal - { - require(msg.value == _amount, "UsdcBridge: bridging ETH must include sufficient ETH value"); - - // Emit the correct events. By default this will be _amount, but child - // contracts may override this function in order to emit legacy events as well. - _emitETHBridgeInitiated(_from, _to, _amount, _extraData); - - messenger.sendMessage{ value: _amount }({ - _target: address(otherBridge), - _message: abi.encodeWithSelector(this.finalizeBridgeETH.selector, _from, _to, _amount, _extraData), - _minGasLimit: _minGasLimit - }); - } - /// @notice Sends ERC20 tokens to a receiver's address on the other chain. /// @param _localToken Address of the ERC20 on this chain. /// @param _remoteToken Address of the corresponding token on the remote chain. @@ -348,17 +279,19 @@ abstract contract UsdcBridge is Initializable { bytes memory _extraData ) internal + whenNotPaused { - if (_isOptimismMintableERC20(_localToken)) { - require( - _isCorrectTokenPair(_localToken, _remoteToken), - "UsdcBridge: wrong remote token for Optimism Mintable ERC20 local token" - ); - - OptimismMintableERC20(_localToken).burn(_from, _amount); - } else { + if (_isL2Usdc(_localToken) && _isL1Usdc(_remoteToken)) { + // L2 --> L1 + // usdc has no burnFrom, so first transfer to the contract and then burn. + IERC20(_localToken).safeTransferFrom(_from, address(this), _amount); + IPartialUsdc(_localToken).burn(_amount); + } else if (_isL1Usdc(_localToken) && _isL2Usdc(_remoteToken)) { + // L1 --> L2 IERC20(_localToken).safeTransferFrom(_from, address(this), _amount); deposits[_localToken][_remoteToken] = deposits[_localToken][_remoteToken] + _amount; + } else { + revert("Invalid token pair"); } // Emit the correct events. By default this will be ERC20BridgeInitiated, but child @@ -383,65 +316,6 @@ abstract contract UsdcBridge is Initializable { }); } - /// @notice Checks if a given address is an OptimismMintableERC20. Not perfect, but good enough. - /// Just the way we like it. - /// @param _token Address of the token to check. - /// @return True if the token is an OptimismMintableERC20. - function _isOptimismMintableERC20(address _token) internal view returns (bool) { - return ERC165Checker.supportsInterface(_token, type(ILegacyMintableERC20).interfaceId) - || ERC165Checker.supportsInterface(_token, type(IOptimismMintableERC20).interfaceId); - } - - /// @notice Checks if the "other token" is the correct pair token for the OptimismMintableERC20. - /// Calls can be saved in the future by combining this logic with - /// `_isOptimismMintableERC20`. - /// @param _mintableToken OptimismMintableERC20 to check against. - /// @param _otherToken Pair token to check. - /// @return True if the other token is the correct pair token for the OptimismMintableERC20. - function _isCorrectTokenPair(address _mintableToken, address _otherToken) internal view returns (bool) { - if (ERC165Checker.supportsInterface(_mintableToken, type(ILegacyMintableERC20).interfaceId)) { - return _otherToken == ILegacyMintableERC20(_mintableToken).l1Token(); - } else { - return _otherToken == IOptimismMintableERC20(_mintableToken).remoteToken(); - } - } - - /// @notice Emits the ETHBridgeInitiated event and if necessary the appropriate legacy event - /// when an ETH bridge is finalized on this 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. - function _emitETHBridgeInitiated( - address _from, - address _to, - uint256 _amount, - bytes memory _extraData - ) - internal - virtual - { - emit ETHBridgeInitiated(_from, _to, _amount, _extraData); - } - - /// @notice Emits the ETHBridgeFinalized and if necessary the appropriate legacy event when an - /// ETH bridge is finalized on this 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. - function _emitETHBridgeFinalized( - address _from, - address _to, - uint256 _amount, - bytes memory _extraData - ) - internal - virtual - { - emit ETHBridgeFinalized(_from, _to, _amount, _extraData); - } - /// @notice Emits the ERC20BridgeInitiated event and if necessary the appropriate legacy /// event when an ERC20 bridge is initiated to the other chain. /// @param _localToken Address of the ERC20 on this chain.