From 50c4d572565f7792b0b9a711656b37f8764df2e1 Mon Sep 17 00:00:00 2001 From: hongdexiang <1090664234@qq.com> Date: Thu, 15 Jun 2023 11:49:00 +0800 Subject: [PATCH 1/3] feature: add blacklist in OptimismPortal --- .../L1/OptimismPortalWithBlackList.sol | 591 ++++++++++++++++++ 1 file changed, 591 insertions(+) create mode 100644 packages/contracts-bedrock/contracts/L1/OptimismPortalWithBlackList.sol diff --git a/packages/contracts-bedrock/contracts/L1/OptimismPortalWithBlackList.sol b/packages/contracts-bedrock/contracts/L1/OptimismPortalWithBlackList.sol new file mode 100644 index 0000000000000..3b40944c383ed --- /dev/null +++ b/packages/contracts-bedrock/contracts/L1/OptimismPortalWithBlackList.sol @@ -0,0 +1,591 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import { SafeCall } from "../libraries/SafeCall.sol"; +import { L2OutputOracle } from "./L2OutputOracle.sol"; +import { SystemConfig } from "./SystemConfig.sol"; +import { Constants } from "../libraries/Constants.sol"; +import { Types } from "../libraries/Types.sol"; +import { Hashing } from "../libraries/Hashing.sol"; +import { SecureMerkleTrie } from "../libraries/trie/SecureMerkleTrie.sol"; +import { AddressAliasHelper } from "../vendor/AddressAliasHelper.sol"; +import { ResourceMetering } from "./ResourceMetering.sol"; +import { Semver } from "../universal/Semver.sol"; + +/** + * @custom:proxied + * @title OptimismPortal + * @notice The OptimismPortal is a low-level contract responsible for passing messages between L1 + * and L2. Messages sent directly to the OptimismPortal have no form of replayability. + * Users are encouraged to use the L1CrossDomainMessenger for a higher-level interface. + */ +contract OptimismPortal is Initializable, ResourceMetering, Semver { + /** + * @notice Represents a proven withdrawal. + * + * @custom:field outputRoot Root of the L2 output this was proven against. + * @custom:field timestamp Timestamp at whcih the withdrawal was proven. + * @custom:field l2OutputIndex Index of the output this was proven against. + */ + struct ProvenWithdrawal { + bytes32 outputRoot; + uint128 timestamp; + uint128 l2OutputIndex; + } + + /** + * @notice Version of the deposit event. + */ + uint256 internal constant DEPOSIT_VERSION = 0; + + /** + * @notice The L2 gas limit set when eth is deposited using the receive() function. + */ + uint64 internal constant RECEIVE_DEFAULT_GAS_LIMIT = 100_000; + + /** + * @notice Address of the L2OutputOracle contract. + */ + L2OutputOracle public immutable L2_ORACLE; + + /** + * @notice Address of the SystemConfig contract. + */ + SystemConfig public immutable SYSTEM_CONFIG; + + /** + * @notice Address that has the ability to pause and unpause withdrawals. + */ + address public immutable GUARDIAN; + + /** + * @notice Address of the L2 account which initiated a withdrawal in this transaction. If the + * of this variable is the default L2 sender address, then we are NOT inside of a call + * to finalizeWithdrawalTransaction. + */ + address public l2Sender; + + /** + * @notice A list of withdrawal hashes which have been successfully finalized. + */ + mapping(bytes32 => bool) public finalizedWithdrawals; + + /** + * @notice A mapping of withdrawal hashes to `ProvenWithdrawal` data. + */ + mapping(bytes32 => ProvenWithdrawal) public provenWithdrawals; + + /** + * @notice Determines if cross domain messaging is paused. When set to true, + * withdrawals are paused. This may be removed in the future. + */ + bool public paused; + + /** + * @notice A array of black accounts which can't `ProvenWithdrawal` and `finalizeWithdrawal` + */ + address[] private _blackList; + + /** + * @notice A map of black accounts + */ + mapping(address => uint256) private _blackListMap; + + /** + * @notice Emitted when a transaction is deposited from L1 to L2. The parameters of this event + * are read by the rollup node and used to derive deposit transactions on L2. + * + * @param from Address that triggered the deposit transaction. + * @param to Address that the deposit transaction is directed to. + * @param version Version of this deposit transaction event. + * @param opaqueData ABI encoded deposit data to be parsed off-chain. + */ + event TransactionDeposited( + address indexed from, + address indexed to, + uint256 indexed version, + bytes opaqueData + ); + + /** + * @notice Emitted when a withdrawal transaction is proven. + * + * @param withdrawalHash Hash of the withdrawal transaction. + */ + event WithdrawalProven( + bytes32 indexed withdrawalHash, + address indexed from, + address indexed to + ); + + /** + * @notice Emitted when a withdrawal transaction is finalized. + * + * @param withdrawalHash Hash of the withdrawal transaction. + * @param success Whether the withdrawal transaction was successful. + */ + event WithdrawalFinalized(bytes32 indexed withdrawalHash, bool success); + + /** + * @notice Emitted when the pause is triggered. + * + * @param account Address of the account triggering the pause. + */ + event Paused(address account); + + /** + * @notice Emitted when the pause is lifted. + * + * @param account Address of the account triggering the unpause. + */ + event Unpaused(address account); + + /** + * @notice Emitted when the black account is added. + * + * @param blackAccount Address + */ + event blackAccountAdded(address blackAccount); + + /** + * @notice Emitted when the black account is removed. + * + * @param blackAccount Address + */ + event blackAccountRemoved(address blackAccount); + + /** + * @notice Reverts when paused. + */ + modifier whenNotPaused() { + require(paused == false, "OptimismPortal: paused"); + _; + } + + /** + * @notice Reverts when paused. + */ + modifier notBlackAccount() { + require(!isBlackAccount(msg.sender), "OptimismPortal: black account"); + _; + } + + /** + * @custom:semver 1.6.0 + * + * @param _l2Oracle Address of the L2OutputOracle contract. + * @param _guardian Address that can pause deposits and withdrawals. + * @param _paused Sets the contract's pausability state. + * @param _config Address of the SystemConfig contract. + */ + constructor( + L2OutputOracle _l2Oracle, + address _guardian, + bool _paused, + SystemConfig _config + ) Semver(1, 6, 0) { + L2_ORACLE = _l2Oracle; + GUARDIAN = _guardian; + SYSTEM_CONFIG = _config; + initialize(_paused); + } + + /** + * @notice Initializer. + */ + function initialize(bool _paused) public initializer { + l2Sender = Constants.DEFAULT_L2_SENDER; + paused = _paused; + __ResourceMetering_init(); + } + + /** + * @notice Pause deposits and withdrawals. + */ + function pause() external { + require(msg.sender == GUARDIAN, "OptimismPortal: only guardian can pause"); + paused = true; + emit Paused(msg.sender); + } + + /** + * @notice Unpause deposits and withdrawals. + */ + function unpause() external { + require(msg.sender == GUARDIAN, "OptimismPortal: only guardian can unpause"); + paused = false; + emit Unpaused(msg.sender); + } + + /** + * @notice Computes the minimum gas limit for a deposit. The minimum gas limit + * linearly increases based on the size of the calldata. This is to prevent + * users from creating L2 resource usage without paying for it. This function + * can be used when interacting with the portal to ensure forwards compatibility. + * + */ + function minimumGasLimit(uint64 _byteCount) public pure returns (uint64) { + return _byteCount * 16 + 21000; + } + + /** + * @notice Accepts value so that users can send ETH directly to this contract and have the + * funds be deposited to their address on L2. This is intended as a convenience + * function for EOAs. Contracts should call the depositTransaction() function directly + * otherwise any deposited funds will be lost due to address aliasing. + */ + // solhint-disable-next-line ordering + receive() external payable { + depositTransaction(msg.sender, msg.value, RECEIVE_DEFAULT_GAS_LIMIT, false, bytes("")); + } + + /** + * @notice Accepts ETH value without triggering a deposit to L2. This function mainly exists + * for the sake of the migration between the legacy Optimism system and Bedrock. + */ + function donateETH() external payable { + // Intentionally empty. + } + + /** + * @notice Getter for the resource config. Used internally by the ResourceMetering + * contract. The SystemConfig is the source of truth for the resource config. + * + * @return ResourceMetering.ResourceConfig + */ + function _resourceConfig() + internal + view + override + returns (ResourceMetering.ResourceConfig memory) + { + return SYSTEM_CONFIG.resourceConfig(); + } + + /** + * @notice Proves a withdrawal transaction. + * + * @param _tx Withdrawal transaction to finalize. + * @param _l2OutputIndex L2 output index to prove against. + * @param _outputRootProof Inclusion proof of the L2ToL1MessagePasser contract's storage root. + * @param _withdrawalProof Inclusion proof of the withdrawal in L2ToL1MessagePasser contract. + */ + function proveWithdrawalTransaction( + Types.WithdrawalTransaction memory _tx, + uint256 _l2OutputIndex, + Types.OutputRootProof calldata _outputRootProof, + bytes[] calldata _withdrawalProof + ) external whenNotPaused notBlackAccount { + // Prevent users from creating a deposit transaction where this address is the message + // sender on L2. Because this is checked here, we do not need to check again in + // `finalizeWithdrawalTransaction`. + require( + _tx.target != address(this), + "OptimismPortal: you cannot send messages to the portal contract" + ); + + // Get the output root and load onto the stack to prevent multiple mloads. This will + // revert if there is no output root for the given block number. + bytes32 outputRoot = L2_ORACLE.getL2Output(_l2OutputIndex).outputRoot; + + // Verify that the output root can be generated with the elements in the proof. + require( + outputRoot == Hashing.hashOutputRootProof(_outputRootProof), + "OptimismPortal: invalid output root proof" + ); + + // Load the ProvenWithdrawal into memory, using the withdrawal hash as a unique identifier. + bytes32 withdrawalHash = Hashing.hashWithdrawal(_tx); + ProvenWithdrawal memory provenWithdrawal = provenWithdrawals[withdrawalHash]; + + // We generally want to prevent users from proving the same withdrawal multiple times + // because each successive proof will update the timestamp. A malicious user can take + // advantage of this to prevent other users from finalizing their withdrawal. However, + // since withdrawals are proven before an output root is finalized, we need to allow users + // to re-prove their withdrawal only in the case that the output root for their specified + // output index has been updated. + require( + provenWithdrawal.timestamp == 0 || + L2_ORACLE.getL2Output(provenWithdrawal.l2OutputIndex).outputRoot != + provenWithdrawal.outputRoot, + "OptimismPortal: withdrawal hash has already been proven" + ); + + // Compute the storage slot of the withdrawal hash in the L2ToL1MessagePasser contract. + // Refer to the Solidity documentation for more information on how storage layouts are + // computed for mappings. + bytes32 storageKey = keccak256( + abi.encode( + withdrawalHash, + uint256(0) // The withdrawals mapping is at the first slot in the layout. + ) + ); + + // Verify that the hash of this withdrawal was stored in the L2toL1MessagePasser contract + // on L2. If this is true, under the assumption that the SecureMerkleTrie does not have + // bugs, then we know that this withdrawal was actually triggered on L2 and can therefore + // be relayed on L1. + require( + SecureMerkleTrie.verifyInclusionProof( + abi.encode(storageKey), + hex"01", + _withdrawalProof, + _outputRootProof.messagePasserStorageRoot + ), + "OptimismPortal: invalid withdrawal inclusion proof" + ); + + // Designate the withdrawalHash as proven by storing the `outputRoot`, `timestamp`, and + // `l2BlockNumber` in the `provenWithdrawals` mapping. A `withdrawalHash` can only be + // proven once unless it is submitted again with a different outputRoot. + provenWithdrawals[withdrawalHash] = ProvenWithdrawal({ + outputRoot: outputRoot, + timestamp: uint128(block.timestamp), + l2OutputIndex: uint128(_l2OutputIndex) + }); + + // Emit a `WithdrawalProven` event. + emit WithdrawalProven(withdrawalHash, _tx.sender, _tx.target); + } + + /** + * @notice Finalizes a withdrawal transaction. + * + * @param _tx Withdrawal transaction to finalize. + */ + function finalizeWithdrawalTransaction(Types.WithdrawalTransaction memory _tx) + external + whenNotPaused notBlackAccount + { + // Make sure that the l2Sender has not yet been set. The l2Sender is set to a value other + // than the default value when a withdrawal transaction is being finalized. This check is + // a defacto reentrancy guard. + require( + l2Sender == Constants.DEFAULT_L2_SENDER, + "OptimismPortal: can only trigger one withdrawal per transaction" + ); + + // Grab the proven withdrawal from the `provenWithdrawals` map. + bytes32 withdrawalHash = Hashing.hashWithdrawal(_tx); + ProvenWithdrawal memory provenWithdrawal = provenWithdrawals[withdrawalHash]; + + // A withdrawal can only be finalized if it has been proven. We know that a withdrawal has + // been proven at least once when its timestamp is non-zero. Unproven withdrawals will have + // a timestamp of zero. + require( + provenWithdrawal.timestamp != 0, + "OptimismPortal: withdrawal has not been proven yet" + ); + + // As a sanity check, we make sure that the proven withdrawal's timestamp is greater than + // starting timestamp inside the L2OutputOracle. Not strictly necessary but extra layer of + // safety against weird bugs in the proving step. + require( + provenWithdrawal.timestamp >= L2_ORACLE.startingTimestamp(), + "OptimismPortal: withdrawal timestamp less than L2 Oracle starting timestamp" + ); + + // A proven withdrawal must wait at least the finalization period before it can be + // finalized. This waiting period can elapse in parallel with the waiting period for the + // output the withdrawal was proven against. In effect, this means that the minimum + // withdrawal time is proposal submission time + finalization period. + require( + _isFinalizationPeriodElapsed(provenWithdrawal.timestamp), + "OptimismPortal: proven withdrawal finalization period has not elapsed" + ); + + // Grab the OutputProposal from the L2OutputOracle, will revert if the output that + // corresponds to the given index has not been proposed yet. + Types.OutputProposal memory proposal = L2_ORACLE.getL2Output( + provenWithdrawal.l2OutputIndex + ); + + // Check that the output root that was used to prove the withdrawal is the same as the + // current output root for the given output index. An output root may change if it is + // deleted by the challenger address and then re-proposed. + require( + proposal.outputRoot == provenWithdrawal.outputRoot, + "OptimismPortal: output root proven is not the same as current output root" + ); + + // Check that the output proposal has also been finalized. + require( + _isFinalizationPeriodElapsed(proposal.timestamp), + "OptimismPortal: output proposal finalization period has not elapsed" + ); + + // Check that this withdrawal has not already been finalized, this is replay protection. + require( + finalizedWithdrawals[withdrawalHash] == false, + "OptimismPortal: withdrawal has already been finalized" + ); + + // Mark the withdrawal as finalized so it can't be replayed. + finalizedWithdrawals[withdrawalHash] = true; + + // Set the l2Sender so contracts know who triggered this withdrawal on L2. + l2Sender = _tx.sender; + + // Trigger the call to the target contract. We use a custom low level method + // SafeCall.callWithMinGas to ensure two key properties + // 1. Target contracts cannot force this call to run out of gas by returning a very large + // amount of data (and this is OK because we don't care about the returndata here). + // 2. The amount of gas provided to the execution context of the target is at least the + // gas limit specified by the user. If there is not enough gas in the current context + // to accomplish this, `callWithMinGas` will revert. + bool success = SafeCall.callWithMinGas(_tx.target, _tx.gasLimit, _tx.value, _tx.data); + + // Reset the l2Sender back to the default value. + l2Sender = Constants.DEFAULT_L2_SENDER; + + // All withdrawals are immediately finalized. Replayability can + // be achieved through contracts built on top of this contract + emit WithdrawalFinalized(withdrawalHash, success); + + // Reverting here is useful for determining the exact gas cost to successfully execute the + // sub call to the target contract if the minimum gas limit specified by the user would not + // be sufficient to execute the sub call. + if (success == false && tx.origin == Constants.ESTIMATION_ADDRESS) { + revert("OptimismPortal: withdrawal failed"); + } + } + + /** + * @notice Accepts deposits of ETH and data, and emits a TransactionDeposited event for use in + * deriving deposit transactions. Note that if a deposit is made by a contract, its + * address will be aliased when retrieved using `tx.origin` or `msg.sender`. Consider + * using the CrossDomainMessenger contracts for a simpler developer experience. + * + * @param _to Target address on L2. + * @param _value ETH value to send to the recipient. + * @param _gasLimit Minimum L2 gas limit (can be greater than or equal to this value). + * @param _isCreation Whether or not the transaction is a contract creation. + * @param _data Data to trigger the recipient with. + */ + function depositTransaction( + address _to, + uint256 _value, + uint64 _gasLimit, + bool _isCreation, + bytes memory _data + ) public payable metered(_gasLimit) { + // Just to be safe, make sure that people specify address(0) as the target when doing + // contract creations. + if (_isCreation) { + require( + _to == address(0), + "OptimismPortal: must send to address(0) when creating a contract" + ); + } + + // Prevent depositing transactions that have too small of a gas limit. Users should pay + // more for more resource usage. + require( + _gasLimit >= minimumGasLimit(uint64(_data.length)), + "OptimismPortal: gas limit too small" + ); + + // Prevent the creation of deposit transactions that have too much calldata. This gives an + // upper limit on the size of unsafe blocks over the p2p network. 120kb is chosen to ensure + // that the transaction can fit into the p2p network policy of 128kb even though deposit + // transactions are not gossipped over the p2p network. + require(_data.length <= 120_000, "OptimismPortal: data too large"); + + // Transform the from-address to its alias if the caller is a contract. + address from = msg.sender; + if (msg.sender != tx.origin) { + from = AddressAliasHelper.applyL1ToL2Alias(msg.sender); + } + + // Compute the opaque data that will be emitted as part of the TransactionDeposited event. + // We use opaque data so that we can update the TransactionDeposited event in the future + // without breaking the current interface. + bytes memory opaqueData = abi.encodePacked( + msg.value, + _value, + _gasLimit, + _isCreation, + _data + ); + + // Emit a TransactionDeposited event so that the rollup node can derive a deposit + // transaction for this deposit. + emit TransactionDeposited(from, _to, DEPOSIT_VERSION, opaqueData); + } + + /** + * @notice Determine if a given output is finalized. Reverts if the call to + * L2_ORACLE.getL2Output reverts. Returns a boolean otherwise. + * + * @param _l2OutputIndex Index of the L2 output to check. + * + * @return Whether or not the output is finalized. + */ + function isOutputFinalized(uint256 _l2OutputIndex) external view returns (bool) { + return _isFinalizationPeriodElapsed(L2_ORACLE.getL2Output(_l2OutputIndex).timestamp); + } + + /** + * @notice Determines whether the finalization period has elapsed w/r/t a given timestamp. + * + * @param _timestamp Timestamp to check. + * + * @return Whether or not the finalization period has elapsed. + */ + function _isFinalizationPeriodElapsed(uint256 _timestamp) internal view returns (bool) { + return block.timestamp > _timestamp + L2_ORACLE.FINALIZATION_PERIOD_SECONDS(); + } + + /** + * @notice Add black account + * + * @param blackAccount address to add blacklist + */ + function addBlackAccount(address blackAccount) external { + require(msg.sender == GUARDIAN, "OptimismPortal: only guardian can add black account"); + require(_blackListMap[blackAccount] == 0, "The black account already exist!"); + _blackList.push(blackAccount); + _blackListMap[blackAccount] = _blackList.length; + emit blackAccountAdded(blackAccount); + } + + /** + * @notice remove black account + * + * @param blackAccount address to remove from blacklist + */ + function removeBlackAccount(address blackAccount) external { + require(msg.sender == GUARDIAN, "OptimismPortal: only guardian can remove black account"); + uint256 position = _blackListMap[blackAccount]; + if (position > 0) { + uint256 indexOf = position - 1; + if (_blackList.length > 1 && indexOf != _blackList.length - 1) { + address lastAddress = _blackList[_blackList.length - 1]; + _blackList[indexOf] = lastAddress; + _blackListMap[lastAddress] = indexOf + 1; + } + _blackList.pop(); + delete _blackListMap[blackAccount]; + emit blackAccountRemoved(blackAccount); + } + } + + /** + * @notice get black account list + * + * @return black account list + */ + function getBlackListList() external view returns (address[] memory) { + return _blackList; + } + + /** + * @notice get black account list + * + * @return Whether or not the address is black account + */ + function isBlackAccount(address blackAccount) public view returns (bool) { + return _blackListMap[blackAccount] != 0; + } +} From ae401d8ec2e4163c50d0f0224f7f0475ae7a4a10 Mon Sep 17 00:00:00 2001 From: hongdexiang <1090664234@qq.com> Date: Thu, 15 Jun 2023 17:07:27 +0800 Subject: [PATCH 2/3] fix: fix prove or finalize withdraw logic --- .../contracts/L1/OptimismPortalWithBlackList.sol | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/packages/contracts-bedrock/contracts/L1/OptimismPortalWithBlackList.sol b/packages/contracts-bedrock/contracts/L1/OptimismPortalWithBlackList.sol index 3b40944c383ed..52072bf09de92 100644 --- a/packages/contracts-bedrock/contracts/L1/OptimismPortalWithBlackList.sol +++ b/packages/contracts-bedrock/contracts/L1/OptimismPortalWithBlackList.sol @@ -163,14 +163,6 @@ contract OptimismPortal is Initializable, ResourceMetering, Semver { _; } - /** - * @notice Reverts when paused. - */ - modifier notBlackAccount() { - require(!isBlackAccount(msg.sender), "OptimismPortal: black account"); - _; - } - /** * @custom:semver 1.6.0 * @@ -276,7 +268,8 @@ contract OptimismPortal is Initializable, ResourceMetering, Semver { uint256 _l2OutputIndex, Types.OutputRootProof calldata _outputRootProof, bytes[] calldata _withdrawalProof - ) external whenNotPaused notBlackAccount { + ) external whenNotPaused { + require(!isBlackAccount(_tx.sender), "OptimismPortal: black account"); // Prevent users from creating a deposit transaction where this address is the message // sender on L2. Because this is checked here, we do not need to check again in // `finalizeWithdrawalTransaction`. @@ -356,8 +349,9 @@ contract OptimismPortal is Initializable, ResourceMetering, Semver { */ function finalizeWithdrawalTransaction(Types.WithdrawalTransaction memory _tx) external - whenNotPaused notBlackAccount + whenNotPaused { + require(!isBlackAccount(_tx.sender), "OptimismPortal: black account"); // Make sure that the l2Sender has not yet been set. The l2Sender is set to a value other // than the default value when a withdrawal transaction is being finalized. This check is // a defacto reentrancy guard. From 8d3ea3a0a0ffa84fe45c31bf6ea6995734dc8c72 Mon Sep 17 00:00:00 2001 From: hongdexiang <1090664234@qq.com> Date: Tue, 20 Jun 2023 17:20:43 +0800 Subject: [PATCH 3/3] feature: add black account list --- .../L1/L1StandardBridgeWithBlackList.sol | 365 ++++++++++ .../L1/OptimismPortalWithBlackList.sol | 585 ---------------- .../universal/StandardBridgeWithBlackList.sol | 646 ++++++++++++++++++ 3 files changed, 1011 insertions(+), 585 deletions(-) create mode 100644 packages/contracts-bedrock/contracts/L1/L1StandardBridgeWithBlackList.sol delete mode 100644 packages/contracts-bedrock/contracts/L1/OptimismPortalWithBlackList.sol create mode 100644 packages/contracts-bedrock/contracts/universal/StandardBridgeWithBlackList.sol diff --git a/packages/contracts-bedrock/contracts/L1/L1StandardBridgeWithBlackList.sol b/packages/contracts-bedrock/contracts/L1/L1StandardBridgeWithBlackList.sol new file mode 100644 index 0000000000000..392588506b44e --- /dev/null +++ b/packages/contracts-bedrock/contracts/L1/L1StandardBridgeWithBlackList.sol @@ -0,0 +1,365 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import { Predeploys } from "../libraries/Predeploys.sol"; +import { StandardBridge } from "../universal/StandardBridgeWithBlackList.sol"; +import { Semver } from "../universal/Semver.sol"; + +/** + * @custom:proxied + * @title L1StandardBridge + * @notice The L1StandardBridge is responsible for transfering ETH and ERC20 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 L1StandardBridge is StandardBridge, Semver { + /** + * @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 + ); + + /** + * @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 + ); + + /** + * @custom:semver 1.1.0 + * + * @param _messenger Address of the L1CrossDomainMessenger. + * @param _guardian Address that has the ability to add and remove black accounts. + */ + constructor(address payable _messenger, address _guardian) + Semver(1, 1, 0) + StandardBridge(_messenger, payable(Predeploys.L2_STANDARD_BRIDGE), _guardian) + {} + + /** + * @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("")); + } + + /** + * @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); + } + + /** + * @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 L2StandardBridge. 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); + } + + /** + * @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 onlyEOA { + _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 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. + * @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); + } + + /** + * @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(OTHER_BRIDGE); + } + + /** + * @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. + * @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); + } + + /** + * @notice Emits the legacy ETHDepositInitiated event followed by the ETHBridgeInitiated event. + * This is necessary for backwards compatibility with the legacy bridge. + * + * @inheritdoc StandardBridge + */ + 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); + } + + /** + * @notice Emits the legacy ETHWithdrawalFinalized event followed by the ETHBridgeFinalized + * event. This is necessary for backwards compatibility with the legacy bridge. + * + * @inheritdoc StandardBridge + */ + 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); + } + + /** + * @notice Emits the legacy ERC20DepositInitiated event followed by the ERC20BridgeInitiated + * event. This is necessary for backwards compatibility with the legacy bridge. + * + * @inheritdoc StandardBridge + */ + 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); + } + + /** + * @notice Emits the legacy ERC20WithdrawalFinalized event followed by the ERC20BridgeFinalized + * event. This is necessary for backwards compatibility with the legacy bridge. + * + * @inheritdoc StandardBridge + */ + 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/contracts/L1/OptimismPortalWithBlackList.sol b/packages/contracts-bedrock/contracts/L1/OptimismPortalWithBlackList.sol deleted file mode 100644 index 52072bf09de92..0000000000000 --- a/packages/contracts-bedrock/contracts/L1/OptimismPortalWithBlackList.sol +++ /dev/null @@ -1,585 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.15; - -import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; -import { SafeCall } from "../libraries/SafeCall.sol"; -import { L2OutputOracle } from "./L2OutputOracle.sol"; -import { SystemConfig } from "./SystemConfig.sol"; -import { Constants } from "../libraries/Constants.sol"; -import { Types } from "../libraries/Types.sol"; -import { Hashing } from "../libraries/Hashing.sol"; -import { SecureMerkleTrie } from "../libraries/trie/SecureMerkleTrie.sol"; -import { AddressAliasHelper } from "../vendor/AddressAliasHelper.sol"; -import { ResourceMetering } from "./ResourceMetering.sol"; -import { Semver } from "../universal/Semver.sol"; - -/** - * @custom:proxied - * @title OptimismPortal - * @notice The OptimismPortal is a low-level contract responsible for passing messages between L1 - * and L2. Messages sent directly to the OptimismPortal have no form of replayability. - * Users are encouraged to use the L1CrossDomainMessenger for a higher-level interface. - */ -contract OptimismPortal is Initializable, ResourceMetering, Semver { - /** - * @notice Represents a proven withdrawal. - * - * @custom:field outputRoot Root of the L2 output this was proven against. - * @custom:field timestamp Timestamp at whcih the withdrawal was proven. - * @custom:field l2OutputIndex Index of the output this was proven against. - */ - struct ProvenWithdrawal { - bytes32 outputRoot; - uint128 timestamp; - uint128 l2OutputIndex; - } - - /** - * @notice Version of the deposit event. - */ - uint256 internal constant DEPOSIT_VERSION = 0; - - /** - * @notice The L2 gas limit set when eth is deposited using the receive() function. - */ - uint64 internal constant RECEIVE_DEFAULT_GAS_LIMIT = 100_000; - - /** - * @notice Address of the L2OutputOracle contract. - */ - L2OutputOracle public immutable L2_ORACLE; - - /** - * @notice Address of the SystemConfig contract. - */ - SystemConfig public immutable SYSTEM_CONFIG; - - /** - * @notice Address that has the ability to pause and unpause withdrawals. - */ - address public immutable GUARDIAN; - - /** - * @notice Address of the L2 account which initiated a withdrawal in this transaction. If the - * of this variable is the default L2 sender address, then we are NOT inside of a call - * to finalizeWithdrawalTransaction. - */ - address public l2Sender; - - /** - * @notice A list of withdrawal hashes which have been successfully finalized. - */ - mapping(bytes32 => bool) public finalizedWithdrawals; - - /** - * @notice A mapping of withdrawal hashes to `ProvenWithdrawal` data. - */ - mapping(bytes32 => ProvenWithdrawal) public provenWithdrawals; - - /** - * @notice Determines if cross domain messaging is paused. When set to true, - * withdrawals are paused. This may be removed in the future. - */ - bool public paused; - - /** - * @notice A array of black accounts which can't `ProvenWithdrawal` and `finalizeWithdrawal` - */ - address[] private _blackList; - - /** - * @notice A map of black accounts - */ - mapping(address => uint256) private _blackListMap; - - /** - * @notice Emitted when a transaction is deposited from L1 to L2. The parameters of this event - * are read by the rollup node and used to derive deposit transactions on L2. - * - * @param from Address that triggered the deposit transaction. - * @param to Address that the deposit transaction is directed to. - * @param version Version of this deposit transaction event. - * @param opaqueData ABI encoded deposit data to be parsed off-chain. - */ - event TransactionDeposited( - address indexed from, - address indexed to, - uint256 indexed version, - bytes opaqueData - ); - - /** - * @notice Emitted when a withdrawal transaction is proven. - * - * @param withdrawalHash Hash of the withdrawal transaction. - */ - event WithdrawalProven( - bytes32 indexed withdrawalHash, - address indexed from, - address indexed to - ); - - /** - * @notice Emitted when a withdrawal transaction is finalized. - * - * @param withdrawalHash Hash of the withdrawal transaction. - * @param success Whether the withdrawal transaction was successful. - */ - event WithdrawalFinalized(bytes32 indexed withdrawalHash, bool success); - - /** - * @notice Emitted when the pause is triggered. - * - * @param account Address of the account triggering the pause. - */ - event Paused(address account); - - /** - * @notice Emitted when the pause is lifted. - * - * @param account Address of the account triggering the unpause. - */ - event Unpaused(address account); - - /** - * @notice Emitted when the black account is added. - * - * @param blackAccount Address - */ - event blackAccountAdded(address blackAccount); - - /** - * @notice Emitted when the black account is removed. - * - * @param blackAccount Address - */ - event blackAccountRemoved(address blackAccount); - - /** - * @notice Reverts when paused. - */ - modifier whenNotPaused() { - require(paused == false, "OptimismPortal: paused"); - _; - } - - /** - * @custom:semver 1.6.0 - * - * @param _l2Oracle Address of the L2OutputOracle contract. - * @param _guardian Address that can pause deposits and withdrawals. - * @param _paused Sets the contract's pausability state. - * @param _config Address of the SystemConfig contract. - */ - constructor( - L2OutputOracle _l2Oracle, - address _guardian, - bool _paused, - SystemConfig _config - ) Semver(1, 6, 0) { - L2_ORACLE = _l2Oracle; - GUARDIAN = _guardian; - SYSTEM_CONFIG = _config; - initialize(_paused); - } - - /** - * @notice Initializer. - */ - function initialize(bool _paused) public initializer { - l2Sender = Constants.DEFAULT_L2_SENDER; - paused = _paused; - __ResourceMetering_init(); - } - - /** - * @notice Pause deposits and withdrawals. - */ - function pause() external { - require(msg.sender == GUARDIAN, "OptimismPortal: only guardian can pause"); - paused = true; - emit Paused(msg.sender); - } - - /** - * @notice Unpause deposits and withdrawals. - */ - function unpause() external { - require(msg.sender == GUARDIAN, "OptimismPortal: only guardian can unpause"); - paused = false; - emit Unpaused(msg.sender); - } - - /** - * @notice Computes the minimum gas limit for a deposit. The minimum gas limit - * linearly increases based on the size of the calldata. This is to prevent - * users from creating L2 resource usage without paying for it. This function - * can be used when interacting with the portal to ensure forwards compatibility. - * - */ - function minimumGasLimit(uint64 _byteCount) public pure returns (uint64) { - return _byteCount * 16 + 21000; - } - - /** - * @notice Accepts value so that users can send ETH directly to this contract and have the - * funds be deposited to their address on L2. This is intended as a convenience - * function for EOAs. Contracts should call the depositTransaction() function directly - * otherwise any deposited funds will be lost due to address aliasing. - */ - // solhint-disable-next-line ordering - receive() external payable { - depositTransaction(msg.sender, msg.value, RECEIVE_DEFAULT_GAS_LIMIT, false, bytes("")); - } - - /** - * @notice Accepts ETH value without triggering a deposit to L2. This function mainly exists - * for the sake of the migration between the legacy Optimism system and Bedrock. - */ - function donateETH() external payable { - // Intentionally empty. - } - - /** - * @notice Getter for the resource config. Used internally by the ResourceMetering - * contract. The SystemConfig is the source of truth for the resource config. - * - * @return ResourceMetering.ResourceConfig - */ - function _resourceConfig() - internal - view - override - returns (ResourceMetering.ResourceConfig memory) - { - return SYSTEM_CONFIG.resourceConfig(); - } - - /** - * @notice Proves a withdrawal transaction. - * - * @param _tx Withdrawal transaction to finalize. - * @param _l2OutputIndex L2 output index to prove against. - * @param _outputRootProof Inclusion proof of the L2ToL1MessagePasser contract's storage root. - * @param _withdrawalProof Inclusion proof of the withdrawal in L2ToL1MessagePasser contract. - */ - function proveWithdrawalTransaction( - Types.WithdrawalTransaction memory _tx, - uint256 _l2OutputIndex, - Types.OutputRootProof calldata _outputRootProof, - bytes[] calldata _withdrawalProof - ) external whenNotPaused { - require(!isBlackAccount(_tx.sender), "OptimismPortal: black account"); - // Prevent users from creating a deposit transaction where this address is the message - // sender on L2. Because this is checked here, we do not need to check again in - // `finalizeWithdrawalTransaction`. - require( - _tx.target != address(this), - "OptimismPortal: you cannot send messages to the portal contract" - ); - - // Get the output root and load onto the stack to prevent multiple mloads. This will - // revert if there is no output root for the given block number. - bytes32 outputRoot = L2_ORACLE.getL2Output(_l2OutputIndex).outputRoot; - - // Verify that the output root can be generated with the elements in the proof. - require( - outputRoot == Hashing.hashOutputRootProof(_outputRootProof), - "OptimismPortal: invalid output root proof" - ); - - // Load the ProvenWithdrawal into memory, using the withdrawal hash as a unique identifier. - bytes32 withdrawalHash = Hashing.hashWithdrawal(_tx); - ProvenWithdrawal memory provenWithdrawal = provenWithdrawals[withdrawalHash]; - - // We generally want to prevent users from proving the same withdrawal multiple times - // because each successive proof will update the timestamp. A malicious user can take - // advantage of this to prevent other users from finalizing their withdrawal. However, - // since withdrawals are proven before an output root is finalized, we need to allow users - // to re-prove their withdrawal only in the case that the output root for their specified - // output index has been updated. - require( - provenWithdrawal.timestamp == 0 || - L2_ORACLE.getL2Output(provenWithdrawal.l2OutputIndex).outputRoot != - provenWithdrawal.outputRoot, - "OptimismPortal: withdrawal hash has already been proven" - ); - - // Compute the storage slot of the withdrawal hash in the L2ToL1MessagePasser contract. - // Refer to the Solidity documentation for more information on how storage layouts are - // computed for mappings. - bytes32 storageKey = keccak256( - abi.encode( - withdrawalHash, - uint256(0) // The withdrawals mapping is at the first slot in the layout. - ) - ); - - // Verify that the hash of this withdrawal was stored in the L2toL1MessagePasser contract - // on L2. If this is true, under the assumption that the SecureMerkleTrie does not have - // bugs, then we know that this withdrawal was actually triggered on L2 and can therefore - // be relayed on L1. - require( - SecureMerkleTrie.verifyInclusionProof( - abi.encode(storageKey), - hex"01", - _withdrawalProof, - _outputRootProof.messagePasserStorageRoot - ), - "OptimismPortal: invalid withdrawal inclusion proof" - ); - - // Designate the withdrawalHash as proven by storing the `outputRoot`, `timestamp`, and - // `l2BlockNumber` in the `provenWithdrawals` mapping. A `withdrawalHash` can only be - // proven once unless it is submitted again with a different outputRoot. - provenWithdrawals[withdrawalHash] = ProvenWithdrawal({ - outputRoot: outputRoot, - timestamp: uint128(block.timestamp), - l2OutputIndex: uint128(_l2OutputIndex) - }); - - // Emit a `WithdrawalProven` event. - emit WithdrawalProven(withdrawalHash, _tx.sender, _tx.target); - } - - /** - * @notice Finalizes a withdrawal transaction. - * - * @param _tx Withdrawal transaction to finalize. - */ - function finalizeWithdrawalTransaction(Types.WithdrawalTransaction memory _tx) - external - whenNotPaused - { - require(!isBlackAccount(_tx.sender), "OptimismPortal: black account"); - // Make sure that the l2Sender has not yet been set. The l2Sender is set to a value other - // than the default value when a withdrawal transaction is being finalized. This check is - // a defacto reentrancy guard. - require( - l2Sender == Constants.DEFAULT_L2_SENDER, - "OptimismPortal: can only trigger one withdrawal per transaction" - ); - - // Grab the proven withdrawal from the `provenWithdrawals` map. - bytes32 withdrawalHash = Hashing.hashWithdrawal(_tx); - ProvenWithdrawal memory provenWithdrawal = provenWithdrawals[withdrawalHash]; - - // A withdrawal can only be finalized if it has been proven. We know that a withdrawal has - // been proven at least once when its timestamp is non-zero. Unproven withdrawals will have - // a timestamp of zero. - require( - provenWithdrawal.timestamp != 0, - "OptimismPortal: withdrawal has not been proven yet" - ); - - // As a sanity check, we make sure that the proven withdrawal's timestamp is greater than - // starting timestamp inside the L2OutputOracle. Not strictly necessary but extra layer of - // safety against weird bugs in the proving step. - require( - provenWithdrawal.timestamp >= L2_ORACLE.startingTimestamp(), - "OptimismPortal: withdrawal timestamp less than L2 Oracle starting timestamp" - ); - - // A proven withdrawal must wait at least the finalization period before it can be - // finalized. This waiting period can elapse in parallel with the waiting period for the - // output the withdrawal was proven against. In effect, this means that the minimum - // withdrawal time is proposal submission time + finalization period. - require( - _isFinalizationPeriodElapsed(provenWithdrawal.timestamp), - "OptimismPortal: proven withdrawal finalization period has not elapsed" - ); - - // Grab the OutputProposal from the L2OutputOracle, will revert if the output that - // corresponds to the given index has not been proposed yet. - Types.OutputProposal memory proposal = L2_ORACLE.getL2Output( - provenWithdrawal.l2OutputIndex - ); - - // Check that the output root that was used to prove the withdrawal is the same as the - // current output root for the given output index. An output root may change if it is - // deleted by the challenger address and then re-proposed. - require( - proposal.outputRoot == provenWithdrawal.outputRoot, - "OptimismPortal: output root proven is not the same as current output root" - ); - - // Check that the output proposal has also been finalized. - require( - _isFinalizationPeriodElapsed(proposal.timestamp), - "OptimismPortal: output proposal finalization period has not elapsed" - ); - - // Check that this withdrawal has not already been finalized, this is replay protection. - require( - finalizedWithdrawals[withdrawalHash] == false, - "OptimismPortal: withdrawal has already been finalized" - ); - - // Mark the withdrawal as finalized so it can't be replayed. - finalizedWithdrawals[withdrawalHash] = true; - - // Set the l2Sender so contracts know who triggered this withdrawal on L2. - l2Sender = _tx.sender; - - // Trigger the call to the target contract. We use a custom low level method - // SafeCall.callWithMinGas to ensure two key properties - // 1. Target contracts cannot force this call to run out of gas by returning a very large - // amount of data (and this is OK because we don't care about the returndata here). - // 2. The amount of gas provided to the execution context of the target is at least the - // gas limit specified by the user. If there is not enough gas in the current context - // to accomplish this, `callWithMinGas` will revert. - bool success = SafeCall.callWithMinGas(_tx.target, _tx.gasLimit, _tx.value, _tx.data); - - // Reset the l2Sender back to the default value. - l2Sender = Constants.DEFAULT_L2_SENDER; - - // All withdrawals are immediately finalized. Replayability can - // be achieved through contracts built on top of this contract - emit WithdrawalFinalized(withdrawalHash, success); - - // Reverting here is useful for determining the exact gas cost to successfully execute the - // sub call to the target contract if the minimum gas limit specified by the user would not - // be sufficient to execute the sub call. - if (success == false && tx.origin == Constants.ESTIMATION_ADDRESS) { - revert("OptimismPortal: withdrawal failed"); - } - } - - /** - * @notice Accepts deposits of ETH and data, and emits a TransactionDeposited event for use in - * deriving deposit transactions. Note that if a deposit is made by a contract, its - * address will be aliased when retrieved using `tx.origin` or `msg.sender`. Consider - * using the CrossDomainMessenger contracts for a simpler developer experience. - * - * @param _to Target address on L2. - * @param _value ETH value to send to the recipient. - * @param _gasLimit Minimum L2 gas limit (can be greater than or equal to this value). - * @param _isCreation Whether or not the transaction is a contract creation. - * @param _data Data to trigger the recipient with. - */ - function depositTransaction( - address _to, - uint256 _value, - uint64 _gasLimit, - bool _isCreation, - bytes memory _data - ) public payable metered(_gasLimit) { - // Just to be safe, make sure that people specify address(0) as the target when doing - // contract creations. - if (_isCreation) { - require( - _to == address(0), - "OptimismPortal: must send to address(0) when creating a contract" - ); - } - - // Prevent depositing transactions that have too small of a gas limit. Users should pay - // more for more resource usage. - require( - _gasLimit >= minimumGasLimit(uint64(_data.length)), - "OptimismPortal: gas limit too small" - ); - - // Prevent the creation of deposit transactions that have too much calldata. This gives an - // upper limit on the size of unsafe blocks over the p2p network. 120kb is chosen to ensure - // that the transaction can fit into the p2p network policy of 128kb even though deposit - // transactions are not gossipped over the p2p network. - require(_data.length <= 120_000, "OptimismPortal: data too large"); - - // Transform the from-address to its alias if the caller is a contract. - address from = msg.sender; - if (msg.sender != tx.origin) { - from = AddressAliasHelper.applyL1ToL2Alias(msg.sender); - } - - // Compute the opaque data that will be emitted as part of the TransactionDeposited event. - // We use opaque data so that we can update the TransactionDeposited event in the future - // without breaking the current interface. - bytes memory opaqueData = abi.encodePacked( - msg.value, - _value, - _gasLimit, - _isCreation, - _data - ); - - // Emit a TransactionDeposited event so that the rollup node can derive a deposit - // transaction for this deposit. - emit TransactionDeposited(from, _to, DEPOSIT_VERSION, opaqueData); - } - - /** - * @notice Determine if a given output is finalized. Reverts if the call to - * L2_ORACLE.getL2Output reverts. Returns a boolean otherwise. - * - * @param _l2OutputIndex Index of the L2 output to check. - * - * @return Whether or not the output is finalized. - */ - function isOutputFinalized(uint256 _l2OutputIndex) external view returns (bool) { - return _isFinalizationPeriodElapsed(L2_ORACLE.getL2Output(_l2OutputIndex).timestamp); - } - - /** - * @notice Determines whether the finalization period has elapsed w/r/t a given timestamp. - * - * @param _timestamp Timestamp to check. - * - * @return Whether or not the finalization period has elapsed. - */ - function _isFinalizationPeriodElapsed(uint256 _timestamp) internal view returns (bool) { - return block.timestamp > _timestamp + L2_ORACLE.FINALIZATION_PERIOD_SECONDS(); - } - - /** - * @notice Add black account - * - * @param blackAccount address to add blacklist - */ - function addBlackAccount(address blackAccount) external { - require(msg.sender == GUARDIAN, "OptimismPortal: only guardian can add black account"); - require(_blackListMap[blackAccount] == 0, "The black account already exist!"); - _blackList.push(blackAccount); - _blackListMap[blackAccount] = _blackList.length; - emit blackAccountAdded(blackAccount); - } - - /** - * @notice remove black account - * - * @param blackAccount address to remove from blacklist - */ - function removeBlackAccount(address blackAccount) external { - require(msg.sender == GUARDIAN, "OptimismPortal: only guardian can remove black account"); - uint256 position = _blackListMap[blackAccount]; - if (position > 0) { - uint256 indexOf = position - 1; - if (_blackList.length > 1 && indexOf != _blackList.length - 1) { - address lastAddress = _blackList[_blackList.length - 1]; - _blackList[indexOf] = lastAddress; - _blackListMap[lastAddress] = indexOf + 1; - } - _blackList.pop(); - delete _blackListMap[blackAccount]; - emit blackAccountRemoved(blackAccount); - } - } - - /** - * @notice get black account list - * - * @return black account list - */ - function getBlackListList() external view returns (address[] memory) { - return _blackList; - } - - /** - * @notice get black account list - * - * @return Whether or not the address is black account - */ - function isBlackAccount(address blackAccount) public view returns (bool) { - return _blackListMap[blackAccount] != 0; - } -} diff --git a/packages/contracts-bedrock/contracts/universal/StandardBridgeWithBlackList.sol b/packages/contracts-bedrock/contracts/universal/StandardBridgeWithBlackList.sol new file mode 100644 index 0000000000000..10da4b8d0bd11 --- /dev/null +++ b/packages/contracts-bedrock/contracts/universal/StandardBridgeWithBlackList.sol @@ -0,0 +1,646 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { SafeCall } from "../libraries/SafeCall.sol"; +import { IOptimismMintableERC20, ILegacyMintableERC20 } from "./IOptimismMintableERC20.sol"; +import { CrossDomainMessenger } from "./CrossDomainMessenger.sol"; +import { OptimismMintableERC20 } from "./OptimismMintableERC20.sol"; + +/** + * @custom:upgradeable + * @title StandardBridge + * @notice StandardBridge 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 StandardBridge { + 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 Messenger contract on this domain. + */ + CrossDomainMessenger public immutable MESSENGER; + + /** + * @notice Corresponding bridge on the other domain. + */ + StandardBridge public immutable OTHER_BRIDGE; + + /** + * @custom:legacy + * @custom:spacer messenger + * @notice Spacer for backwards compatibility. + */ + address private spacer_0_0_20; + + /** + * @custom:legacy + * @custom:spacer l2TokenBridge + * @notice Spacer for backwards compatibility. + */ + address private spacer_1_0_20; + + /** + * @notice Mapping that stores deposits for a given pair of local and remote tokens. + */ + mapping(address => mapping(address => uint256)) public deposits; + + /** + * @notice Reserve extra slots (to a total of 50) in the storage layout for future upgrades. + * A gap size of 47 was chosen here, so that the first slot used in a child contract + * would be a multiple of 50. + */ + uint256[47] private __gap; + + /** + * @notice Address that has the ability to add and remove black accounts. + */ + address public immutable GUARDIAN; + + /** + * @notice A array of black accounts which can't withdraw + */ + address[] private _blackList; + + /** + * @notice A map of black accounts + */ + mapping(address => uint256) private _blackListMap; + + /** + * @notice Emitted when the black account is added. + * + * @param blackAccount Address + */ + event blackAccountAdded(address blackAccount); + + /** + * @notice Emitted when the black account is removed. + * + * @param blackAccount Address + */ + event blackAccountRemoved(address blackAccount); + + /** + * @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 + ); + + /** + * @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), + "StandardBridge: 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(OTHER_BRIDGE), + "StandardBridge: function can only be called from the other bridge" + ); + _; + } + + /** + * @param _messenger Address of CrossDomainMessenger on this network. + * @param _otherBridge Address of the other StandardBridge contract. + * @param _guardian Address that has the ability to add and remove black accounts. + */ + constructor(address payable _messenger, address payable _otherBridge, address _guardian) { + MESSENGER = CrossDomainMessenger(_messenger); + OTHER_BRIDGE = StandardBridge(_otherBridge); + GUARDIAN = _guardian; + } + + /** + * @notice Allows EOAs to bridge ETH by sending directly to the bridge. + * Must be implemented by contracts that inherit. + */ + receive() external payable virtual; + + /** + * @custom:legacy + * @notice Legacy getter for messenger contract. + * + * @return Messenger contract on this domain. + */ + function messenger() external view returns (CrossDomainMessenger) { + return MESSENGER; + } + + /** + * @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); + } + + /** + * @notice Sends ETH to a receiver's address on the other chain. Note that if ETH is sent to a + * smart contract and the call fails, the ETH will be temporarily locked in the + * StandardBridge on the other chain until the call is replayed. If the call cannot be + * replayed with any amount of gas (call always reverts), then the ETH will be + * permanently locked in the StandardBridge on the other chain. ETH will also + * be locked if the receiver is the other bridge, because finalizeBridgeETH will revert + * in that case. + * + * @param _to Address of the receiver. + * @param _minGasLimit Minimum amount of gas that the bridge can be relayed with. + * @param _extraData Extra data to be sent with the transaction. Note that the recipient will + * not be triggered with this data, but it will be emitted and can be used + * to identify the transaction. + */ + function bridgeETHTo( + address _to, + uint32 _minGasLimit, + bytes calldata _extraData + ) public payable { + _initiateBridgeETH(msg.sender, _to, msg.value, _minGasLimit, _extraData); + } + + /** + * @notice Sends ERC20 tokens to the sender's address on the other chain. Note that if the + * ERC20 token on the other chain does not recognize the local token as the correct + * pair token, the ERC20 bridge will fail and the tokens will be returned to sender on + * this chain. + * + * @param _localToken Address of the ERC20 on this chain. + * @param _remoteToken Address of the corresponding token on the remote chain. + * @param _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. Note that if the + * ERC20 token on the other chain does not recognize the local token as the correct + * pair token, the ERC20 bridge will fail and the tokens will be returned to sender on + * this chain. + * + * @param _localToken Address of the ERC20 on this chain. + * @param _remoteToken Address of the corresponding token on the remote chain. + * @param _to Address of the receiver. + * @param _amount Amount of local tokens to deposit. + * @param _minGasLimit Minimum amount of gas that the bridge can be relayed with. + * @param _extraData Extra data to be sent with the transaction. Note that the recipient will + * not be triggered with this data, but it will be emitted and can be used + * to identify the transaction. + */ + function bridgeERC20To( + address _localToken, + address _remoteToken, + address _to, + uint256 _amount, + uint32 _minGasLimit, + bytes calldata _extraData + ) public virtual { + _initiateBridgeERC20( + _localToken, + _remoteToken, + msg.sender, + _to, + _amount, + _minGasLimit, + _extraData + ); + } + + /** + * @notice Finalizes an ETH bridge on this chain. Can only be triggered by the other + * StandardBridge 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(!isBlackAccount(_from), "black account"); + require(msg.value == _amount, "StandardBridge: amount sent does not match amount required"); + require(_to != address(this), "StandardBridge: cannot send to self"); + require(_to != address(MESSENGER), "StandardBridge: 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, "StandardBridge: ETH transfer failed"); + } + + /** + * @notice Finalizes an ERC20 bridge on this chain. Can only be triggered by the other + * StandardBridge 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(!isBlackAccount(_from), "black account"); + if (_isOptimismMintableERC20(_localToken)) { + require( + _isCorrectTokenPair(_localToken, _remoteToken), + "StandardBridge: wrong remote token for Optimism Mintable ERC20 local token" + ); + + OptimismMintableERC20(_localToken).mint(_to, _amount); + } else { + 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 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, + "StandardBridge: 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 }( + address(OTHER_BRIDGE), + abi.encodeWithSelector( + this.finalizeBridgeETH.selector, + _from, + _to, + _amount, + _extraData + ), + _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. + * @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 { + if (_isOptimismMintableERC20(_localToken)) { + require( + _isCorrectTokenPair(_localToken, _remoteToken), + "StandardBridge: wrong remote token for Optimism Mintable ERC20 local token" + ); + + OptimismMintableERC20(_localToken).burn(_from, _amount); + } else { + 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( + address(OTHER_BRIDGE), + 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 + ); + } + + /** + * @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. + * @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); + } + + /** + * @notice Add black account + * + * @param blackAccount address to add blacklist + */ + function addBlackAccount(address blackAccount) external { + require(msg.sender == GUARDIAN, "only guardian can add black account"); + require(_blackListMap[blackAccount] == 0, "The black account already exist!"); + _blackList.push(blackAccount); + _blackListMap[blackAccount] = _blackList.length; + emit blackAccountAdded(blackAccount); + } + + /** + * @notice remove black account + * + * @param blackAccount address to remove from blacklist + */ + function removeBlackAccount(address blackAccount) external { + require(msg.sender == GUARDIAN, "only guardian can remove black account"); + uint256 position = _blackListMap[blackAccount]; + if (position > 0) { + uint256 indexOf = position - 1; + if (_blackList.length > 1 && indexOf != _blackList.length - 1) { + address lastAddress = _blackList[_blackList.length - 1]; + _blackList[indexOf] = lastAddress; + _blackListMap[lastAddress] = indexOf + 1; + } + _blackList.pop(); + delete _blackListMap[blackAccount]; + emit blackAccountRemoved(blackAccount); + } + } + + /** + * @notice get black account list + * + * @return black account list + */ + function getBlackListList() external view returns (address[] memory) { + return _blackList; + } + + /** + * @notice get black account list + * + * @return Whether or not the address is black account + */ + function isBlackAccount(address blackAccount) public view returns (bool) { + return _blackListMap[blackAccount] != 0; + } +}