diff --git a/src/bridge/optimism/L1BobBridge.sol b/src/bridge/optimism/L1BobBridge.sol new file mode 100644 index 0000000..36b5d91 --- /dev/null +++ b/src/bridge/optimism/L1BobBridge.sol @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + +import "@openzeppelin/contracts/utils/Address.sol"; +import "../../interfaces/IMintableERC20.sol"; +import "../../interfaces/IBurnableERC20.sol"; +import "./interfaces/IL1ERC20Bridge.sol"; +import "./interfaces/IL2ERC20Bridge.sol"; +import "./libraries/CrossDomainEnabled.sol"; + +/** + * @title L1BobBridge + */ +contract L1BobBridge is IL1ERC20Bridge, CrossDomainEnabled { + address public immutable l2TokenBridge; + address public immutable l1Token; + address public immutable l2Token; + + constructor( + address _l1Messenger, + address _l2TokenBridge, + address _l1Token, + address _l2Token + ) + CrossDomainEnabled(_l1Messenger) + { + l2TokenBridge = _l2TokenBridge; + l1Token = _l1Token; + l2Token = _l2Token; + } + + /** + * @dev Modifier requiring sender to be EOA. This check could be bypassed by a malicious + * contract via initcode, but it takes care of the user error we want to avoid. + */ + modifier onlyEOA() { + // Used to stop deposits from contracts (avoid accidentally lost tokens) + require(!Address.isContract(msg.sender), "Account not EOA"); + _; + } + + /** + * @inheritdoc IL1ERC20Bridge + */ + function depositERC20( + address _l1Token, + address _l2Token, + uint256 _amount, + uint32 _l2Gas, + bytes calldata _data + ) + external + virtual + onlyEOA + { + _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, msg.sender, _amount, _l2Gas, _data); + } + + /** + * @inheritdoc IL1ERC20Bridge + */ + function depositERC20To( + address _l1Token, + address _l2Token, + address _to, + uint256 _amount, + uint32 _l2Gas, + bytes calldata _data + ) + external + virtual + { + _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, _to, _amount, _l2Gas, _data); + } + + /** + * @dev Performs the logic for deposits by informing the L2 Deposited Token + * contract of the deposit and calling a handler to lock the L1 funds. (e.g. transferFrom) + * + * @param _l1Token Address of the L1 ERC20 we are depositing + * @param _l2Token Address of the L1 respective L2 ERC20 + * @param _from Account to pull the deposit from on L1 + * @param _to Account to give the deposit to on L2 + * @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 _initiateERC20Deposit( + address _l1Token, + address _l2Token, + address _from, + address _to, + uint256 _amount, + uint32 _l2Gas, + bytes calldata _data + ) + internal + { + require(_l1Token == l1Token, "L1BobBridge: invalid l1Token"); + require(_l2Token == l2Token, "L1BobBridge: invalid l2Token"); + + IBurnableERC20(_l1Token).burnFrom(_from, _amount); + + // Construct calldata for _l2Token.finalizeDeposit(_to, _amount) + bytes memory message = abi.encodeWithSelector( + IL2ERC20Bridge.finalizeDeposit.selector, _l1Token, _l2Token, _from, _to, _amount, _data + ); + + // Send calldata into L2 + sendCrossDomainMessage(l2TokenBridge, _l2Gas, message); + + emit ERC20DepositInitiated(_l1Token, _l2Token, _from, _to, _amount, _data); + } + + /** + * @inheritdoc IL1ERC20Bridge + */ + function finalizeERC20Withdrawal( + address _l1Token, + address _l2Token, + address _from, + address _to, + uint256 _amount, + bytes calldata _data + ) + external + onlyFromCrossDomainAccount(l2TokenBridge) + { + require(_l1Token == l1Token, "L1BobBridge: invalid l1Token"); + require(_l2Token == l2Token, "L1BobBridge: invalid l2Token"); + + IMintableERC20(_l1Token).mint(_to, _amount); + + emit ERC20WithdrawalFinalized(_l1Token, _l2Token, _from, _to, _amount, _data); + } +} diff --git a/src/bridge/optimism/L2BobBridge.sol b/src/bridge/optimism/L2BobBridge.sol new file mode 100644 index 0000000..0b72afb --- /dev/null +++ b/src/bridge/optimism/L2BobBridge.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + +import "@openzeppelin/contracts/utils/Address.sol"; +import "../../interfaces/IMintableERC20.sol"; +import "../../interfaces/IBurnableERC20.sol"; +import "./interfaces/IL1ERC20Bridge.sol"; +import "./interfaces/IL2ERC20Bridge.sol"; +import "./libraries/CrossDomainEnabled.sol"; + +/** + * @title L2BobBridge + */ +contract L2BobBridge is IL2ERC20Bridge, CrossDomainEnabled { + address public immutable l1TokenBridge; + address public immutable l1Token; + address public immutable l2Token; + + constructor( + address _l2Messenger, + address _l1TokenBridge, + address _l1Token, + address _l2Token + ) + CrossDomainEnabled(_l2Messenger) + { + l1TokenBridge = _l1TokenBridge; + l1Token = _l1Token; + l2Token = _l2Token; + } + + /** + * @inheritdoc IL2ERC20Bridge + */ + function withdraw(address _l2Token, uint256 _amount, uint32 _l1Gas, bytes calldata _data) external virtual { + _initiateWithdrawal(_l2Token, msg.sender, msg.sender, _amount, _l1Gas, _data); + } + + /** + * @inheritdoc IL2ERC20Bridge + */ + function withdrawTo( + address _l2Token, + address _to, + uint256 _amount, + uint32 _l1Gas, + bytes calldata _data + ) + external + virtual + { + _initiateWithdrawal(_l2Token, msg.sender, _to, _amount, _l1Gas, _data); + } + + /** + * @dev Performs the logic for withdrawals by burning the token and informing + * the L1 token Gateway of the withdrawal. + * @param _l2Token Address of L2 token where withdrawal is initiated. + * @param _from Account to pull the withdrawal from on L2. + * @param _to Account to give the withdrawal to on L1. + * @param _amount Amount of the token to withdraw. + * @param _l1Gas Unused, but included for potential forward compatibility considerations. + * @param _data Optional data to forward to L1. 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 _initiateWithdrawal( + address _l2Token, + address _from, + address _to, + uint256 _amount, + uint32 _l1Gas, + bytes calldata _data + ) + internal + { + require(_l2Token == l2Token, "L2BobBridge: invalid l2Token"); + + IBurnableERC20(_l2Token).burnFrom(msg.sender, _amount); + + bytes memory message = abi.encodeWithSelector( + IL1ERC20Bridge.finalizeERC20Withdrawal.selector, l1Token, _l2Token, _from, _to, _amount, _data + ); + + // Send message up to L1 bridge + sendCrossDomainMessage(l1TokenBridge, _l1Gas, message); + + emit WithdrawalInitiated(l1Token, _l2Token, msg.sender, _to, _amount, _data); + } + + /** + * @inheritdoc IL2ERC20Bridge + */ + function finalizeDeposit( + address _l1Token, + address _l2Token, + address _from, + address _to, + uint256 _amount, + bytes calldata _data + ) + external + virtual + onlyFromCrossDomainAccount(l1TokenBridge) + { + require(_l1Token == l1Token, "L2BobBridge: invalid l1Token"); + require(_l2Token == l2Token, "L2BobBridge: invalid l2Token"); + + IMintableERC20(_l2Token).mint(_to, _amount); + + emit DepositFinalized(_l1Token, _l2Token, _from, _to, _amount, _data); + } +} diff --git a/src/bridge/optimism/interfaces/IL1ERC20Bridge.sol b/src/bridge/optimism/interfaces/IL1ERC20Bridge.sol new file mode 100644 index 0000000..e8dbc6f --- /dev/null +++ b/src/bridge/optimism/interfaces/IL1ERC20Bridge.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >0.5.0 <0.9.0; + +/** + * @title IL1ERC20Bridge + */ +interface IL1ERC20Bridge { + 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 + ); + + /** + * @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; + + /** + * @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/src/bridge/optimism/interfaces/IL2ERC20Bridge.sol b/src/bridge/optimism/interfaces/IL2ERC20Bridge.sol new file mode 100644 index 0000000..4d40d94 --- /dev/null +++ b/src/bridge/optimism/interfaces/IL2ERC20Bridge.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.9; + +/** + * @title IL2ERC20Bridge + */ +interface IL2ERC20Bridge { + 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 + ); + + /** + * @dev get the address of the corresponding L1 bridge contract. + * @return Address of the corresponding L1 bridge contract. + */ + function l1TokenBridge() external returns (address); + + /** + * @dev initiate a withdraw of some tokens to the caller's account on L1 + * @param _l2Token Address of L2 token where withdrawal was initiated. + * @param _amount Amount of the token to withdraw. + * @param _l1Gas Unused, but included for potential forward compatibility considerations. + * @param _data Optional data to forward to L1. 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 withdraw(address _l2Token, uint256 _amount, uint32 _l1Gas, bytes calldata _data) external; + + /** + * @dev initiate a withdraw of some token to a recipient's account on L1. + * @param _l2Token Address of L2 token where withdrawal is initiated. + * @param _to L1 adress to credit the withdrawal to. + * @param _amount Amount of the token to withdraw. + * @param _l1Gas Unused, but included for potential forward compatibility considerations. + * @param _data Optional data to forward to L1. 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 withdrawTo(address _l2Token, address _to, uint256 _amount, uint32 _l1Gas, bytes calldata _data) external; + + /** + * @dev Complete a deposit from L1 to L2, and credits funds to the recipient's balance of this + * L2 token. This call will fail if it did not originate from a corresponding deposit in + * L1StandardTokenBridge. + * @param _l1Token Address for the l1 token this is called with + * @param _l2Token Address for the l2 token this is called with + * @param _from Account to pull the deposit from on L2. + * @param _to Address to receive the withdrawal at + * @param _amount Amount of the token to withdraw + * @param _data Data provider by the sender on L1. 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 finalizeDeposit( + address _l1Token, + address _l2Token, + address _from, + address _to, + uint256 _amount, + bytes calldata _data + ) + external; +} diff --git a/src/bridge/optimism/libraries/CrossDomainEnabled.sol b/src/bridge/optimism/libraries/CrossDomainEnabled.sol new file mode 100644 index 0000000..9561ff2 --- /dev/null +++ b/src/bridge/optimism/libraries/CrossDomainEnabled.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >0.5.0 <0.9.0; + +import "@openzeppelin/contracts/vendor/optimism/ICrossDomainMessenger.sol"; + +/** + * @title CrossDomainEnabled + * @dev Helper contract for contracts performing cross-domain communications + * + * Compiler used: defined by inheriting contract + */ +contract CrossDomainEnabled { + // Messenger contract used to send and receive messages from the other domain. + address public immutable messenger; + + /** + * @param _messenger Address of the CrossDomainMessenger on the current layer. + */ + constructor(address _messenger) { + messenger = _messenger; + } + + /** + * Enforces that the modified function is only callable by a specific cross-domain account. + * @param _sourceDomainAccount The only account on the originating domain which is + * authenticated to call this function. + */ + modifier onlyFromCrossDomainAccount(address _sourceDomainAccount) { + require(msg.sender == address(getCrossDomainMessenger()), "OVM_XCHAIN: messenger contract unauthenticated"); + + require( + getCrossDomainMessenger().xDomainMessageSender() == _sourceDomainAccount, + "OVM_XCHAIN: wrong sender of cross-domain message" + ); + + _; + } + + /** + * Gets the messenger, usually from storage. This function is exposed in case a child contract + * needs to override. + * @return The address of the cross-domain messenger contract which should be used. + */ + function getCrossDomainMessenger() internal virtual returns (ICrossDomainMessenger) { + return ICrossDomainMessenger(messenger); + } + + /** + * @dev Sends a message to an account on another domain + * @param _crossDomainTarget The intended recipient on the destination domain + * @param _message The data to send to the target (usually calldata to a function with + * `onlyFromCrossDomainAccount()`) + * @param _gasLimit The gasLimit for the receipt of the message on the target domain. + */ + function sendCrossDomainMessage(address _crossDomainTarget, uint32 _gasLimit, bytes memory _message) internal { + getCrossDomainMessenger().sendMessage(_crossDomainTarget, _message, _gasLimit); + } +} diff --git a/src/interfaces/IBurnableERC20.sol b/src/interfaces/IBurnableERC20.sol index e3e4241..39d8fda 100644 --- a/src/interfaces/IBurnableERC20.sol +++ b/src/interfaces/IBurnableERC20.sol @@ -4,4 +4,5 @@ pragma solidity 0.8.15; interface IBurnableERC20 { function burn(uint256 amount) external; + function burnFrom(address user, uint256 amount) external; } diff --git a/src/token/ERC20MintBurn.sol b/src/token/ERC20MintBurn.sol index c7e0f43..02ff69f 100644 --- a/src/token/ERC20MintBurn.sol +++ b/src/token/ERC20MintBurn.sol @@ -3,7 +3,6 @@ pragma solidity 0.8.15; import "../utils/Ownable.sol"; -import "../interfaces/IMintableERC20.sol"; import "./BaseERC20.sol"; import "../interfaces/IMintableERC20.sol"; import "../interfaces/IBurnableERC20.sol"; @@ -58,4 +57,18 @@ abstract contract ERC20MintBurn is IMintableERC20, IBurnableERC20, Ownable, Base _burn(msg.sender, _value); } + + /** + * @dev Burns pre-approved tokens from the other address. + * Callable only by one of the burner addresses. + * @param _from account to burn tokens from. + * @param _value amount of tokens to burn. Should be less than or equal to caller balance. + */ + function burnFrom(address _from, uint256 _value) external virtual { + require(isBurner(msg.sender), "ERC20MintBurn: not a burner"); + + _spendAllowance(_from, msg.sender, _value); + + _burn(_from, _value); + } } diff --git a/test/bridge/optimism/OptimismBridge.sol b/test/bridge/optimism/OptimismBridge.sol new file mode 100644 index 0000000..1e27dc8 --- /dev/null +++ b/test/bridge/optimism/OptimismBridge.sol @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity 0.8.15; + +import "forge-std/Test.sol"; +import "../../shared/Env.t.sol"; +import "../../../src/BobToken.sol"; +import "../../../src/proxy/EIP1967Proxy.sol"; +import "../../../src/bridge/optimism/L1BobBridge.sol"; +import "../../../src/bridge/optimism/L2BobBridge.sol"; + +interface IMessageRelay { + function relayMessage(address _target, address _sender, bytes memory _message, uint256 _messageNonce) external; +} + +contract OptimismBridge is Test { + 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 + ); + + BobToken bobMainnet; + BobToken bobOptimism; + + uint256 mainnetFork; + uint256 optimismFork; + + L1BobBridge l1Bridge; + L2BobBridge l2Bridge; + + address l1Messenger = 0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1; + address l2Messenger = 0x4200000000000000000000000000000000000007; + + function setUp() public { + mainnetFork = vm.createFork(forkRpcUrlMainnet); + optimismFork = vm.createFork(forkRpcUrlOptimism); + + vm.selectFork(mainnetFork); + + EIP1967Proxy l1BridgeProxy = new EIP1967Proxy(address(this), mockImpl, ""); + + EIP1967Proxy bobProxy = new EIP1967Proxy(address(this), mockImpl, ""); + BobToken bobImpl = new BobToken(address(bobProxy)); + bobProxy.upgradeTo(address(bobImpl)); + bobMainnet = BobToken(address(bobProxy)); + + bobMainnet.updateMinter(address(this), true, false); + bobMainnet.updateMinter(address(l1BridgeProxy), true, true); + + vm.selectFork(optimismFork); + + EIP1967Proxy l2BridgeProxy = new EIP1967Proxy(address(this), mockImpl, ""); + + bobProxy = new EIP1967Proxy(address(this), mockImpl, ""); + BobToken bobImpl2 = new BobToken(address(bobProxy)); + bobProxy.upgradeTo(address(bobImpl2)); + bobOptimism = BobToken(address(bobProxy)); + + bobOptimism.updateMinter(address(this), true, false); + bobOptimism.updateMinter(address(l2BridgeProxy), true, true); + + vm.selectFork(mainnetFork); + + l1Bridge = new L1BobBridge(l1Messenger, address(l2BridgeProxy), address(bobMainnet), address(bobOptimism)); + l1BridgeProxy.upgradeTo(address(l1Bridge)); + l1Bridge = L1BobBridge(address(l1BridgeProxy)); + + vm.selectFork(optimismFork); + + l2Bridge = new L2BobBridge(l2Messenger, address(l1BridgeProxy), address(bobMainnet), address(bobOptimism)); + l2BridgeProxy.upgradeTo(address(l2Bridge)); + l2Bridge = L2BobBridge(address(l2BridgeProxy)); + + vm.label(address(bobMainnet), "BOB"); + vm.label(address(bobOptimism), "BOB"); + } + + function _syncL1ToL2State() internal { + uint256 curFork = vm.activeFork(); + Vm.Log[] memory logs = vm.getRecordedLogs(); + vm.selectFork(optimismFork); + for (uint256 i = 0; i < logs.length; i++) { + if (logs[i].topics[0] == bytes32(0xcb0f7ffd78f9aee47a248fae8db181db6eee833039123e026dcbff529522e52a)) { + vm.prank(address(uint160(l1Messenger) + uint160(0x1111000000000000000000000000000000001111))); + address to = address(uint160(uint256(logs[i].topics[1]))); + (address from, bytes memory data, uint256 nonce, uint256 gasLimit) = + abi.decode(logs[i].data, (address, bytes, uint256, uint256)); + IMessageRelay(l2Messenger).relayMessage(to, from, data, nonce); + } + } + vm.selectFork(curFork); + } + + function _syncL2ToL1State() internal { + uint256 curFork = vm.activeFork(); + Vm.Log[] memory logs = vm.getRecordedLogs(); + vm.selectFork(mainnetFork); + for (uint256 i = 0; i < logs.length; i++) { + if (logs[i].topics[0] == bytes32(0xcb0f7ffd78f9aee47a248fae8db181db6eee833039123e026dcbff529522e52a)) { + vm.etch(l1Messenger, ""); + vm.prank(l1Messenger); + address to = address(uint160(uint256(logs[i].topics[1]))); + (address from, bytes memory data, uint256 nonce, uint256 gasLimit) = + abi.decode(logs[i].data, (address, bytes, uint256, uint256)); + vm.mockCall( + l1Messenger, + abi.encodeWithSelector(ICrossDomainMessenger.xDomainMessageSender.selector), + abi.encode(from) + ); + (bool status,) = to.call{gas: gasLimit}(data); + } + } + vm.selectFork(curFork); + } + + function testBridgeToOptimism() public { + vm.selectFork(mainnetFork); + + bobMainnet.mint(user1, 100 ether); + + vm.startPrank(user1); + bobMainnet.approve(address(l1Bridge), 10 ether); + vm.expectEmit(true, true, true, true, address(l1Bridge)); + emit ERC20DepositInitiated(address(bobMainnet), address(bobOptimism), user1, user2, 10 ether, ""); + vm.recordLogs(); + l1Bridge.depositERC20To(address(bobMainnet), address(bobOptimism), user2, 10 ether, 1000000, ""); + vm.stopPrank(); + + vm.expectEmit(true, true, true, true, address(l2Bridge)); + emit DepositFinalized(address(bobMainnet), address(bobOptimism), user1, user2, 10 ether, ""); + _syncL1ToL2State(); + + assertEq(bobMainnet.totalSupply(), 90 ether); + assertEq(bobMainnet.balanceOf(user1), 90 ether); + + vm.selectFork(optimismFork); + + assertEq(bobOptimism.totalSupply(), 10 ether); + assertEq(bobOptimism.balanceOf(user2), 10 ether); + } + + function testBridgeFromOptimism() public { + vm.selectFork(optimismFork); + + bobOptimism.mint(user2, 100 ether); + + vm.startPrank(user2); + bobOptimism.approve(address(l2Bridge), 10 ether); + vm.expectEmit(true, true, true, true, address(l2Bridge)); + emit WithdrawalInitiated(address(bobMainnet), address(bobOptimism), user2, user1, 10 ether, ""); + vm.recordLogs(); + l2Bridge.withdrawTo(address(bobOptimism), user1, 10 ether, 1000000, ""); + vm.stopPrank(); + + vm.expectEmit(true, true, true, true, address(l1Bridge)); + emit ERC20WithdrawalFinalized(address(bobMainnet), address(bobOptimism), user2, user1, 10 ether, ""); + _syncL2ToL1State(); + + vm.selectFork(mainnetFork); + + assertEq(bobMainnet.totalSupply(), 10 ether); + assertEq(bobMainnet.balanceOf(user1), 10 ether); + + vm.selectFork(optimismFork); + + assertEq(bobOptimism.totalSupply(), 90 ether); + assertEq(bobOptimism.balanceOf(user2), 90 ether); + } +}