diff --git a/packages/contracts-bedrock/scripts/DeployConfig.s.sol b/packages/contracts-bedrock/scripts/DeployConfig.s.sol index 25869e97f08..4b273141d56 100644 --- a/packages/contracts-bedrock/scripts/DeployConfig.s.sol +++ b/packages/contracts-bedrock/scripts/DeployConfig.s.sol @@ -91,6 +91,9 @@ contract DeployConfig is Script { bool public useInterop; + address public systemConfigProxy; + address public l1CrossDomainMessengerProxy; + function read(string memory _path) public { console.log("DeployConfig: reading file %s", _path); try vm.readFile(_path) returns (string memory data) { @@ -174,6 +177,9 @@ contract DeployConfig is Script { customGasTokenAddress = _readOr(_json, "$.customGasTokenAddress", address(0)); useInterop = _readOr(_json, "$.useInterop", false); + + l1CrossDomainMessengerProxy = stdJson.readAddress(_json, "$.l1CrossDomainMessengerProxy"); + systemConfigProxy = stdJson.readAddress(_json, "$.systemConfigProxy"); } function fork() public view returns (Fork fork_) { diff --git a/packages/contracts-bedrock/scripts/DeployDedicatedBridge.s.sol b/packages/contracts-bedrock/scripts/DeployDedicatedBridge.s.sol new file mode 100644 index 00000000000..2189a9c72d6 --- /dev/null +++ b/packages/contracts-bedrock/scripts/DeployDedicatedBridge.s.sol @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { VmSafe } from "forge-std/Vm.sol"; +import { Script } from "forge-std/Script.sol"; + +import { console2 as console } from "forge-std/console2.sol"; +import { stdJson } from "forge-std/StdJson.sol"; + +import { GnosisSafe as Safe } from "safe-contracts/GnosisSafe.sol"; + +import { Deploy } from "scripts/Deploy.s.sol"; + +import { ProxyAdmin } from "src/universal/ProxyAdmin.sol"; +import { DedicatedBridge } from "src/universal/DedicatedBridge.sol"; +import { L1DedicatedBridge } from "src/L1/L1DedicatedBridge.sol"; +import { L1DedicatedUSDCBridge } from "src/L1/L1DedicatedUSDCBridge.sol"; +import { L2DedicatedBridge } from "src/L2/L2DedicatedBridge.sol"; +import { L1ChugSplashProxy } from "src/legacy/L1ChugSplashProxy.sol"; +import { L1CrossDomainMessenger } from "src/L1/L1CrossDomainMessenger.sol"; +import { SuperchainConfig } from "src/L1/SuperchainConfig.sol"; +import { SystemConfig } from "src/L1/SystemConfig.sol"; +import { Proxy } from "src/universal/Proxy.sol"; + + +import "src/dispute/lib/Types.sol"; +import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; + +/// @title DeployDedicatedBridge +/// @notice Script used to deploy a dedicated bridge. +contract DeployDedicatedBridge is Deploy { + using stdJson for string; + + + //////////////////////////////////////////////////////////////// + // High Level Deployment Functions // + //////////////////////////////////////////////////////////////// + + /// @notice Deploy L1 dedicated bridge + function runL1DedicatedBridgeDeployment(address _otherBridge, address _l1Token, address _l2Token) public { + console.log("Deploying L1 Dedicated bridge"); + + deploySafe("SystemOwnerSafe"); + + // Deploy a new ProxyAdmin and AddressManager + // This proxy will be used on the SuperchainConfig and ProtocolVersions contracts, as well as the contracts + // in the OP Chain system. + deployAddressManager(); + deployProxyAdmin(); + transferProxyAdminOwnership(); + + // Deploy the SuperchainConfigProxy + deployERC1967Proxy("SuperchainConfigProxy"); + deploySuperchainConfig(); + initializeSuperchainConfig(); + + deployL1DedicatedBridgeProxy(); + transferAddressManagerOwnership(); // to the ProxyAdmin + deployL1DedicatedBridge(); + initializeL1DedicatedBridge(_otherBridge, _l1Token, _l2Token); + console.log("Done!"); + } + + //////////////////////////////////////////////////////////////// + // Proxy Deployment Functions // + //////////////////////////////////////////////////////////////// + + /// @notice Deploy the L1DedicatedBridgeProxy using a ChugSplashProxy + function deployL1DedicatedBridgeProxy() public broadcast returns (address addr_) { + console.log("Deploying proxy for L1DedicatedUSDCBridge"); + address proxyAdmin = mustGetAddress("ProxyAdmin"); + L1ChugSplashProxy proxy = new L1ChugSplashProxy(proxyAdmin); + + require(EIP1967Helper.getAdmin(address(proxy)) == proxyAdmin); + + save("L1DedicatedBridgeProxy", address(proxy)); + console.log("L1DedicatedBridgeProxy deployed at %s", address(proxy)); + addr_ = address(proxy); + } + + /// @notice Deploy the L2DedicatedBridgeProxy using a Proxy + function deployL2DedicatedBridgeProxy() public returns (address addr_) { + console.log("Deploying proxy for L2DedicatedBridge"); + addr_ = deployERC1967ProxyWithOwner("L2DedicatedBridgeProxy", msg.sender); + } + + /// @notice Deploy the L1DedicatedUSDCBridge + function deployL1DedicatedBridge() public broadcast returns (address addr_) { + console.log("Deploying L1DedicatedUSDCBridge implementation"); + + L1DedicatedUSDCBridge bridge = new L1DedicatedUSDCBridge{ salt: _implSalt() }(); + + save("L1DedicatedUSDCBridge", address(bridge)); + console.log("L1DedicatedUSDCBridge deployed at %s", address(bridge)); + + addr_ = address(bridge); + } + + /// @notice Deploy the L2DedicatedBridge + function deployL2DedicatedBridge() public broadcast returns (address addr_) { + console.log("Deploying L2DedicatedBridge implementation"); + + L2DedicatedBridge bridge = new L2DedicatedBridge{ salt: _implSalt() }(); + + save("L2DedicatedBridge", address(bridge)); + console.log("L2DedicatedBridge deployed at %s", address(bridge)); + + addr_ = address(bridge); + } + + //////////////////////////////////////////////////////////////// + // Initialize Functions // + //////////////////////////////////////////////////////////////// + + /// @notice Initialize the L1DedicatedUSDCBridge + function initializeL1DedicatedBridge(address _otherBridge, address _l1Token, address _l2Token) public broadcast { + console.log("Upgrading and initializing L1DedicatedUSDCBridge proxy"); + ProxyAdmin proxyAdmin = ProxyAdmin(mustGetAddress("ProxyAdmin")); + address l1DedicatedBridgeProxy = mustGetAddress("L1DedicatedBridgeProxy"); + + uint256 proxyType = uint256(proxyAdmin.proxyType(l1DedicatedBridgeProxy)); + Safe safe = Safe(mustGetAddress("SystemOwnerSafe")); + if (proxyType != uint256(ProxyAdmin.ProxyType.CHUGSPLASH)) { + _callViaSafe({ + _safe: safe, + _target: address(proxyAdmin), + _data: abi.encodeCall(ProxyAdmin.setProxyType, (l1DedicatedBridgeProxy, ProxyAdmin.ProxyType.CHUGSPLASH)) + }); + } + require(uint256(proxyAdmin.proxyType(l1DedicatedBridgeProxy)) == uint256(ProxyAdmin.ProxyType.CHUGSPLASH)); + + _upgradeAndCallViaSafe({ + _proxy: payable(l1DedicatedBridgeProxy), + _implementation: mustGetAddress("L1DedicatedUSDCBridge"), + _innerCallData: abi.encodeCall( + L1DedicatedBridge.initialize, + ( + L1CrossDomainMessenger(cfg.l1CrossDomainMessengerProxy()), + SuperchainConfig(mustGetAddress("SuperchainConfigProxy")), + SystemConfig(cfg.systemConfigProxy()), + _otherBridge, + _l1Token, + _l2Token + ) + ) + }); + + string memory version = L1DedicatedUSDCBridge(payable(l1DedicatedBridgeProxy)).version(); + console.log("L1DedicatedUSDCBridge version: %s", version); + } + + /// @notice Initialize the L2DedicatedBridge + function initializeL2DedicatedBridge( + address _proxy, + address _implementation, + address _otherBridge, + address _l1Token, + address _l2Token + ) + public + broadcast + { + bytes memory _data = abi.encodeCall(L2DedicatedBridge.initialize, (_otherBridge, _l1Token, _l2Token)); + Proxy(payable(_proxy)).upgradeToAndCall(_implementation, _data); + } +} diff --git a/packages/contracts-bedrock/scripts/usdc_deploy.sh b/packages/contracts-bedrock/scripts/usdc_deploy.sh new file mode 100755 index 00000000000..d33f1040251 --- /dev/null +++ b/packages/contracts-bedrock/scripts/usdc_deploy.sh @@ -0,0 +1,46 @@ +DEPLOY_CONFIG_PATH="deploy-config/usdc-sepolia-devnet.json" +USDC_L1_ADDRESS=0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238 +USDC_L2_ADDRESS=0x85977F663949E5AC21334F2219E2B4048EF74A80 + +# source .env for PRIVATE_KEY +source ../.env + +export IMPL_SALT="avocado magnetico salato" + +L1_VERIFIER_URL=https://eth-sepolia.blockscout.com/api\? +L2_VERIFIER_URL=https://sepolia-blockscout.lisk.com/api\? + +# L1_RPC_URL=wss://ethereum-sepolia-rpc.publicnode.com +# L2_RPC_URL=https://rpc.sepolia-api.lisk.com + +L1_RPC_URL=http://localhost:8545 +L2_RPC_URL=http://localhost:8546 + +# outputs L2DedicatedBridgeProxy +forge script -vvv DeployDedicatedBridge.s.sol:DeployDedicatedBridge --sig 'deployL2DedicatedBridgeProxy()' --rpc-url "$L2_RPC_URL" --broadcast --private-key $PRIVATE_KEY +L2DedicatedBridgeProxy=$(cat ../deployments/4202-deploy.json | grep -Eo '"L2DedicatedBridgeProxy": "(\d*?,|.*?[^\\])"' | awk -F'"' '{print $4}' ) + +# outputs USDC_L1_BRIDGE_PROXY and USDC_L1_BRIDGE_DEPLOYMENT +forge script -vvv DeployDedicatedBridge.s.sol:DeployDedicatedBridge $L2DedicatedBridgeProxy $USDC_L1_ADDRESS $USDC_L2_ADDRESS --sig 'runL1DedicatedBridgeDeployment(address,address,address)' --rpc-url "$L1_RPC_URL" --broadcast --private-key $PRIVATE_KEY +L1DedicatedBridgeProxy=$(cat ../deployments/11155111-deploy.json | grep -Eo '"L1DedicatedBridgeProxy": "(\d*?,|.*?[^\\])"' | awk -F'"' '{print $4}' ) +L1DedicatedUSDCBridge=$(cat ../deployments/11155111-deploy.json | grep -Eo '"L1DedicatedUSDCBridge": "(\d*?,|.*?[^\\])"' | awk -F'"' '{print $4}' ) + + +# outputs USDC_L2_BRIDGE_DEPLOYMENT +forge script -vvv DeployDedicatedBridge.s.sol:DeployDedicatedBridge --sig 'deployL2DedicatedBridge()' --rpc-url "$L2_RPC_URL" --broadcast --private-key $PRIVATE_KEY +L2DedicatedBridge=$(cat ../deployments/4202-deploy.json | grep -Eo '"L2DedicatedBridge": "(\d*?,|.*?[^\\])"' | awk -F'"' '{print $4}' ) + +forge script -vvv DeployDedicatedBridge.s.sol:DeployDedicatedBridge $L2DedicatedBridgeProxy $L2DedicatedBridge $L1DedicatedBridgeProxy $USDC_L1_ADDRESS $USDC_L2_ADDRESS --sig 'initializeL2DedicatedBridge(address,address,address,address,address)' --rpc-url "$L2_RPC_URL" --broadcast --private-key $PRIVATE_KEY + + +# forge verify-contract $USDC_L1_BRIDGE_DEPLOYMENT src/L1/L1DedicatedUSDCBridge.sol:L1DedicatedUSDCBridge --compiler-version 0.8.15 --rpc-url "$L1_RPC_URL" --verifier blockscout --verifier-url $L1_VERIFIER_URL +# forge verify-contract $USDC_L2_BRIDGE_DEPLOYMENT src/L2/L2DedicatedBridge.sol:L2DedicatedBridge --compiler-version 0.8.15 --rpc-url "$L2_RPC_URL" --verifier blockscout --verifier-url $L2_VERIFIER_URL + + + + +echo "All done!" +echo "L1DedicatedBridgeProxy", $L1DedicatedBridgeProxy +echo "L1DedicatedUSDCBridge", $L1DedicatedUSDCBridge +echo "L2DedicatedBridgeProxy", $L2DedicatedBridgeProxy +echo "L2DedicatedBridge", $L2DedicatedBridge diff --git a/packages/contracts-bedrock/src/L1/L1DedicatedBridge.sol b/packages/contracts-bedrock/src/L1/L1DedicatedBridge.sol new file mode 100644 index 00000000000..93c864c9295 --- /dev/null +++ b/packages/contracts-bedrock/src/L1/L1DedicatedBridge.sol @@ -0,0 +1,300 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { DedicatedBridge } from "src/universal/DedicatedBridge.sol"; +import { ISemver } from "src/universal/ISemver.sol"; +import { CrossDomainMessenger } from "src/universal/CrossDomainMessenger.sol"; +import { SuperchainConfig } from "src/L1/SuperchainConfig.sol"; +import { OptimismPortal } from "src/L1/OptimismPortal.sol"; +import { SystemConfig } from "src/L1/SystemConfig.sol"; + +/// @custom:proxied +/// @title L1DedicatedBridge +/// @notice The L1DedicatedBridge is responsible for transfering a single ERC20 token 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. +/// This contract is based on the L1StandardBridge contract. +contract L1DedicatedBridge is DedicatedBridge, ISemver { + /// @custom:legacy + /// @notice Emitted whenever an ERC20 deposit is initiated. + /// @param l1Token Address of the token on L1. + /// @param l2Token Address of the corresponding token on L2. + /// @param from Address of the depositor. + /// @param to Address of the recipient on L2. + /// @param amount Amount of the ERC20 deposited. + /// @param extraData Extra data attached to the deposit. + event ERC20DepositInitiated( + address indexed l1Token, + address indexed l2Token, + address indexed from, + address to, + uint256 amount, + bytes extraData + ); + + /// @custom:legacy + /// @notice Emitted whenever an ERC20 withdrawal is finalized. + /// @param l1Token Address of the token on L1. + /// @param l2Token Address of the corresponding token on L2. + /// @param from Address of the withdrawer. + /// @param to Address of the recipient on L1. + /// @param amount Amount of the ERC20 withdrawn. + /// @param extraData Extra data attached to the withdrawal. + event ERC20WithdrawalFinalized( + address indexed l1Token, + address indexed l2Token, + address indexed from, + address to, + uint256 amount, + bytes extraData + ); + + /// @notice Semantic version. + /// @custom:semver 2.2.0 + string public constant version = "2.2.0"; + + /// @notice Address of the SuperchainConfig contract. + SuperchainConfig public superchainConfig; + + /// @notice Address of the SystemConfig contract. + SystemConfig public systemConfig; + + /// @notice The address of L1 token. + // solhint-disable-next-line var-name-mixedcase + address public l1Token; + + /// @notice The address of L2 token. + address public l2Token; + + /// @notice Constructs the L1DedicatedBridge contract. + constructor() DedicatedBridge() { + initialize({ + _messenger: CrossDomainMessenger(address(0)), + _superchainConfig: SuperchainConfig(address(0)), + _systemConfig: SystemConfig(address(0)), + _otherBridge: address(0), + _l1Token: address(0), + _l2Token: address(0) + }); + } + + /// @notice Initializer. + /// @param _messenger Contract for the CrossDomainMessenger on this network. + /// @param _superchainConfig Contract for the SuperchainConfig on this network. + /// @param _otherBridge Contract for the other DedicatedBridge contract. + function initialize( + CrossDomainMessenger _messenger, + SuperchainConfig _superchainConfig, + SystemConfig _systemConfig, + address _otherBridge, + address _l1Token, + address _l2Token + ) + public + initializer + { + superchainConfig = _superchainConfig; + systemConfig = _systemConfig; + __StandardBridge_init({ _messenger: _messenger, _otherBridge: DedicatedBridge(payable(_otherBridge)) }); + l1Token = _l1Token; + l2Token = _l2Token; + } + + /// @inheritdoc DedicatedBridge + function paused() public view override returns (bool) { + return superchainConfig.paused(); + } + + /// @inheritdoc DedicatedBridge + function gasPayingToken() internal view override returns (address addr_, uint8 decimals_) { + (addr_, decimals_) = systemConfig.gasPayingToken(); + } + + /// @custom:legacy + /// @notice Deposits some amount of ERC20 tokens into the sender's account on L2. + /// @param _l1Token Address of the L1 token being deposited. + /// @param _l2Token Address of the corresponding token on L2. + /// @param _amount Amount of the ERC20 to deposit. + /// @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 depositERC20( + address _l1Token, + address _l2Token, + uint256 _amount, + uint32 _minGasLimit, + bytes calldata _extraData + ) + external + virtual + { + _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, msg.sender, _amount, _minGasLimit, _extraData); + } + + /// @custom:legacy + /// @notice Deposits some amount of ERC20 tokens into a target account on L2. + /// @param _l1Token Address of the L1 token being deposited. + /// @param _l2Token Address of the corresponding token on L2. + /// @param _to Address of the recipient on L2. + /// @param _amount Amount of the ERC20 to deposit. + /// @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 depositERC20To( + address _l1Token, + address _l2Token, + address _to, + uint256 _amount, + uint32 _minGasLimit, + bytes calldata _extraData + ) + external + virtual + { + _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, _to, _amount, _minGasLimit, _extraData); + } + + /// @custom:legacy + /// @notice Finalizes a withdrawal of ERC20 tokens from L2. + /// @param _l1Token Address of the token on L1. + /// @param _l2Token Address of the corresponding token on L2. + /// @param _from Address of the withdrawer on L2. + /// @param _to Address of the recipient on L1. + /// @param _amount Amount of the ERC20 to withdraw. + /// @param _extraData Optional data forwarded from L2. + function finalizeERC20Withdrawal( + address _l1Token, + address _l2Token, + address _from, + address _to, + uint256 _amount, + bytes calldata _extraData + ) + external + { + finalizeBridgeERC20(_l1Token, _l2Token, _from, _to, _amount, _extraData); + // update total supply + // warnning check there is no reentrancy + } + + /// @notice Deposits some amount of l1Token tokens into the sender's account on L2. + /// A convenience function which does not require the extra token parameters. + /// @param _amount Amount of the ERC20 to deposit. + /// @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 depositL1Token(uint256 _amount, uint32 _minGasLimit, bytes calldata _extraData) external virtual { + _initiateERC20Deposit(l1Token, l2Token, msg.sender, msg.sender, _amount, _minGasLimit, _extraData); + } + + /// @notice Deposits some amount of l1Token tokens into a target account on L2. + /// A convenience function which does not require the extra token parameters. + /// @param _to Address of the recipient on L2. + /// @param _amount Amount of the ERC20 to deposit. + /// @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 depositL1TokenTo( + address _to, + uint256 _amount, + uint32 _minGasLimit, + bytes calldata _extraData + ) + external + virtual + { + _initiateERC20Deposit(l1Token, l2Token, msg.sender, _to, _amount, _minGasLimit, _extraData); + } + + /// @notice Finalizes a withdrawal of l2Token tokens from L2. + /// A convenience function which does not require the extra token parameters. + /// @param _from Address of the withdrawer on L2. + /// @param _to Address of the recipient on L1. + /// @param _amount Amount of the ERC20 to withdraw. + /// @param _extraData Optional data forwarded from L2. + function finalizeL2TokenWithdrawal( + address _from, + address _to, + uint256 _amount, + bytes calldata _extraData + ) + external + { + finalizeBridgeERC20(l1Token, l2Token, _from, _to, _amount, _extraData); + } + + /// @custom:legacy + /// @notice Retrieves the access of the corresponding L2 bridge contract. + /// @return Address of the corresponding L2 bridge contract. + function l2TokenBridge() external view returns (address) { + return address(otherBridge); + } + + /// @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. + /// @param _from Address of the sender on L1. + /// @param _to Address of the recipient on L2. + /// @param _amount Amount of the ERC20 to deposit. + /// @param _minGasLimit Minimum gas limit for the deposit message on L2. + /// @param _extraData Optional data to forward to L2. + function _initiateERC20Deposit( + address _l1Token, + address _l2Token, + address _from, + address _to, + uint256 _amount, + uint32 _minGasLimit, + bytes memory _extraData + ) + internal + { + _initiateBridgeERC20(_l1Token, _l2Token, _from, _to, _amount, _minGasLimit, _extraData); + } + + /// @inheritdoc DedicatedBridge + function _isCorrectTokenPair(address _mintableToken, address _otherToken) internal view override returns (bool) { + return (_mintableToken == l1Token && _otherToken == l2Token); + } + + /// @inheritdoc DedicatedBridge + /// @notice Emits the legacy ERC20WithdrawalFinalized event followed by the ERC20BridgeFinalized + /// event. This is necessary for backwards compatibility with the legacy bridge. + function _emitERC20BridgeInitiated( + address _localToken, + address _remoteToken, + address _from, + address _to, + uint256 _amount, + bytes memory _extraData + ) + internal + override + { + emit ERC20DepositInitiated(_localToken, _remoteToken, _from, _to, _amount, _extraData); + super._emitERC20BridgeInitiated(_localToken, _remoteToken, _from, _to, _amount, _extraData); + } + + /// @inheritdoc DedicatedBridge + /// @notice Emits the legacy ERC20WithdrawalFinalized event followed by the ERC20BridgeFinalized + /// event. This is necessary for backwards compatibility with the legacy bridge. + function _emitERC20BridgeFinalized( + address _localToken, + address _remoteToken, + address _from, + address _to, + uint256 _amount, + bytes memory _extraData + ) + internal + override + { + emit ERC20WithdrawalFinalized(_localToken, _remoteToken, _from, _to, _amount, _extraData); + super._emitERC20BridgeFinalized(_localToken, _remoteToken, _from, _to, _amount, _extraData); + } +} diff --git a/packages/contracts-bedrock/src/L1/L1DedicatedUSDCBridge.sol b/packages/contracts-bedrock/src/L1/L1DedicatedUSDCBridge.sol new file mode 100644 index 00000000000..e7b1d09bc55 --- /dev/null +++ b/packages/contracts-bedrock/src/L1/L1DedicatedUSDCBridge.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import { L1DedicatedBridge } from "src/L1/L1DedicatedBridge.sol"; + +/// @custom:proxied +/// @title L1DedicatedUSDCBridge +/// @notice The L1DedicatedUSDCBridge is responsible for transfering the USDC token between L1 and +/// L2. +contract L1DedicatedUSDCBridge is L1DedicatedBridge { + /// @notice Burns all locked USDC if the bridge is already paused + function burnAllLockedUSDC() external { + require(paused() == true, "Bridge should be paused before burning all locked USDC"); + require(msg.sender == superchainConfig.guardian(), "SuperchainConfig: only guardian can burn all USDC"); + deposits[l1Token][l2Token] = 0; + // IERC20(l1USDC).burn(_balance); // check if this needs to be done + } +} diff --git a/packages/contracts-bedrock/src/L2/L2DedicatedBridge.sol b/packages/contracts-bedrock/src/L2/L2DedicatedBridge.sol new file mode 100644 index 00000000000..287ad387ddf --- /dev/null +++ b/packages/contracts-bedrock/src/L2/L2DedicatedBridge.sol @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import { Predeploys } from "src/libraries/Predeploys.sol"; +import { DedicatedBridge } from "src/universal/DedicatedBridge.sol"; +import { ISemver } from "src/universal/ISemver.sol"; +import { CrossDomainMessenger } from "src/universal/CrossDomainMessenger.sol"; +import { L1Block } from "src/L2/L1Block.sol"; + +/// @custom:proxied +/// @title L2DedicatedBridge +/// @notice The L2DedicatedBridge is responsible for transfering a single 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. +/// This contract is based on the L2StandardBridge contract. +contract L2DedicatedBridge is DedicatedBridge, ISemver { + /// @custom:legacy + /// @notice Emitted whenever a withdrawal from L2 to L1 is initiated. + /// @param l1Token Address of the token on L1. + /// @param l2Token Address of the corresponding token on L2. + /// @param from Address of the withdrawer. + /// @param to Address of the recipient on L1. + /// @param amount Amount of the ERC20 withdrawn. + /// @param extraData Extra data attached to the withdrawal. + event WithdrawalInitiated( + address indexed l1Token, + address indexed l2Token, + address indexed from, + address to, + uint256 amount, + bytes extraData + ); + + /// @custom:legacy + /// @notice Emitted whenever an ERC20 deposit is finalized. + /// @param l1Token Address of the token on L1. + /// @param l2Token Address of the corresponding token on L2. + /// @param from Address of the depositor. + /// @param to Address of the recipient on L2. + /// @param amount Amount of the ERC20 deposited. + /// @param extraData Extra data attached to the deposit. + event DepositFinalized( + address indexed l1Token, + address indexed l2Token, + address indexed from, + address to, + uint256 amount, + bytes extraData + ); + + /// @custom:semver 1.10.0 + string public constant version = "1.10.0"; + + /// @notice The address of L1 token address. + // solhint-disable-next-line var-name-mixedcase + address public l1Token; + + /// @notice The address of L2 token address. + address public l2Token; + + /// @notice Constructs the L2DedicatedBridge contract. + constructor() DedicatedBridge() { + initialize({ _otherBridge: address(0), _l1Token: address(0), _l2Token: address(0) }); + } + + /// @notice Initializer. + /// @param _otherBridge Contract for the corresponding bridge on the other chain. + function initialize(address _otherBridge, address _l1Token, address _l2Token) public initializer { + __StandardBridge_init({ + _messenger: CrossDomainMessenger(Predeploys.L2_CROSS_DOMAIN_MESSENGER), + _otherBridge: DedicatedBridge(payable(_otherBridge)) + }); + l1Token = _l1Token; + l2Token = _l2Token; + } + + /// @inheritdoc DedicatedBridge + function gasPayingToken() internal view override returns (address addr_, uint8 decimals_) { + (addr_, decimals_) = L1Block(Predeploys.L1_BLOCK_ATTRIBUTES).gasPayingToken(); + } + + /// @custom:legacy + /// @notice Initiates a withdrawal from L2 to L1. + /// Subject to be deprecated in the future. + /// @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. + /// @param _extraData Extra data attached to the withdrawal. + function withdraw( + address _l2Token, + uint256 _amount, + uint32 _minGasLimit, + bytes calldata _extraData + ) + external + payable + virtual + onlyEOA + { + require(isCustomGasToken() == false, "L2DedicatedBridge: not supported with custom gas token"); + _initiateWithdrawal(_l2Token, msg.sender, msg.sender, _amount, _minGasLimit, _extraData); + } + + /// @custom:legacy + /// @notice Initiates a withdrawal from L2 to L1 to a target account on L1. + /// Subject to be deprecated in the future. + /// @param _l2Token Address of the L2 token to withdraw. + /// @param _to Recipient account on L1. + /// @param _amount Amount of the L2 token to withdraw. + /// @param _minGasLimit Minimum gas limit to use for the transaction. + /// @param _extraData Extra data attached to the withdrawal. + function withdrawTo( + address _l2Token, + address _to, + uint256 _amount, + uint32 _minGasLimit, + bytes calldata _extraData + ) + external + payable + virtual + { + require(isCustomGasToken() == false, "L2DedicatedBridge: not supported with custom gas token"); + _initiateWithdrawal(_l2Token, msg.sender, _to, _amount, _minGasLimit, _extraData); + } + + /// @custom:legacy + /// @notice Retrieves the access of the corresponding L1 bridge contract. + /// @return Address of the corresponding L1 bridge contract. + function l1TokenBridge() external view returns (address) { + return address(otherBridge); + } + + /// @custom:legacy + /// @notice Internal function to initiate a withdrawal from L2 to L1 to a target account on L1. + /// @param _l2Token Address of the L2 token to withdraw. + /// @param _from Address of the withdrawer. + /// @param _to Recipient account on L1. + /// @param _amount Amount of the L2 token to withdraw. + /// @param _minGasLimit Minimum gas limit to use for the transaction. + /// @param _extraData Extra data attached to the withdrawal. + function _initiateWithdrawal( + address _l2Token, + address _from, + address _to, + uint256 _amount, + uint32 _minGasLimit, + bytes memory _extraData + ) + internal + { + _initiateBridgeERC20(_l2Token, l1Token, _from, _to, _amount, _minGasLimit, _extraData); + } + + /// @inheritdoc DedicatedBridge + function _isCorrectTokenPair(address _mintableToken, address _otherToken) internal view override returns (bool) { + return (_mintableToken == l2Token && _otherToken == l1Token); + } + + /// @notice Emits the legacy WithdrawalInitiated event followed by the ERC20BridgeInitiated + /// event. This is necessary for backwards compatibility with the legacy bridge. + /// @inheritdoc DedicatedBridge + function _emitERC20BridgeInitiated( + address _localToken, + address _remoteToken, + address _from, + address _to, + uint256 _amount, + bytes memory _extraData + ) + internal + override + { + emit WithdrawalInitiated(_remoteToken, _localToken, _from, _to, _amount, _extraData); + super._emitERC20BridgeInitiated(_localToken, _remoteToken, _from, _to, _amount, _extraData); + } + + /// @notice Emits the legacy DepositFinalized event followed by the ERC20BridgeFinalized event. + /// This is necessary for backwards compatibility with the legacy bridge. + /// @inheritdoc DedicatedBridge + function _emitERC20BridgeFinalized( + address _localToken, + address _remoteToken, + address _from, + address _to, + uint256 _amount, + bytes memory _extraData + ) + internal + override + { + emit DepositFinalized(_remoteToken, _localToken, _from, _to, _amount, _extraData); + super._emitERC20BridgeFinalized(_localToken, _remoteToken, _from, _to, _amount, _extraData); + } +} diff --git a/packages/contracts-bedrock/src/universal/DedicatedBridge.sol b/packages/contracts-bedrock/src/universal/DedicatedBridge.sol new file mode 100644 index 00000000000..9999e59ff40 --- /dev/null +++ b/packages/contracts-bedrock/src/universal/DedicatedBridge.sol @@ -0,0 +1,320 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +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 { CrossDomainMessenger } from "src/universal/CrossDomainMessenger.sol"; +import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import { Constants } from "src/libraries/Constants.sol"; + +/// @custom:upgradeable +/// @title DedicatedBridge +/// @notice DedicatedBridge is a base contract for the L1 and L2 dedicated 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 DedicatedBridge is Initializable { + using SafeERC20 for IERC20; + + /// @notice The L2 gas limit set when eth is depoisited using the receive() function. + uint32 internal constant RECEIVE_DEFAULT_GAS_LIMIT = 200_000; + + /// @notice Mapping that stores deposits for a given pair of local and remote tokens. + mapping(address => mapping(address => uint256)) public deposits; + + /// @notice Messenger contract on this domain. + /// @custom:network-specific + CrossDomainMessenger public messenger; + + /// @notice Corresponding bridge on the other domain. + /// @custom:network-specific + DedicatedBridge public otherBridge; + + /// @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 + /// would be a multiple of 50. + uint256[45] private __gap; + + /// @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 ERC20 bridge is finalized on this 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 ERC20BridgeFinalized( + address indexed localToken, + address indexed remoteToken, + address indexed from, + address to, + uint256 amount, + bytes extraData + ); + + /// @notice Only allow EOAs to call the functions. Note that this is not safe against contracts + /// calling code within their constructors, but also doesn't really matter since we're + /// just trying to prevent users accidentally depositing with smart contract wallets. + modifier onlyEOA() { + require(!Address.isContract(msg.sender), "DedicatedBridge: function can only be called from an EOA"); + _; + } + + /// @notice Ensures that the caller is a cross-chain message from the other bridge. + modifier onlyOtherBridge() { + require( + msg.sender == address(messenger) && messenger.xDomainMessageSender() == address(otherBridge), + "DedicatedBridge: function can only be called from the other bridge" + ); + _; + } + + /// @notice Initializer. + /// @param _messenger Contract for CrossDomainMessenger on this network. + /// @param _otherBridge Contract for the other DedicatedBridge contract. + function __StandardBridge_init( + CrossDomainMessenger _messenger, + DedicatedBridge _otherBridge + ) + internal + onlyInitializing + { + messenger = _messenger; + otherBridge = _otherBridge; + } + + /// @notice Returns the address of the custom gas token and the token's decimals. + function gasPayingToken() internal view virtual returns (address, uint8); + + /// @notice Returns whether the chain uses a custom gas token or not. + function isCustomGasToken() internal view returns (bool) { + (address token,) = gasPayingToken(); + return token != Constants.ETHER; + } + + /// @notice Getter for messenger contract. + /// Public getter is legacy and will be removed in the future. Use `messenger` instead. + /// @return Contract of the messenger on this domain. + /// @custom:legacy + function MESSENGER() external view returns (CrossDomainMessenger) { + return messenger; + } + + /// @notice Getter for the other bridge contract. + /// Public getter is legacy and will be removed in the future. Use `otherBridge` instead. + /// @return Contract of the bridge on the other network. + /// @custom:legacy + function OTHER_BRIDGE() external view returns (DedicatedBridge) { + 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 ERC20 tokens to the sender'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. + /// @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 bridgeERC20( + address _localToken, + address _remoteToken, + uint256 _amount, + uint32 _minGasLimit, + bytes calldata _extraData + ) + public + virtual + onlyEOA + { + _initiateBridgeERC20(_localToken, _remoteToken, msg.sender, msg.sender, _amount, _minGasLimit, _extraData); + } + + /// @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. + /// @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 + ) + public + virtual + { + _initiateBridgeERC20(_localToken, _remoteToken, msg.sender, _to, _amount, _minGasLimit, _extraData); + } + + /// @notice Finalizes an ERC20 bridge on this chain. Can only be triggered by the other + /// DedicatedBridge contract on the remote chain. + /// @param _localToken Address of the ERC20 on this chain. + /// @param _remoteToken Address of the corresponding token on the remote chain. + /// @param _from Address of the sender. + /// @param _to Address of the receiver. + /// @param _amount Amount of the ERC20 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 finalizeBridgeERC20( + address _localToken, + address _remoteToken, + address _from, + address _to, + uint256 _amount, + bytes calldata _extraData + ) + public + onlyOtherBridge + { + require(paused() == false, "DedicatedBridge: paused"); + require( + _isCorrectTokenPair(_localToken, _remoteToken), + "DedicatedBridge: wrong remote token for Optimism Mintable ERC20 local token" + ); + deposits[_localToken][_remoteToken] = deposits[_localToken][_remoteToken] - _amount; + IERC20(_localToken).safeTransfer(_to, _amount); + + // Emit the correct events. By default this will be ERC20BridgeFinalized, but child + // contracts may override this function in order to emit legacy events as well. + _emitERC20BridgeFinalized(_localToken, _remoteToken, _from, _to, _amount, _extraData); + } + + /// @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. + /// @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 _initiateBridgeERC20( + address _localToken, + address _remoteToken, + address _from, + address _to, + uint256 _amount, + uint32 _minGasLimit, + bytes memory _extraData + ) + internal + { + require(msg.value == 0, "DedicatedBridge: cannot send value"); + require(paused() == false, "DedicatedBridge: paused"); + require( + _isCorrectTokenPair(_localToken, _remoteToken), + "DedicatedBridge: wrong remote token for Optimism Mintable ERC20 local token" + ); + IERC20(_localToken).safeTransferFrom(_from, address(this), _amount); + deposits[_localToken][_remoteToken] = deposits[_localToken][_remoteToken] + _amount; + + // Emit the correct events. By default this will be ERC20BridgeInitiated, but child + // contracts may override this function in order to emit legacy events as well. + _emitERC20BridgeInitiated(_localToken, _remoteToken, _from, _to, _amount, _extraData); + + messenger.sendMessage({ + _target: address(otherBridge), + _message: abi.encodeWithSelector( + this.finalizeBridgeERC20.selector, + // Because this call will be executed on the remote chain, we reverse the order of + // the remote and local token addresses relative to their order in the + // finalizeBridgeERC20 function. + _remoteToken, + _localToken, + _from, + _to, + _amount, + _extraData + ), + _minGasLimit: _minGasLimit + }); + } + + /** + * @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 virtual returns (bool); + + /// @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. + /// @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. + function _emitERC20BridgeInitiated( + address _localToken, + address _remoteToken, + address _from, + address _to, + uint256 _amount, + bytes memory _extraData + ) + internal + virtual + { + emit ERC20BridgeInitiated(_localToken, _remoteToken, _from, _to, _amount, _extraData); + } + + /// @notice Emits the ERC20BridgeFinalized 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. + /// @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. + function _emitERC20BridgeFinalized( + address _localToken, + address _remoteToken, + address _from, + address _to, + uint256 _amount, + bytes memory _extraData + ) + internal + virtual + { + emit ERC20BridgeFinalized(_localToken, _remoteToken, _from, _to, _amount, _extraData); + } +}