diff --git a/packages/contracts/.gas-snapshot b/packages/contracts/.gas-snapshot index 8a288b6a..597e12f9 100644 --- a/packages/contracts/.gas-snapshot +++ b/packages/contracts/.gas-snapshot @@ -1,7 +1,7 @@ DepositFeedTest:test_depositTransaction_ContractCreationReverts() (gas: 9290) DepositFeedTest:test_depositTransaction_NoValueContract() (gas: 24388) DepositFeedTest:test_depositTransaction_NoValueEOA() (gas: 24734) -DepositFeedTest:test_depositTransaction_createWithZeroValueForContract() (gas: 24451) +DepositFeedTest:test_depositTransaction_createWithZeroValueForContract() (gas: 24384) DepositFeedTest:test_depositTransaction_createWithZeroValueForEOA() (gas: 24707) DepositFeedTest:test_depositTransaction_withEthValueAndContractContractCreation() (gas: 31429) DepositFeedTest:test_depositTransaction_withEthValueAndEOAContractCreation() (gas: 22859) @@ -13,25 +13,39 @@ L1BLockTest:test_number() (gas: 7555) L1BLockTest:test_sequenceNumber() (gas: 7577) L1BLockTest:test_timestamp() (gas: 7587) L1CrossDomainMessenger_Test:testCannot_pause() (gas: 10843) -L1CrossDomainMessenger_Test:test_blockAndUnblockSuccessfulMessage() (gas: 105412) -L1CrossDomainMessenger_Test:test_pause() (gas: 31793) +L1CrossDomainMessenger_Test:test_blockAndUnblockSuccessfulMessage() (gas: 105390) +L1CrossDomainMessenger_Test:test_pause() (gas: 31771) L1CrossDomainMessenger_Test:test_relayMessageBlockingAuth() (gas: 18542) L1CrossDomainMessenger_Test:test_relayMessageInsideFraudProofWindow() (gas: 12305) L1CrossDomainMessenger_Test:test_relayMessageSucceeds() (gas: 82425) -L1CrossDomainMessenger_Test:test_relayMessageToSystemContract() (gas: 30160) +L1CrossDomainMessenger_Test:test_relayMessageToSystemContract() (gas: 30138) L1CrossDomainMessenger_Test:test_relayRevertOnBlockedMessage() (gas: 55761) L1CrossDomainMessenger_Test:test_relayShouldRevertIfPaused() (gas: 41560) L1CrossDomainMessenger_Test:test_relayShouldRevertSendingSameMessageTwice() (gas: 188) L1CrossDomainMessenger_Test:test_revertOnInvalidOutputRootProof() (gas: 16067) L1CrossDomainMessenger_Test:test_sendMessage() (gas: 44041) -L1CrossDomainMessenger_Test:test_sendMessageTwice() (gas: 49061) +L1CrossDomainMessenger_Test:test_sendMessageTwice() (gas: 49039) L1CrossDomainMessenger_Test:test_xDomainMessageSenderResets() (gas: 81565) -L1StandardBridge_Test:test_L1BridgeSetsPortalAndL2Bridge() (gas: 14825) +L1StandardBridge_Test:test_L1BridgeDepositERC20() (gas: 338228) +L1StandardBridge_Test:test_L1BridgeDepositERC20To() (gas: 340083) +L1StandardBridge_Test:test_L1BridgeDepositETH() (gas: 50372) +L1StandardBridge_Test:test_L1BridgeDepositETHTo() (gas: 52608) +L1StandardBridge_Test:test_L1BridgeDonateETH() (gas: 17478) +L1StandardBridge_Test:test_L1BridgeFinalizeERC20Withdrawal() (gas: 434878) +L1StandardBridge_Test:test_L1BridgeFinalizeETHWithdrawal() (gas: 43863) +L1StandardBridge_Test:test_L1BridgeOnlyEOADepositERC20() (gas: 29789) +L1StandardBridge_Test:test_L1BridgeOnlyEOADepositETH() (gas: 35476) +L1StandardBridge_Test:test_L1BridgeOnlyL2BridgeFinalizeERC20Withdrawal() (gas: 25010) +L1StandardBridge_Test:test_L1BridgeOnlyL2BridgeFinalizeETHWithdrawal() (gas: 27791) +L1StandardBridge_Test:test_L1BridgeOnlyPortalFinalizeERC20Withdrawal() (gas: 17585) +L1StandardBridge_Test:test_L1BridgeOnlyPortalFinalizeETHWithdrawal() (gas: 19901) +L1StandardBridge_Test:test_L1BridgeReceiveETH() (gas: 48287) +L1StandardBridge_Test:test_L1BridgeSetsPortalAndL2Bridge() (gas: 14892) L2CrossDomainMessenger_Test:test_L2MessengerCallsTarget() (gas: 64095) L2CrossDomainMessenger_Test:test_L2MessengerCannotCallL2MessagePasser() (gas: 42128) L2CrossDomainMessenger_Test:test_L2MessengerCannotRelaySameMessageTwice() (gas: 67491) L2CrossDomainMessenger_Test:test_L2MessengerCorrectL1Messenger() (gas: 9762) -L2CrossDomainMessenger_Test:test_L2MessengerRevertInvalidL1XDomainMessenger() (gas: 11535) +L2CrossDomainMessenger_Test:test_L2MessengerRevertInvalidL1XDomainMessenger() (gas: 11557) L2CrossDomainMessenger_Test:test_L2MessengerSendMessage() (gas: 122584) L2CrossDomainMessenger_Test:test_L2MessengerSendSameMessageTwice() (gas: 162903) L2CrossDomainMessenger_Test:test_L2MessengerXDomainMessageSenderReset() (gas: 69285) @@ -41,21 +55,31 @@ L2OutputOracleTest:testCannot_appendEmptyOutput() (gas: 16724) L2OutputOracleTest:testCannot_appendFutureTimestamp() (gas: 18642) L2OutputOracleTest:testCannot_appendOutputIfNotSequencer() (gas: 16088) L2OutputOracleTest:testCannot_appendUnexpectedTimestamp() (gas: 18893) -L2OutputOracleTest:testCannot_computePreHistoricalL2BlockNumber() (gas: 11048) +L2OutputOracleTest:testCannot_computePreHistoricalL2BlockNumber() (gas: 11070) L2OutputOracleTest:testCannot_deleteL2Output_ifNotSequencer() (gas: 16287) L2OutputOracleTest:testCannot_deleteL2Output_ifWrongOutput() (gas: 23243) L2OutputOracleTest:test_appendingAnotherOutput() (gas: 47332) L2OutputOracleTest:test_computeL2BlockNumber() (gas: 15003) L2OutputOracleTest:test_deleteL2Output() (gas: 30647) -L2OutputOracleTest:test_getL2Output() (gas: 15071) +L2OutputOracleTest:test_getL2Output() (gas: 15093) L2OutputOracleTest:test_latestBlockTimestamp() (gas: 9699) -L2OutputOracleTest:test_nextTimestamp() (gas: 12031) +L2OutputOracleTest:test_nextTimestamp() (gas: 12053) L2OutputOracleTest_Constructor:test_constructor() (gas: 29173) -L2StandardBridge_Test:test_L2BridgeCorrectL1Bridge() (gas: 9726) +L2StandardBridge_Test:test_L2BridgeBadDeposit() (gas: 89330) +L2StandardBridge_Test:test_L2BridgeCorrectL1Bridge() (gas: 9727) +L2StandardBridge_Test:test_L2BridgeFinalizeDeposit() (gas: 56204) +L2StandardBridge_Test:test_L2BridgeFinalizeDepositRevertsOnCaller() (gas: 13753) +L2StandardBridge_Test:test_L2BridgeFinalizeETHDeposit() (gas: 39194) +L2StandardBridge_Test:test_L2BridgeFinalizeETHDepositWrongAmount() (gas: 100057) +L2StandardBridge_Test:test_L2BridgeRevertWithdraw() (gas: 20447) +L2StandardBridge_Test:test_L2BridgeRevertWithdrawETH() (gas: 11632) +L2StandardBridge_Test:test_L2BridgeWithdraw() (gas: 111806) +L2StandardBridge_Test:test_L2BridgeWithdrawETH() (gas: 95606) +L2StandardBridge_Test:test_L2BridgeWithdrawTo() (gas: 114318) OptimismPortal_Test:test_receive_withEthValueFromEOA() (gas: 21979) -WithdrawalsRelay_finalizeWithdrawalTransaction_Test:test_cannotVerifyInvalidProof() (gas: 37418) -WithdrawalsRelay_finalizeWithdrawalTransaction_Test:test_cannotVerifyRecentWithdrawal() (gas: 33219) -WithdrawalsRelay_finalizeWithdrawalTransaction_Test:test_verifyWithdrawal() (gas: 190048) +WithdrawalsRelay_finalizeWithdrawalTransaction_Test:test_cannotVerifyInvalidProof() (gas: 37415) +WithdrawalsRelay_finalizeWithdrawalTransaction_Test:test_cannotVerifyRecentWithdrawal() (gas: 33241) +WithdrawalsRelay_finalizeWithdrawalTransaction_Test:test_verifyWithdrawal() (gas: 190003) WithdawerBurnTest:test_burn() (gas: 50276) WithdrawerTestInitiateWithdrawal:test_initiateWithdrawal_fromContract() (gas: 71990) WithdrawerTestInitiateWithdrawal:test_initiateWithdrawal_fromEOA() (gas: 72475) diff --git a/packages/contracts/contracts/L1/messaging/IL1ERC20Bridge.sol b/packages/contracts/contracts/L1/messaging/IL1ERC20Bridge.sol new file mode 100644 index 00000000..f8d6680e --- /dev/null +++ b/packages/contracts/contracts/L1/messaging/IL1ERC20Bridge.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: MIT +pragma solidity >0.5.0 <0.9.0; + +/** + * @title IL1ERC20Bridge + */ +interface IL1ERC20Bridge { + /********** + * Events * + **********/ + + event ERC20DepositInitiated( + address indexed _l1Token, + address indexed _l2Token, + address indexed _from, + address _to, + uint256 _amount, + bytes _data + ); + + event ERC20WithdrawalFinalized( + address indexed _l1Token, + address indexed _l2Token, + address indexed _from, + address _to, + uint256 _amount, + bytes _data + ); + + /******************** + * Public Functions * + ********************/ + + /** + * @dev get the address of the corresponding L2 bridge contract. + * @return Address of the corresponding L2 bridge contract. + */ + function l2TokenBridge() external returns (address); + + /** + * @dev deposit an amount of the ERC20 to the caller's balance on L2. + * @param _l1Token Address of the L1 ERC20 we are depositing + * @param _l2Token Address of the L1 respective L2 ERC20 + * @param _amount Amount of the ERC20 to deposit + * @param _l2Gas Gas limit required to complete the deposit on L2. + * @param _data Optional data to forward to L2. This data is provided + * solely as a convenience for external contracts. Aside from enforcing a maximum + * length, these contracts provide no guarantees about its content. + */ + function depositERC20( + address _l1Token, + address _l2Token, + uint256 _amount, + uint32 _l2Gas, + bytes calldata _data + ) external; + + /** + * @dev deposit an amount of ERC20 to a recipient's balance on L2. + * @param _l1Token Address of the L1 ERC20 we are depositing + * @param _l2Token Address of the L1 respective L2 ERC20 + * @param _to L2 address to credit the withdrawal to. + * @param _amount Amount of the ERC20 to deposit. + * @param _l2Gas Gas limit required to complete the deposit on L2. + * @param _data Optional data to forward to L2. This data is provided + * solely as a convenience for external contracts. Aside from enforcing a maximum + * length, these contracts provide no guarantees about its content. + */ + function depositERC20To( + address _l1Token, + address _l2Token, + address _to, + uint256 _amount, + uint32 _l2Gas, + bytes calldata _data + ) external; + + /************************* + * Cross-chain Functions * + *************************/ + + /** + * @dev Complete a withdrawal from L2 to L1, and credit funds to the recipient's balance of the + * L1 ERC20 token. + * This call will fail if the initialized withdrawal from L2 has not been finalized. + * + * @param _l1Token Address of L1 token to finalizeWithdrawal for. + * @param _l2Token Address of L2 token where withdrawal was initiated. + * @param _from L2 address initiating the transfer. + * @param _to L1 address to credit the withdrawal to. + * @param _amount Amount of the ERC20 to deposit. + * @param _data Data provided by the sender on L2. This data is provided + * solely as a convenience for external contracts. Aside from enforcing a maximum + * length, these contracts provide no guarantees about its content. + */ + function finalizeERC20Withdrawal( + address _l1Token, + address _l2Token, + address _from, + address _to, + uint256 _amount, + bytes calldata _data + ) external; +} diff --git a/packages/contracts/contracts/L1/messaging/IL1StandardBridge.sol b/packages/contracts/contracts/L1/messaging/IL1StandardBridge.sol new file mode 100644 index 00000000..a12d033c --- /dev/null +++ b/packages/contracts/contracts/L1/messaging/IL1StandardBridge.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +pragma solidity >0.5.0 <0.9.0; + +import "./IL1ERC20Bridge.sol"; + +/** + * @title IL1StandardBridge + */ +interface IL1StandardBridge is IL1ERC20Bridge { + /********** + * Events * + **********/ + event ETHDepositInitiated( + address indexed _from, + address indexed _to, + uint256 _amount, + bytes _data + ); + + event ETHWithdrawalFinalized( + address indexed _from, + address indexed _to, + uint256 _amount, + bytes _data + ); + + /******************** + * Public Functions * + ********************/ + + /** + * @dev Deposit an amount of the ETH to the caller's balance on L2. + * @param _l2Gas Gas limit required to complete the deposit on L2. + * @param _data Optional data to forward to L2. This data is provided + * solely as a convenience for external contracts. Aside from enforcing a maximum + * length, these contracts provide no guarantees about its content. + */ + function depositETH(uint32 _l2Gas, bytes calldata _data) external payable; + + /** + * @dev Deposit an amount of ETH to a recipient's balance on L2. + * @param _to L2 address to credit the withdrawal to. + * @param _l2Gas Gas limit required to complete the deposit on L2. + * @param _data Optional data to forward to L2. This data is provided + * solely as a convenience for external contracts. Aside from enforcing a maximum + * length, these contracts provide no guarantees about its content. + */ + function depositETHTo( + address _to, + uint32 _l2Gas, + bytes calldata _data + ) external payable; + + /************************* + * Cross-chain Functions * + *************************/ + + /** + * @dev Complete a withdrawal from L2 to L1, and credit funds to the recipient's balance of the + * L1 ETH token. Since only the xDomainMessenger can call this function, it will never be called + * before the withdrawal is finalized. + * @param _from L2 address initiating the transfer. + * @param _to L1 address to credit the withdrawal to. + * @param _amount Amount of the ERC20 to deposit. + * @param _data Optional data to forward to L2. This data is provided + * solely as a convenience for external contracts. Aside from enforcing a maximum + * length, these contracts provide no guarantees about its content. + */ + function finalizeETHWithdrawal( + address _from, + address _to, + uint256 _amount, + bytes calldata _data + ) external payable; +} diff --git a/packages/contracts/contracts/L1/messaging/L1CrossDomainMessenger.sol b/packages/contracts/contracts/L1/messaging/L1CrossDomainMessenger.sol index 910880a5..8edcb6e1 100644 --- a/packages/contracts/contracts/L1/messaging/L1CrossDomainMessenger.sol +++ b/packages/contracts/contracts/L1/messaging/L1CrossDomainMessenger.sol @@ -38,9 +38,7 @@ import { /** * @title L1CrossDomainMessenger * @dev The L1 Cross Domain Messenger contract sends messages from L1 to L2, and relays messages - * from L2 onto L1. In the event that a message sent from L1 to L2 is rejected for exceeding the L2 - * epoch gas limit, it can be resubmitted via this contract's replay function. - * + * from L2 onto L1. */ contract L1CrossDomainMessenger is IL1CrossDomainMessenger, diff --git a/packages/contracts/contracts/L1/messaging/L1StandardBridge.sol b/packages/contracts/contracts/L1/messaging/L1StandardBridge.sol index 2cb22988..9db8d715 100644 --- a/packages/contracts/contracts/L1/messaging/L1StandardBridge.sol +++ b/packages/contracts/contracts/L1/messaging/L1StandardBridge.sol @@ -2,11 +2,11 @@ pragma solidity ^0.8.9; /* Interface Imports */ -import { IL1StandardBridge } from "@eth-optimism/contracts/L1/messaging/IL1StandardBridge.sol"; -import { IL1ERC20Bridge } from "@eth-optimism/contracts/L1/messaging/IL1ERC20Bridge.sol"; import { IL2ERC20Bridge } from "@eth-optimism/contracts/L2/messaging/IL2ERC20Bridge.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { OptimismPortal } from "../OptimismPortal.sol"; +import { IL1StandardBridge } from "./IL1StandardBridge.sol"; +import { IL1ERC20Bridge } from "./IL1ERC20Bridge.sol"; /* Library Imports */ import { @@ -77,14 +77,14 @@ contract L1StandardBridge is IL1StandardBridge { * default amount is forwarded to L2. */ receive() external payable onlyEOA { - _initiateETHDeposit(msg.sender, msg.sender, 200_000, bytes("")); + _initiateETHDeposit(msg.sender, msg.sender, msg.value, 200_000, bytes("")); } /** * @inheritdoc IL1StandardBridge */ function depositETH(uint32 _l2Gas, bytes calldata _data) external payable onlyEOA { - _initiateETHDeposit(msg.sender, msg.sender, _l2Gas, _data); + _initiateETHDeposit(msg.sender, msg.sender, msg.value, _l2Gas, _data); } /** @@ -95,7 +95,7 @@ contract L1StandardBridge is IL1StandardBridge { uint32 _l2Gas, bytes calldata _data ) external payable { - _initiateETHDeposit(msg.sender, _to, _l2Gas, _data); + _initiateETHDeposit(msg.sender, _to, msg.value, _l2Gas, _data); } /** @@ -111,13 +111,28 @@ contract L1StandardBridge is IL1StandardBridge { function _initiateETHDeposit( address _from, address _to, + uint256 _amount, uint32 _l2Gas, bytes memory _data ) internal { - emit ETHDepositInitiated(_from, _to, msg.value, _data); + emit ETHDepositInitiated(_from, _to, _amount, _data); // Send calldata into L2 - optimismPortal.depositTransaction{ value: msg.value }(_to, msg.value, _l2Gas, false, _data); + optimismPortal.depositTransaction{ value: _amount }( + Lib_PredeployAddresses.L2_STANDARD_BRIDGE, + _amount, + _l2Gas, + false, + abi.encodeWithSelector( + IL2ERC20Bridge.finalizeDeposit.selector, + address(0), + Lib_PredeployAddresses.OVM_ETH, + _from, + _to, + _amount, + _data + ) + ); } /** @@ -231,7 +246,7 @@ contract L1StandardBridge is IL1StandardBridge { address _to, uint256 _amount, bytes calldata _data - ) external onlyL2Bridge { + ) external payable onlyL2Bridge { emit ETHWithdrawalFinalized(_from, _to, _amount, _data); (bool success, ) = _to.call{ value: _amount }(new bytes(0)); diff --git a/packages/contracts/contracts/L2/messaging/IL2ERC20Bridge.sol b/packages/contracts/contracts/L2/messaging/IL2ERC20Bridge.sol index 3fa345d6..8d3fcad2 100644 --- a/packages/contracts/contracts/L2/messaging/IL2ERC20Bridge.sol +++ b/packages/contracts/contracts/L2/messaging/IL2ERC20Bridge.sol @@ -60,7 +60,7 @@ interface IL2ERC20Bridge { uint256 _amount, uint32 _l1Gas, bytes calldata _data - ) external; + ) external payable; /** * @dev initiate a withdraw of some token to a recipient's account on L1. @@ -78,7 +78,7 @@ interface IL2ERC20Bridge { uint256 _amount, uint32 _l1Gas, bytes calldata _data - ) external; + ) external payable; /************************* * Cross-chain Functions * @@ -104,5 +104,5 @@ interface IL2ERC20Bridge { address _to, uint256 _amount, bytes calldata _data - ) external; + ) external payable; } diff --git a/packages/contracts/contracts/L2/messaging/IL2StandardBridge.sol b/packages/contracts/contracts/L2/messaging/IL2StandardBridge.sol new file mode 100644 index 00000000..74f5e909 --- /dev/null +++ b/packages/contracts/contracts/L2/messaging/IL2StandardBridge.sol @@ -0,0 +1,57 @@ +pragma solidity ^0.8.10; + +// TODO: this should be removed as its a duplicate +// of IL2ERC20Bridge. I believe its only used +// in tests now +interface IL2StandardBridge { + event DepositFailed( + address indexed _l1Token, + address indexed _l2Token, + address indexed _from, + address _to, + uint256 _amount, + bytes _data + ); + event DepositFinalized( + address indexed _l1Token, + address indexed _l2Token, + address indexed _from, + address _to, + uint256 _amount, + bytes _data + ); + event WithdrawalInitiated( + address indexed _l1Token, + address indexed _l2Token, + address indexed _from, + address _to, + uint256 _amount, + bytes _data + ); + + function finalizeDeposit( + address _l1Token, + address _l2Token, + address _from, + address _to, + uint256 _amount, + bytes memory _data + ) external payable; + + function l1TokenBridge() external view returns (address); + + function withdraw( + address _l2Token, + uint256 _amount, + uint32 _l1Gas, + bytes memory _data + ) external payable; + + function withdrawTo( + address _l2Token, + address _to, + uint256 _amount, + uint32 _l1Gas, + bytes memory _data + ) external payable; +} diff --git a/packages/contracts/contracts/L2/messaging/L2StandardBridge.sol b/packages/contracts/contracts/L2/messaging/L2StandardBridge.sol index b3671f30..68d01115 100644 --- a/packages/contracts/contracts/L2/messaging/L2StandardBridge.sol +++ b/packages/contracts/contracts/L2/messaging/L2StandardBridge.sol @@ -2,9 +2,9 @@ pragma solidity ^0.8.9; /* Interface Imports */ -import { IL1StandardBridge } from "@eth-optimism/contracts/L1/messaging/IL1StandardBridge.sol"; import { IL1ERC20Bridge } from "@eth-optimism/contracts/L1/messaging/IL1ERC20Bridge.sol"; import { IL2ERC20Bridge } from "./IL2ERC20Bridge.sol"; +import { IL1StandardBridge } from "../../L1/messaging/IL1StandardBridge.sol"; /* Library Imports */ import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; @@ -28,6 +28,13 @@ import { Withdrawer } from "../Withdrawer.sol"; * bridge to release L1 funds. */ contract L2StandardBridge is IL2ERC20Bridge { + /********** + * Errors * + **********/ + + /// @notice Represents invalid value handling to prevent stuck ETH + error InvalidWithdrawalAmount(); + /******************************** * External Contract References * ********************************/ @@ -57,7 +64,7 @@ contract L2StandardBridge is IL2ERC20Bridge { uint256 _amount, uint32 _l1Gas, bytes calldata _data - ) external virtual { + ) external payable virtual { _initiateWithdrawal(_l2Token, msg.sender, msg.sender, _amount, _l1Gas, _data); } @@ -70,10 +77,86 @@ contract L2StandardBridge is IL2ERC20Bridge { uint256 _amount, uint32 _l1Gas, bytes calldata _data - ) external virtual { + ) external payable virtual { _initiateWithdrawal(_l2Token, msg.sender, _to, _amount, _l1Gas, _data); } + function withdrawETH() external payable { + _initiateETHWithdrawal(msg.sender, msg.sender, msg.value, 30000, hex""); + } + + function withdrawETHTo( + address _to, + uint256 _l1Gas, + bytes calldata _data + ) external payable { + _initiateETHWithdrawal(msg.sender, _to, msg.value, _l1Gas, _data); + } + + function _initiateETHWithdrawal( + address _from, + address _to, + uint256 _amount, + uint256 _l1Gas, + bytes memory _data + ) internal { + // Send message up to L1 bridge + Withdrawer(Lib_BedrockPredeployAddresses.WITHDRAWER).initiateWithdrawal{ value: _amount }( + l1TokenBridge, + _l1Gas, + abi.encodeWithSelector( + IL1StandardBridge.finalizeETHWithdrawal.selector, + _from, + _to, + _amount, + _data + ) + ); + + emit WithdrawalInitiated( + address(0), + Lib_PredeployAddresses.OVM_ETH, + msg.sender, + _to, + _amount, + _data + ); + } + + function _initiateERC20Withdrawal( + address _l2Token, + address _from, + address _to, + uint256 _amount, + uint32 _l1Gas, + bytes calldata _data + ) internal { + // When a withdrawal is initiated, we burn the withdrawer's funds to prevent + // subsequent L2 usage + // slither-disable-next-line reentrancy-events + IL2StandardERC20(_l2Token).burn(msg.sender, _amount); + + // slither-disable-next-line reentrancy-events + address l1Token = IL2StandardERC20(_l2Token).l1Token(); + + // Send message up to L1 bridge + Withdrawer(Lib_BedrockPredeployAddresses.WITHDRAWER).initiateWithdrawal( + l1TokenBridge, + _l1Gas, + abi.encodeWithSelector( + IL1ERC20Bridge.finalizeERC20Withdrawal.selector, + l1Token, + _l2Token, + _from, + _to, + _amount, + _data + ) + ); + // slither-disable-next-line reentrancy-events + emit WithdrawalInitiated(l1Token, _l2Token, msg.sender, _to, _amount, _data); + } + /** * @dev Performs the logic for withdrawals by burning the token and informing * the L1 token Gateway of the withdrawal. @@ -94,33 +177,19 @@ contract L2StandardBridge is IL2ERC20Bridge { uint32 _l1Gas, bytes calldata _data ) internal { - // When a withdrawal is initiated, we burn the withdrawer's funds to prevent subsequent L2 - // usage - // slither-disable-next-line reentrancy-events - IL2StandardERC20(_l2Token).burn(msg.sender, _amount); + if (_l2Token == Lib_PredeployAddresses.OVM_ETH) { + if (msg.value != _amount) { + revert InvalidWithdrawalAmount(); + } - // Construct calldata for l1TokenBridge.finalizeERC20Withdrawal(_to, _amount) - // slither-disable-next-line reentrancy-events - address l1Token = IL2StandardERC20(_l2Token).l1Token(); - bytes memory message = abi.encodeWithSelector( - IL1ERC20Bridge.finalizeERC20Withdrawal.selector, - l1Token, - _l2Token, - _from, - _to, - _amount, - _data - ); - - // slither-disable-next-line reentrancy-events - emit WithdrawalInitiated(l1Token, _l2Token, msg.sender, _to, _amount, _data); + _initiateETHWithdrawal(_from, _to, _amount, _l1Gas, _data); + } else { + if (msg.value != 0) { + revert InvalidWithdrawalAmount(); + } - // Send message up to L1 bridge - Withdrawer(Lib_BedrockPredeployAddresses.WITHDRAWER).initiateWithdrawal( - l1TokenBridge, - _l1Gas, - message - ); + _initiateERC20Withdrawal(_l2Token, _from, _to, _amount, _l1Gas, _data); + } } /************************************ @@ -130,6 +199,7 @@ contract L2StandardBridge is IL2ERC20Bridge { /** * @inheritdoc IL2ERC20Bridge */ + function finalizeDeposit( address _l1Token, address _l2Token, @@ -137,7 +207,7 @@ contract L2StandardBridge is IL2ERC20Bridge { address _to, uint256 _amount, bytes calldata _data - ) external virtual { + ) external payable virtual { // Since it is impossible to deploy a contract to an address on L2 which matches // the alias of the l1TokenBridge, this check can only pass when it is called in // the first call frame of a deposit transaction. Thus reentrancy is prevented here. @@ -146,7 +216,21 @@ contract L2StandardBridge is IL2ERC20Bridge { "Can only be called by a the l1TokenBridge" ); + // Check to see if the bridge is being used to deposit ETH. + // The `msg.value` must match the `_amount` to prevent + // ETH from getting stuck in the contract if ( + _l1Token == address(0) && + _l2Token == Lib_PredeployAddresses.OVM_ETH && + msg.value == _amount + ) { + // An ETH deposit is being made via the Token Bridge. + // We simply forward it on. If this call fails, ETH will be stuck, but the L1Bridge + // uses onlyEOA on the receive function, so anyone sending to a contract knows + // what they are doing. + address(_to).call{ value: _amount }(hex""); + emit DepositFinalized(_l1Token, _l2Token, _from, _to, _amount, _data); + } else if ( // Check the target token is compliant and // verify the deposited token on L1 matches the L2 deposited token representation here // slither-disable-next-line reentrancy-events @@ -168,24 +252,40 @@ contract L2StandardBridge is IL2ERC20Bridge { // message so that users can get their funds out in some cases. // There is no way to prevent malicious token contracts altogether, but this does limit // user error and mitigate some forms of malicious contract behavior. - bytes memory message = abi.encodeWithSelector( - IL1ERC20Bridge.finalizeERC20Withdrawal.selector, - _l1Token, - _l2Token, - _to, // switched the _to and _from here to bounce back the deposit to the sender - _from, - _amount, - _data - ); emit DepositFailed(_l1Token, _l2Token, _from, _to, _amount, _data); - // Send message up to L1 bridge - Withdrawer(Lib_BedrockPredeployAddresses.WITHDRAWER).initiateWithdrawal( - l1TokenBridge, - 0, - message - ); + // Withdraw ETH in the case that the user submitted a bad ETH + // deposit to prevent ETH from getting stuck + if (_l1Token == address(0) && _l2Token == Lib_PredeployAddresses.OVM_ETH) { + Withdrawer(Lib_BedrockPredeployAddresses.WITHDRAWER).initiateWithdrawal{ + value: msg.value + }( + l1TokenBridge, + 0, // TODO: does a 0 gaslimit work here? + abi.encodeWithSelector( + IL1StandardBridge.finalizeETHWithdrawal.selector, + _to, // switch the _to and _from to send deposit back to the sender + _from, + msg.value, + _data + ) + ); + } else { + Withdrawer(Lib_BedrockPredeployAddresses.WITHDRAWER).initiateWithdrawal( + l1TokenBridge, + 0, // TODO: does a 0 gaslimit work here? + abi.encodeWithSelector( + IL1ERC20Bridge.finalizeERC20Withdrawal.selector, + _l1Token, + _l2Token, + _to, // switch the _to and _from to send deposit back to the sender + _from, + _amount, + _data + ) + ); + } } } } diff --git a/packages/contracts/contracts/test/CommonTest.t.sol b/packages/contracts/contracts/test/CommonTest.t.sol index f3442f14..76e2fa93 100644 --- a/packages/contracts/contracts/test/CommonTest.t.sol +++ b/packages/contracts/contracts/test/CommonTest.t.sol @@ -3,9 +3,11 @@ pragma solidity 0.8.10; /* Testing utilities */ import { Test } from "forge-std/Test.sol"; -import { Vm } from "forge-std/Vm.sol"; contract CommonTest is Test { + address alice = address(128); + address bob = address(256); + address immutable ZERO_ADDRESS = address(0); address immutable NON_ZERO_ADDRESS = address(1); uint256 immutable NON_ZERO_VALUE = 100; diff --git a/packages/contracts/contracts/test/L1CrossDomainMessenger.t.sol b/packages/contracts/contracts/test/L1CrossDomainMessenger.t.sol index bd86d37c..4002853f 100644 --- a/packages/contracts/contracts/test/L1CrossDomainMessenger.t.sol +++ b/packages/contracts/contracts/test/L1CrossDomainMessenger.t.sol @@ -241,9 +241,11 @@ contract L1CrossDomainMessenger_Test is CommonTest, L2OutputOracle_Initializer { } // relayMessage: should revert if trying to send the same message twice + /* function test_relayShouldRevertSendingSameMessageTwice() external { // TODO: this is a test on the L2CrossDomainMessenger } + */ // relayMessage: should revert if paused function test_relayShouldRevertIfPaused() external { diff --git a/packages/contracts/contracts/test/L1StandardBridge.t.sol b/packages/contracts/contracts/test/L1StandardBridge.t.sol index 89e709c5..fe950d3c 100644 --- a/packages/contracts/contracts/test/L1StandardBridge.t.sol +++ b/packages/contracts/contracts/test/L1StandardBridge.t.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.10; import { Lib_PredeployAddresses } from "@eth-optimism/contracts/libraries/constants/Lib_PredeployAddresses.sol"; +import { IL2ERC20Bridge } from "@eth-optimism/contracts/L2/messaging/IL2ERC20Bridge.sol"; import { IWithdrawer } from "../L2/IWithdrawer.sol"; import { Withdrawer } from "../L2/Withdrawer.sol"; @@ -18,46 +19,13 @@ import { IL2StandardERC20 } from "../L2/tokens/IL2StandardERC20.sol"; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { CommonTest } from "./CommonTest.t.sol"; -import { L2OutputOracle_Initializer } from "./L2OutputOracle.t.sol"; +import { L2OutputOracle_Initializer, BridgeInitializer } from "./L2OutputOracle.t.sol"; import { LibRLP } from "./Lib_RLP.t.sol"; -contract L1StandardBridge_Test is CommonTest, L2OutputOracle_Initializer { - OptimismPortal op; +import { stdStorage, StdStorage } from "forge-std/Test.sol"; - IWithdrawer W; - L1StandardBridge L1Bridge; - L2StandardBridge L2Bridge; - IL2StandardTokenFactory L2TokenFactory; - IL2StandardERC20 L2Token; - - function setUp() external { - L1Bridge = new L1StandardBridge(); - L2Bridge = new L2StandardBridge(address(L1Bridge)); - op = new OptimismPortal(oracle, 100); - - L1Bridge.initialize(op, address(L2Bridge)); - - Withdrawer w = new Withdrawer(); - vm.etch(Lib_BedrockPredeployAddresses.WITHDRAWER, address(w).code); - W = IWithdrawer(Lib_BedrockPredeployAddresses.WITHDRAWER); - - L2StandardTokenFactory factory = new L2StandardTokenFactory(); - vm.etch(Lib_PredeployAddresses.L2_STANDARD_TOKEN_FACTORY, address(factory).code); - L2TokenFactory = IL2StandardTokenFactory(Lib_PredeployAddresses.L2_STANDARD_TOKEN_FACTORY); - - ERC20 token = new ERC20("Test Token", "TT"); - - // Deploy the L2 ERC20 now - L2TokenFactory.createStandardL2Token( - address(token), - string(abi.encodePacked("L2-", token.name())), - string(abi.encodePacked("L2-", token.symbol())) - ); - - L2Token = IL2StandardERC20( - LibRLP.computeAddress(address(L2TokenFactory), 0) - ); - } +contract L1StandardBridge_Test is CommonTest, BridgeInitializer { + using stdStorage for StdStorage; function test_L1BridgeSetsPortalAndL2Bridge() external { OptimismPortal portal = L1Bridge.optimismPortal(); @@ -69,34 +37,393 @@ contract L1StandardBridge_Test is CommonTest, L2OutputOracle_Initializer { // receive // - can accept ETH + function test_L1BridgeReceiveETH() external { + vm.expectEmit(true, true, true, true); + emit ETHDepositInitiated(alice, alice, 100, hex""); + + vm.expectCall( + address(op), + abi.encodeWithSelector( + op.depositTransaction.selector, + Lib_PredeployAddresses.L2_STANDARD_BRIDGE, + 100, + 200000, + false, + abi.encodeWithSelector( + IL2ERC20Bridge.finalizeDeposit.selector, + address(0), + Lib_PredeployAddresses.OVM_ETH, + alice, + alice, + 100, + hex"" + ) + ) + ); + + vm.prank(alice); + (bool success, bytes memory data) = address(L1Bridge).call{ value: 100 }(hex""); + assertEq(success, true); + assertEq(data, hex""); + assertEq(address(op).balance, 100); + } + // depositETH // - emits ETHDepositInitiated // - calls optimismPortal.depositTransaction // - only EOA // - ETH ends up in the optimismPortal + function test_L1BridgeDepositETH() external { + vm.expectEmit(true, true, true, true); + emit ETHDepositInitiated(alice, alice, 1000, hex"ff"); + + vm.expectCall( + address(op), + abi.encodeWithSelector( + op.depositTransaction.selector, + Lib_PredeployAddresses.L2_STANDARD_BRIDGE, + 1000, + 10000, + false, + abi.encodeWithSelector( + IL2ERC20Bridge.finalizeDeposit.selector, + address(0), + Lib_PredeployAddresses.OVM_ETH, + alice, + alice, + 1000, + hex"ff" + ) + ) + ); + + vm.prank(alice); + L1Bridge.depositETH{ value: 1000 }(10000, hex"ff"); + assertEq(address(op).balance, 1000); + } + + function test_L1BridgeOnlyEOADepositETH() external { + vm.etch(alice, address(token).code); + + vm.expectRevert("Account not EOA"); + vm.prank(alice); + L1Bridge.depositETH{ value: 1000 }(10000, hex"ff"); + assertEq(address(op).balance, 0); + } + // depositETHTo // - emits ETHDepositInitiated // - calls optimismPortal.depositTransaction // - EOA or contract can call // - ETH ends up in the optimismPortal + function test_L1BridgeDepositETHTo() external { + vm.expectEmit(true, true, true, true); + emit ETHDepositInitiated(alice, bob, 1000, hex"ff"); + + vm.expectCall( + address(op), + abi.encodeWithSelector( + op.depositTransaction.selector, + Lib_PredeployAddresses.L2_STANDARD_BRIDGE, + 1000, + 10000, + false, + abi.encodeWithSelector( + IL2ERC20Bridge.finalizeDeposit.selector, + address(0), + Lib_PredeployAddresses.OVM_ETH, + alice, + bob, + 1000, + hex"ff" + ) + ) + ); + + vm.prank(alice); + L1Bridge.depositETHTo{ value: 1000 }(bob, 10000, hex"ff"); + assertEq(address(op).balance, 1000); + } + // depositERC20 // - updates bridge.deposits // - emits ERC20DepositInitiated // - calls optimismPortal.depositTransaction // - only callable by EOA + function test_L1BridgeDepositERC20() external { + vm.expectEmit(true, true, true, true); + emit ERC20DepositInitiated( + address(token), + address(L2Token), + alice, + alice, + 100, + hex"" + ); + + deal(address(token), alice, 100000, true); + + vm.prank(alice); + token.approve(address(L1Bridge), type(uint256).max); + + vm.expectCall( + address(token), + abi.encodeWithSelector( + ERC20.transferFrom.selector, + alice, + address(L1Bridge), + 100 + ) + ); + + vm.expectCall( + address(op), + abi.encodeWithSelector( + op.depositTransaction.selector, + Lib_PredeployAddresses.L2_STANDARD_BRIDGE, + 0, + 10000, + false, + abi.encodeWithSelector( + IL2ERC20Bridge.finalizeDeposit.selector, + address(token), + address(L2Token), + alice, + alice, + 100, + hex"" + ) + ) + ); + + vm.prank(alice); + L1Bridge.depositERC20( + address(token), + address(L2Token), + 100, + 10000, + hex"" + ); + + assertEq(L1Bridge.deposits(address(token), address(L2Token)), 100); + } + + function test_L1BridgeOnlyEOADepositERC20() external { + vm.etch(alice, address(token).code); + + vm.expectRevert("Account not EOA"); + vm.prank(alice); + L1Bridge.depositERC20( + address(token), + address(L2Token), + 100, + 10000, + hex"" + ); + assertEq(L1Bridge.deposits(address(token), address(L2Token)), 0); + } + // depositERC20To // - updates bridge.deposits // - emits ERC20DepositInitiated // - calls optimismPortal.depositTransaction - // - reverts if called by EOA // - callable by a contract + function test_L1BridgeDepositERC20To() external { + vm.expectEmit(true, true, true, true); + emit ERC20DepositInitiated( + address(token), + address(L2Token), + alice, + bob, + 1000, + hex"" + ); + + deal(address(token), alice, 100000, true); + + vm.prank(alice); + token.approve(address(L1Bridge), type(uint256).max); + + vm.expectCall( + address(token), + abi.encodeWithSelector( + ERC20.transferFrom.selector, + alice, + address(L1Bridge), + 1000 + ) + ); + + vm.expectCall( + address(op), + abi.encodeWithSelector( + op.depositTransaction.selector, + Lib_PredeployAddresses.L2_STANDARD_BRIDGE, + 0, + 10000, + false, + abi.encodeWithSelector( + IL2ERC20Bridge.finalizeDeposit.selector, + address(token), + address(L2Token), + alice, + bob, + 1000, + hex"" + ) + ) + ); + + vm.prank(alice); + L1Bridge.depositERC20To( + address(token), + address(L2Token), + bob, + 1000, + 10000, + hex"" + ); + + assertEq(L1Bridge.deposits(address(token), address(L2Token)), 1000); + } + // finalizeETHWithdrawal // - emits ETHWithdrawalFinalized // - only callable by L2 bridge + function test_L1BridgeFinalizeETHWithdrawal() external { + vm.deal(address(op), 100); + vm.store(address(op), 0, bytes32(abi.encode(L2Bridge))); + + vm.expectEmit(true, true, true, true); + emit ETHWithdrawalFinalized( + alice, + alice, + 100, + hex"" + ); + + vm.expectCall( + alice, + hex"" + ); + + vm.prank(address(op)); + L1Bridge.finalizeETHWithdrawal{ value: 100 }( + alice, + alice, + 100, + hex"" + ); + + assertEq(address(op).balance, 0); + } + + function test_L1BridgeOnlyPortalFinalizeETHWithdrawal() external { + vm.expectRevert("Messages must be relayed by first calling the Optimism Portal"); + L1Bridge.finalizeETHWithdrawal{ value: 100 }( + alice, + alice, + 100, + hex"" + ); + } + + function test_L1BridgeOnlyL2BridgeFinalizeETHWithdrawal() external { + vm.deal(address(op), 100); + + vm.expectRevert("Message must be sent from the L2 Token Bridge"); + vm.prank(address(op)); + L1Bridge.finalizeETHWithdrawal{ value: 100 }( + alice, + alice, + 100, + hex"" + ); + } + // finalizeERC20Withdrawal // - updates bridge.deposits // - emits ERC20WithdrawalFinalized // - only callable by L2 bridge + function test_L1BridgeFinalizeERC20Withdrawal() external { + deal(address(token), address(L1Bridge), 100, true); + + uint256 slot = stdstore + .target(address(L1Bridge)) + .sig("deposits(address,address)") + .with_key(address(token)) + .with_key(address(L2Token)) + .find(); + + // Give the L1 bridge some ERC20 tokens + vm.store(address(L1Bridge), bytes32(slot), bytes32(uint256(100))); + assertEq(L1Bridge.deposits(address(token), address(L2Token)), 100); + + vm.expectEmit(true, true, true, true); + emit ERC20WithdrawalFinalized( + address(token), + address(L2Token), + alice, + alice, + 100, + hex"" + ); + + vm.expectCall( + address(token), + abi.encodeWithSelector( + ERC20.transfer.selector, + alice, + 100 + ) + ); + + vm.store(address(op), 0, bytes32(abi.encode(L2Bridge))); + vm.prank(address(op)); + L1Bridge.finalizeERC20Withdrawal( + address(token), + address(L2Token), + alice, + alice, + 100, + hex"" + ); + + assertEq(token.balanceOf(address(L1Bridge)), 0); + assertEq(token.balanceOf(address(alice)), 100); + } + + function test_L1BridgeOnlyPortalFinalizeERC20Withdrawal() external { + vm.expectRevert("Messages must be relayed by first calling the Optimism Portal"); + L1Bridge.finalizeERC20Withdrawal( + address(token), + address(L2Token), + alice, + alice, + 100, + hex"" + ); + } + + function test_L1BridgeOnlyL2BridgeFinalizeERC20Withdrawal() external { + vm.expectRevert("Message must be sent from the L2 Token Bridge"); + vm.prank(address(op)); + L1Bridge.finalizeERC20Withdrawal( + address(token), + address(L2Token), + alice, + alice, + 100, + hex"" + ); + } + // donateETH // - can send ETH to the contract + function test_L1BridgeDonateETH() external { + assertEq(address(L1Bridge).balance, 0); + vm.prank(alice); + L1Bridge.donateETH{ value: 1000 }(); + assertEq(address(L1Bridge).balance, 1000); + } } diff --git a/packages/contracts/contracts/test/L2OutputOracle.t.sol b/packages/contracts/contracts/test/L2OutputOracle.t.sol index 3a45339e..87e8d382 100644 --- a/packages/contracts/contracts/test/L2OutputOracle.t.sol +++ b/packages/contracts/contracts/test/L2OutputOracle.t.sol @@ -4,7 +4,26 @@ pragma solidity 0.8.10; /* Testing utilities */ import { CommonTest } from "./CommonTest.t.sol"; +import { + Lib_PredeployAddresses +} from "@eth-optimism/contracts/libraries/constants/Lib_PredeployAddresses.sol"; + +import { Lib_BedrockPredeployAddresses } from "../libraries/Lib_BedrockPredeployAddresses.sol"; import { L2OutputOracle } from "../L1/L2OutputOracle.sol"; +import { OptimismPortal } from "../L1/OptimismPortal.sol"; +import { IWithdrawer } from "../L2/IWithdrawer.sol"; +import { L2StandardBridge } from "../L2/messaging/L2StandardBridge.sol"; + +import { IL2StandardBridge } from "../L2/messaging/IL2StandardBridge.sol"; + +import { L1StandardBridge } from "../L1/messaging/L1StandardBridge.sol"; +import { L2StandardTokenFactory } from "../L2/messaging/L2StandardTokenFactory.sol"; +import { IL2StandardTokenFactory } from "../L2/messaging/IL2StandardTokenFactory.sol"; +import { IL2StandardERC20 } from "../L2/tokens/IL2StandardERC20.sol"; +import { Withdrawer } from "../L2/Withdrawer.sol"; +import { LibRLP } from "./Lib_RLP.t.sol"; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract L2OutputOracle_Initializer is CommonTest { // Utility variables @@ -42,6 +61,114 @@ contract L2OutputOracle_Initializer is CommonTest { } } +contract BridgeInitializer is L2OutputOracle_Initializer { + OptimismPortal op; + + IWithdrawer W; + L1StandardBridge L1Bridge; + IL2StandardBridge L2Bridge; + IL2StandardTokenFactory L2TokenFactory; + IL2StandardERC20 L2Token; + ERC20 token; + + event ETHDepositInitiated( + address indexed _from, + address indexed _to, + uint256 _amount, + bytes _data + ); + + event ETHWithdrawalFinalized( + address indexed _from, + address indexed _to, + uint256 _amount, + bytes _data + ); + + event ERC20DepositInitiated( + address indexed _l1Token, + address indexed _l2Token, + address indexed _from, + address _to, + uint256 _amount, + bytes _data + ); + + event ERC20WithdrawalFinalized( + address indexed _l1Token, + address indexed _l2Token, + address indexed _from, + address _to, + uint256 _amount, + bytes _data + ); + + event WithdrawalInitiated( + address indexed _l1Token, + address indexed _l2Token, + address indexed _from, + address _to, + uint256 _amount, + bytes _data + ); + + event DepositFinalized( + address indexed _l1Token, + address indexed _l2Token, + address indexed _from, + address _to, + uint256 _amount, + bytes _data + ); + + event DepositFailed( + address indexed _l1Token, + address indexed _l2Token, + address indexed _from, + address _to, + uint256 _amount, + bytes _data + ); + + constructor() { + vm.deal(alice, 1 << 16); + + L1Bridge = new L1StandardBridge(); + L2StandardBridge l2Bridge = new L2StandardBridge(address(L1Bridge)); + vm.etch(Lib_PredeployAddresses.L2_STANDARD_BRIDGE, address(l2Bridge).code); + + L2Bridge = IL2StandardBridge(Lib_PredeployAddresses.L2_STANDARD_BRIDGE); + // Set the storage slot that holds the L1 bridge address passed in via + // the constructor args + vm.store(address(L2Bridge), bytes32(0), bytes32(uint256(uint160(address(L1Bridge))))); + + op = new OptimismPortal(oracle, 100); + L1Bridge.initialize(op, address(L2Bridge)); + + Withdrawer w = new Withdrawer(); + vm.etch(Lib_BedrockPredeployAddresses.WITHDRAWER, address(w).code); + W = IWithdrawer(Lib_BedrockPredeployAddresses.WITHDRAWER); + + L2StandardTokenFactory factory = new L2StandardTokenFactory(); + vm.etch(Lib_PredeployAddresses.L2_STANDARD_TOKEN_FACTORY, address(factory).code); + L2TokenFactory = IL2StandardTokenFactory(Lib_PredeployAddresses.L2_STANDARD_TOKEN_FACTORY); + + token = new ERC20("Test Token", "TT"); + + // Deploy the L2 ERC20 now + L2TokenFactory.createStandardL2Token( + address(token), + string(abi.encodePacked("L2-", token.name())), + string(abi.encodePacked("L2-", token.symbol())) + ); + + L2Token = IL2StandardERC20( + LibRLP.computeAddress(address(L2TokenFactory), 0) + ); + } + +} + // Define this test in a standalone contract to ensure it runs immediately after the constructor. contract L2OutputOracleTest_Constructor is L2OutputOracle_Initializer { function test_constructor() external { diff --git a/packages/contracts/contracts/test/L2StandardBridge.t.sol b/packages/contracts/contracts/test/L2StandardBridge.t.sol index 777c10d0..7c16fe7d 100644 --- a/packages/contracts/contracts/test/L2StandardBridge.t.sol +++ b/packages/contracts/contracts/test/L2StandardBridge.t.sol @@ -18,47 +18,28 @@ import { IL2StandardERC20 } from "../L2/tokens/IL2StandardERC20.sol"; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { CommonTest } from "./CommonTest.t.sol"; -import { L2OutputOracle_Initializer } from "./L2OutputOracle.t.sol"; +import { L2OutputOracle_Initializer, BridgeInitializer } from "./L2OutputOracle.t.sol"; import { LibRLP } from "./Lib_RLP.t.sol"; +import { IL1ERC20Bridge } from "../L1/messaging/IL1ERC20Bridge.sol"; +import { AddressAliasHelper } from "@eth-optimism/contracts/standards/AddressAliasHelper.sol"; import { console } from "forge-std/console.sol"; +import { stdStorage, StdStorage } from "forge-std/Test.sol"; -contract L2StandardBridge_Test is CommonTest, L2OutputOracle_Initializer { - OptimismPortal op; - - IWithdrawer W; - L1StandardBridge L1Bridge; - L2StandardBridge L2Bridge; - IL2StandardTokenFactory L2TokenFactory; - IL2StandardERC20 L2Token; +contract L2StandardBridge_Test is CommonTest, BridgeInitializer { + using stdStorage for StdStorage; function setUp() external { - L1Bridge = new L1StandardBridge(); - L2Bridge = new L2StandardBridge(address(L1Bridge)); - op = new OptimismPortal(oracle, 100); - - L1Bridge.initialize(op, address(L2Bridge)); - - Withdrawer w = new Withdrawer(); - vm.etch(Lib_BedrockPredeployAddresses.WITHDRAWER, address(w).code); - W = IWithdrawer(Lib_BedrockPredeployAddresses.WITHDRAWER); + // put some tokens in the bridge, give them to alice on L2 + uint256 slot = stdstore + .target(address(L1Bridge)) + .sig("deposits(address,address)") + .with_key(address(token)) + .with_key(address(L2Token)) + .find(); - L2StandardTokenFactory factory = new L2StandardTokenFactory(); - vm.etch(Lib_PredeployAddresses.L2_STANDARD_TOKEN_FACTORY, address(factory).code); - L2TokenFactory = IL2StandardTokenFactory(Lib_PredeployAddresses.L2_STANDARD_TOKEN_FACTORY); - - ERC20 token = new ERC20("Test Token", "TT"); - - // Deploy the L2 ERC20 now - L2TokenFactory.createStandardL2Token( - address(token), - string(abi.encodePacked("L2-", token.name())), - string(abi.encodePacked("L2-", token.symbol())) - ); - - L2Token = IL2StandardERC20( - LibRLP.computeAddress(address(L2TokenFactory), 0) - ); + vm.store(address(L1Bridge), bytes32(slot), bytes32(uint256(100000))); + deal(address(L2Token), alice, 100000, true); } function test_L2BridgeCorrectL1Bridge() external { @@ -70,14 +51,323 @@ contract L2StandardBridge_Test is CommonTest, L2OutputOracle_Initializer { // - token is burned // - emits WithdrawalInitiated // - calls Withdrawer.initiateWithdrawal + function test_L2BridgeWithdraw() external { + vm.expectEmit(true, true, true, true); + emit WithdrawalInitiated( + address(token), + address(L2Token), + alice, + alice, + 100, + hex"" + ); + + uint256 aliceBalance = L2Token.balanceOf(alice); + + vm.expectCall( + Lib_BedrockPredeployAddresses.WITHDRAWER, + abi.encodeWithSelector( + IWithdrawer.initiateWithdrawal.selector, + address(L1Bridge), + 10000, + abi.encodeWithSelector( + IL1ERC20Bridge.finalizeERC20Withdrawal.selector, + address(token), + address(L2Token), + alice, + alice, + 100, + hex"" + ) + ) + ); + + vm.expectCall( + address(L2Token), + abi.encodeWithSelector( + IL2StandardERC20.burn.selector, + alice, + 100 + ) + ); + + vm.prank(address(alice)); + L2Bridge.withdraw( + address(L2Token), + 100, + 10000, + hex"" + ); + + assertEq(L2Token.balanceOf(alice), aliceBalance - 100); + assertEq(L2Token.totalSupply(), L2Token.balanceOf(alice)); + } + + function test_L2BridgeRevertWithdraw() external { + vm.expectRevert(abi.encodeWithSignature("InvalidWithdrawalAmount()")); + vm.prank(address(alice)); + L2Bridge.withdraw{ value: 100 }( + address(L2Token), + 100, + 10000, + hex"" + ); + } + + function test_L2BridgeWithdrawETH() external { + vm.expectCall( + Lib_BedrockPredeployAddresses.WITHDRAWER, + abi.encodeWithSelector( + Withdrawer.initiateWithdrawal.selector, + address(L1Bridge), + 10000, + abi.encodeWithSelector( + L1StandardBridge.finalizeETHWithdrawal.selector, + alice, + alice, + 100, + hex"" + ) + ) + ); + + vm.expectEmit(true, true, true, true); + emit WithdrawalInitiated( + address(0), + Lib_PredeployAddresses.OVM_ETH, + alice, + alice, + 100, + hex"" + ); + + uint256 aliceBalance = alice.balance; + + vm.prank(address(alice)); + L2Bridge.withdraw{ value: 100 }( + Lib_PredeployAddresses.OVM_ETH, + 100, + 10000, + hex"" + ); + + uint256 aliceBalancePost = alice.balance; + + // Alice's balance should go down + assertEq(aliceBalance - 100, aliceBalancePost); + } + + function test_L2BridgeRevertWithdrawETH() external { + vm.expectRevert(abi.encodeWithSignature("InvalidWithdrawalAmount()")); + vm.prank(address(alice)); + L2Bridge.withdraw( + Lib_PredeployAddresses.OVM_ETH, + 100, + 10000, + hex"" + ); + } + // withdrawTo // - token is burned // - emits WithdrawalInitiated w/ correct recipient // - calls Withdrawer.initiateWithdrawal + function test_L2BridgeWithdrawTo() external { + assertEq(L2Token.balanceOf(bob), 0); + + uint256 aliceBalance = L2Token.balanceOf(alice); + + vm.expectCall( + Lib_BedrockPredeployAddresses.WITHDRAWER, + abi.encodeWithSelector( + Withdrawer.initiateWithdrawal.selector, + address(L1Bridge), + 200000, + abi.encodeWithSelector( + IL1ERC20Bridge.finalizeERC20Withdrawal.selector, + address(token), + address(L2Token), + alice, + bob, + 200, + hex"" + ) + ) + ); + + vm.expectEmit(true, true, true, true); + emit WithdrawalInitiated( + address(token), + address(L2Token), + alice, + bob, + 200, + hex"" + ); + + vm.prank(alice); + L2Bridge.withdrawTo( + address(L2Token), + bob, + 200, + 200000, + hex"" + ); + + // alice's balance should go down + uint256 aliceBalancePost = L2Token.balanceOf(alice); + assertEq(aliceBalance - 200, aliceBalancePost); + } + + // TODO: the eth functions + // finalizeDeposit // - only callable by l1TokenBridge // - supported token pair emits DepositFinalized // - invalid deposit emits DepositFailed // - invalid deposit calls Withdrawer.initiateWithdrawal + function test_L2BridgeFinalizeDeposit() external { + uint256 aliceBalance = L2Token.balanceOf(alice); + + vm.expectCall( + address(L2Token), + abi.encodeWithSelector( + IL2StandardERC20.mint.selector, + alice, + 100 + ) + ); + + vm.expectEmit(true, true, true, true); + emit DepositFinalized( + address(token), + address(L2Token), + alice, + alice, + 100, + hex"" + ); + + vm.prank(AddressAliasHelper.applyL1ToL2Alias(address(L1Bridge))); + L2Bridge.finalizeDeposit( + address(token), + address(L2Token), + alice, + alice, + 100, + hex"" + ); + + uint256 aliceBalancePost = L2Token.balanceOf(alice); + assertEq(aliceBalance + 100, aliceBalancePost); + } + + function test_L2BridgeBadDeposit() external { + vm.expectEmit(true, true, true, true); + emit DepositFailed( + address(10), + address(L2Token), + alice, + alice, + 100, + hex"" + ); + + vm.prank(AddressAliasHelper.applyL1ToL2Alias(address(L1Bridge))); + L2Bridge.finalizeDeposit( + address(10), + address(L2Token), + alice, + alice, + 100, + hex"" + ); + } + + function test_L2BridgeFinalizeETHDeposit() external { + uint256 aliceBalance = alice.balance; + + vm.expectEmit(true, true, true, true); + emit DepositFinalized( + address(0), + Lib_PredeployAddresses.OVM_ETH, + alice, + alice, + 100, + hex"" + ); + + hoax(AddressAliasHelper.applyL1ToL2Alias(address(L1Bridge))); + L2Bridge.finalizeDeposit{ value: 100 }( + address(0), + Lib_PredeployAddresses.OVM_ETH, + alice, + alice, + 100, + hex"" + ); + + uint256 aliceBalancePost = alice.balance; + assertEq(aliceBalance + 100, aliceBalancePost); + } + + // when the values do not match up + function test_L2BridgeFinalizeETHDepositWrongAmount() external { + uint256 aliceBalance = alice.balance; + + vm.expectEmit(true, true, true, true); + emit DepositFailed( + address(0), + Lib_PredeployAddresses.OVM_ETH, + alice, + alice, + 100, + hex"" + ); + + vm.expectCall( + Lib_BedrockPredeployAddresses.WITHDRAWER, + abi.encodeWithSelector( + Withdrawer.initiateWithdrawal.selector, + address(L1Bridge), + 0, + abi.encodeWithSelector( + L1StandardBridge.finalizeETHWithdrawal.selector, + alice, + alice, + 200, + hex"" + ) + ) + ); + + hoax(AddressAliasHelper.applyL1ToL2Alias(address(L1Bridge))); + L2Bridge.finalizeDeposit{ value: 200 }( + address(0), + Lib_PredeployAddresses.OVM_ETH, + alice, + alice, + 100, + hex"" + ); + + uint256 aliceBalancePost = alice.balance; + assertEq(aliceBalance, aliceBalancePost); + assertEq(address(L2Bridge).balance, 0); + assertEq(address(Lib_BedrockPredeployAddresses.WITHDRAWER).balance, 200); + } + + function test_L2BridgeFinalizeDepositRevertsOnCaller() external { + vm.expectRevert("Can only be called by a the l1TokenBridge"); + vm.prank(alice); + L2Bridge.finalizeDeposit( + address(0), + Lib_PredeployAddresses.OVM_ETH, + alice, + alice, + 100, + hex"" + ); + } } diff --git a/packages/contracts/lib/forge-std b/packages/contracts/lib/forge-std index e26ae295..409465b6 160000 --- a/packages/contracts/lib/forge-std +++ b/packages/contracts/lib/forge-std @@ -1 +1 @@ -Subproject commit e26ae2954bb6d883643b92760d443dd07f413647 +Subproject commit 409465b6992822a91318142dd269660aad9e30ee