diff --git a/src/L1/L1ScrollMessenger.sol b/src/L1/L1ScrollMessenger.sol index a023dcf0..9f98a9da 100644 --- a/src/L1/L1ScrollMessenger.sol +++ b/src/L1/L1ScrollMessenger.sol @@ -289,7 +289,7 @@ contract L1ScrollMessenger is ScrollMessengerBase, IL1ScrollMessenger { bytes memory _message, uint256 _gasLimit, address _refundAddress - ) internal nonReentrant { + ) internal virtual nonReentrant { // compute the actual cross domain message calldata. uint256 _messageNonce = IL1MessageQueueV2(messageQueueV2).nextCrossDomainMessageIndex(); bytes memory _xDomainCalldata = _encodeXDomainCalldata(_msgSender(), _to, _value, _messageNonce, _message); diff --git a/src/mocks/ScrollChainValidiumMock.sol b/src/mocks/ScrollChainValidiumMock.sol new file mode 100644 index 00000000..1c40fc2f --- /dev/null +++ b/src/mocks/ScrollChainValidiumMock.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT + +pragma solidity =0.8.24; + +import {IL1MessageQueueV2} from "../L1/rollup/IL1MessageQueueV2.sol"; + +import {ScrollChainValidium} from "../validium/ScrollChainValidium.sol"; + +contract ScrollChainValidiumMock is ScrollChainValidium { + constructor( + uint64 _chainId, + address _messageQueueV2, + address _verifier + ) ScrollChainValidium(_chainId, _messageQueueV2, _verifier) {} + + /// @dev Internal function to finalize a bundle. + /// @param batchHeader The header of the last batch in this bundle. + /// @param totalL1MessagesPoppedOverall The number of messages processed after this bundle. + function _finalizeBundle( + bytes calldata batchHeader, + uint256 totalL1MessagesPoppedOverall, + bytes calldata + ) internal virtual override { + // actions before verification + (, bytes32 batchHash, uint256 batchIndex, ) = _beforeFinalizeBatch(batchHeader); + + bytes32 postStateRoot = stateRoots[batchIndex]; + bytes32 withdrawRoot = withdrawRoots[batchIndex]; + + // actions after verification + _afterFinalizeBatch(batchIndex, batchHash, totalL1MessagesPoppedOverall, postStateRoot, withdrawRoot); + } +} diff --git a/src/test/validium/FastWithdrawVault.t.sol b/src/test/validium/FastWithdrawVault.t.sol new file mode 100644 index 00000000..1860cce8 --- /dev/null +++ b/src/test/validium/FastWithdrawVault.t.sol @@ -0,0 +1,284 @@ +// SPDX-License-Identifier: MIT + +pragma solidity =0.8.24; + +import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; + +import {StringsUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; +import {ITransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; + +import {IL1ERC20Gateway} from "../../L1/gateways/IL1ERC20Gateway.sol"; +import {WrappedEther} from "../../L2/predeploys/WrappedEther.sol"; +import {L2StandardERC20Gateway} from "../../L2/gateways/L2StandardERC20Gateway.sol"; +import {ScrollStandardERC20} from "../../libraries/token/ScrollStandardERC20.sol"; +import {ScrollStandardERC20Factory} from "../../libraries/token/ScrollStandardERC20Factory.sol"; +import {IWETH} from "../../interfaces/IWETH.sol"; +import {FastWithdrawVault} from "../../validium/FastWithdrawVault.sol"; +import {L1ERC20GatewayValidium} from "../../validium/L1ERC20GatewayValidium.sol"; + +import {ValidiumTestBase} from "./ValidiumTestBase.t.sol"; + +// Helper contract to access private functions +contract FastWithdrawVaultHelper is FastWithdrawVault { + constructor(address _weth, address _gateway) FastWithdrawVault(_weth, _gateway) {} + + function getWithdrawTypehash() public pure returns (bytes32) { + return keccak256("Withdraw(address l1Token,address l2Token,address to,uint256 amount,bytes32 messageHash)"); + } + + function hashTypedDataV4(bytes32 structHash) public view returns (bytes32) { + return _hashTypedDataV4(structHash); + } +} + +contract FastWithdrawVaultTest is ValidiumTestBase { + event Withdraw(address indexed l1Token, address indexed l2Token, address to, uint256 amount, bytes32 messageHash); + + L1ERC20GatewayValidium private gateway; + + ScrollStandardERC20 private template; + ScrollStandardERC20Factory private factory; + L2StandardERC20Gateway private counterpartGateway; + + FastWithdrawVaultHelper private vault; + MockERC20 private l1Token; + WrappedEther private weth; + MockERC20 private l2Token; + + address private vaultAdmin; + + uint256 private sequencerPrivateKey; + address private sequencer; + + uint256 private userPrivateKey; + address private user; + + function setUp() public { + __ValidiumTestBase_setUp(1233); + + // Setup addresses and keys + vaultAdmin = address(this); + + sequencerPrivateKey = 0x1234567890123456789012345678901234567890123456789012345678901234; + userPrivateKey = 0x1234567890123456789012345678901234567890123456789012345678901235; + sequencer = hevm.addr(sequencerPrivateKey); + user = hevm.addr(userPrivateKey); + + // Deploy tokens + weth = new WrappedEther(); + l1Token = new MockERC20("Mock", "M", 18); + + // Deploy L2 contracts + template = new ScrollStandardERC20(); + factory = new ScrollStandardERC20Factory(address(template)); + counterpartGateway = new L2StandardERC20Gateway(address(1), address(1), address(1), address(factory)); + + // Deploy L1 contracts + gateway = _deployGateway(address(l1Messenger)); + vault = _deployVault(); + + // Initialize L1 contracts + gateway.initialize(); + vault.initialize(vaultAdmin, sequencer); + + // Setup token balances + l1Token.mint(address(vault), 100 ether); + weth.deposit{value: 100 ether}(); + weth.transfer(address(vault), 100 ether); + } + + function testInitialize() public { + // Test that the vault was initialized correctly in setUp + assertTrue(vault.hasRole(vault.DEFAULT_ADMIN_ROLE(), vaultAdmin)); + assertTrue(vault.hasRole(vault.SEQUENCER_ROLE(), sequencer)); + + assertEq(vault.weth(), address(weth)); + assertEq(vault.gateway(), address(gateway)); + + // Test role constants + assertEq(vault.SEQUENCER_ROLE(), keccak256("SEQUENCER_ROLE")); + } + + function testClaimFastWithdrawERC20( + address to, + uint256 amount, + bytes32 messageHash + ) public { + hevm.assume(to != address(0)); + hevm.assume(to.code.length == 0); + + amount = bound(amount, 1, 100 ether); + l2Token = MockERC20(gateway.getL2ERC20Address(address(l1Token))); + + // Create the struct hash + bytes32 structHash = keccak256( + abi.encode( + vault.getWithdrawTypehash(), + address(l1Token), + address(l2Token), // l2Token + to, + amount, + messageHash + ) + ); + + // Create the typed data hash + bytes32 hash = vault.hashTypedDataV4(structHash); + + // revert when the signature is invalid + hevm.expectRevert("ECDSA: invalid signature length"); + vault.claimFastWithdraw(address(l1Token), to, amount, messageHash, bytes("invalid")); + hevm.expectRevert("ECDSA: invalid signature"); + vault.claimFastWithdraw(address(l1Token), to, amount, messageHash, new bytes(65)); + + // revert when signer mismatch is not sequencer + bytes memory invalidSignature; + { + (uint8 v, bytes32 r, bytes32 s) = hevm.sign(userPrivateKey, hash); + invalidSignature = abi.encodePacked(r, s, v); + } + hevm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + StringsUpgradeable.toHexString(user), + " is missing role ", + StringsUpgradeable.toHexString(uint256(vault.SEQUENCER_ROLE()), 32) + ) + ); + vault.claimFastWithdraw(address(l1Token), to, amount, messageHash, invalidSignature); + + // Sign the hash with sequencer's private key + bytes memory signature; + { + (uint8 v, bytes32 r, bytes32 s) = hevm.sign(sequencerPrivateKey, hash); + signature = abi.encodePacked(r, s, v); + } + + // Call claimFastWithdraw and Expect the Withdraw event + uint256 toBalanceBefore = l1Token.balanceOf(to); + uint256 vaultBalanceBefore = l1Token.balanceOf(address(vault)); + hevm.expectEmit(true, true, true, true); + emit Withdraw(address(l1Token), address(l2Token), to, amount, messageHash); + vault.claimFastWithdraw(address(l1Token), to, amount, messageHash, signature); + uint256 toBalanceAfter = l1Token.balanceOf(to); + uint256 vaultBalanceAfter = l1Token.balanceOf(address(vault)); + + // Verify token transfer + assertEq(toBalanceAfter - toBalanceBefore, amount); + assertEq(vaultBalanceBefore - vaultBalanceAfter, amount); + + // Verify the withdraw is marked as processed + assertTrue(vault.isWithdrawn(structHash)); + + // revert when claim again on the same struct hash + hevm.expectRevert(FastWithdrawVault.ErrorWithdrawAlreadyProcessed.selector); + hevm.startPrank(sequencer); + vault.claimFastWithdraw(address(l1Token), to, amount, messageHash, signature); + hevm.stopPrank(); + } + + /* + function testClaimFastWithdrawWETH() public { + address wethAddr = address(weth); + address from = user; + address to = recipient; + uint256 amount = 50 ether; + bytes32 messageHash = keccak256("test_weth_message_hash"); + + // Create the struct hash + bytes32 structHash = keccak256( + abi.encode( + vault.getWithdrawTypehash(), + wethAddr, + address(l2Token), // l2Token + to, + amount, + messageHash + ) + ); + + // Create the typed data hash + bytes32 hash = vault.hashTypedDataV4(structHash); + + // Sign the hash with sequencer's private key + (uint8 v, bytes32 r, bytes32 s) = hevm.sign(sequencerPrivateKey, hash); + bytes memory signature = abi.encodePacked(r, s, v); + + // Mock the gateway to return l2Token address + hevm.mockCall( + address(0x123), // gateway address + abi.encodeWithSelector(IL1ERC20Gateway.getL2ERC20Address.selector, wethAddr), + abi.encode(address(l2Token)) + ); + + // Mock WETH withdraw function + hevm.mockCall(wethAddr, abi.encodeWithSelector(IWETH.withdraw.selector, amount), abi.encode()); + + // Expect the Withdraw event + hevm.expectEmit(true, true, true, true); + emit Withdraw(wethAddr, address(l2Token), to, amount, messageHash); + + // Call claimFastWithdraw + vault.claimFastWithdraw(wethAddr, to, amount, messageHash, signature); + + // Verify the withdraw is marked as processed + assertTrue(vault.isWithdrawn(structHash)); + } + */ + + function testWithdrawByAdmin(address recipient, uint256 amount) public { + hevm.assume(recipient != address(0)); + hevm.assume(recipient.code.length == 0); + amount = bound(amount, 1, 100 ether); + + // revert when caller is not admin + hevm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + StringsUpgradeable.toHexString(user), + " is missing role ", + StringsUpgradeable.toHexString(uint256(vault.DEFAULT_ADMIN_ROLE()), 32) + ) + ); + hevm.prank(user); + vault.withdraw(address(l1Token), recipient, amount); + + // Admin should be able to withdraw + uint256 balanceBefore = l1Token.balanceOf(recipient); + uint256 vaultBalanceBefore = l1Token.balanceOf(address(vault)); + hevm.prank(vaultAdmin); + vault.withdraw(address(l1Token), recipient, amount); + uint256 balanceAfter = l1Token.balanceOf(recipient); + uint256 vaultBalanceAfter = l1Token.balanceOf(address(vault)); + + // Verify token transfer + assertEq(balanceAfter - balanceBefore, amount); + assertEq(vaultBalanceBefore - vaultBalanceAfter, amount); + } + + function _deployGateway(address messenger) internal returns (L1ERC20GatewayValidium _gateway) { + _gateway = L1ERC20GatewayValidium(_deployProxy(address(0))); + + admin.upgrade( + ITransparentUpgradeableProxy(address(_gateway)), + address( + new L1ERC20GatewayValidium( + address(counterpartGateway), + address(messenger), + address(template), + address(factory) + ) + ) + ); + } + + function _deployVault() internal returns (FastWithdrawVaultHelper _vault) { + _vault = FastWithdrawVaultHelper(payable(_deployProxy(address(0)))); + + admin.upgrade( + ITransparentUpgradeableProxy(address(_vault)), + address(new FastWithdrawVaultHelper(address(weth), address(gateway))) + ); + } +} diff --git a/src/test/validium/L1ERC20GatewayValidium.t.sol b/src/test/validium/L1ERC20GatewayValidium.t.sol new file mode 100644 index 00000000..240909d3 --- /dev/null +++ b/src/test/validium/L1ERC20GatewayValidium.t.sol @@ -0,0 +1,441 @@ +// SPDX-License-Identifier: MIT + +pragma solidity =0.8.24; + +import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; + +import {ITransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +import {IL1ScrollMessenger} from "../../L1/IL1ScrollMessenger.sol"; +import {L2StandardERC20Gateway} from "../../L2/gateways/L2StandardERC20Gateway.sol"; +import {ScrollStandardERC20} from "../../libraries/token/ScrollStandardERC20.sol"; +import {ScrollStandardERC20Factory} from "../../libraries/token/ScrollStandardERC20Factory.sol"; +import {AddressAliasHelper} from "../../libraries/common/AddressAliasHelper.sol"; +import {IL1ERC20GatewayValidium} from "../../validium/IL1ERC20GatewayValidium.sol"; +import {IL2ERC20GatewayValidium} from "../../validium/IL2ERC20GatewayValidium.sol"; +import {L1ERC20GatewayValidium} from "../../validium/L1ERC20GatewayValidium.sol"; + +import {TransferReentrantToken} from "../mocks/tokens/TransferReentrantToken.sol"; +import {FeeOnTransferToken} from "../mocks/tokens/FeeOnTransferToken.sol"; +import {MockScrollMessenger} from "../mocks/MockScrollMessenger.sol"; +import {MockGatewayRecipient} from "../mocks/MockGatewayRecipient.sol"; + +import {ValidiumTestBase} from "./ValidiumTestBase.t.sol"; + +contract L1ERC20GatewayValidiumTest is ValidiumTestBase { + event FinalizeWithdrawERC20( + address indexed _l1Token, + address indexed _l2Token, + address indexed _from, + address _to, + uint256 _amount, + bytes _data + ); + event DepositERC20( + address indexed l1Token, + address indexed l2Token, + address indexed from, + bytes to, + uint256 amount, + bytes data + ); + + L1ERC20GatewayValidium private gateway; + + ScrollStandardERC20 private template; + ScrollStandardERC20Factory private factory; + L2StandardERC20Gateway private counterpartGateway; + + MockERC20 private l1Token; + MockERC20 private l2Token; + TransferReentrantToken private reentrantToken; + FeeOnTransferToken private feeToken; + + function setUp() public { + __ValidiumTestBase_setUp(1233); + + // Deploy tokens + l1Token = new MockERC20("Mock", "M", 18); + reentrantToken = new TransferReentrantToken("Reentrant", "R", 18); + feeToken = new FeeOnTransferToken("Fee", "F", 18); + + // Deploy L2 contracts + template = new ScrollStandardERC20(); + factory = new ScrollStandardERC20Factory(address(template)); + counterpartGateway = new L2StandardERC20Gateway(address(1), address(1), address(1), address(factory)); + + // Deploy L1 contracts + gateway = _deployGateway(address(l1Messenger)); + + // Initialize L1 contracts + gateway.initialize(); + + address[] memory addresses = new address[](1); + addresses[0] = address(gateway); + gatewayWhitelist.updateWhitelistStatus(addresses, true); + + // Prepare token balances + l2Token = MockERC20(gateway.getL2ERC20Address(address(l1Token))); + l1Token.mint(address(this), type(uint128).max); + l1Token.approve(address(gateway), type(uint256).max); + + reentrantToken.mint(address(this), type(uint128).max); + reentrantToken.approve(address(gateway), type(uint256).max); + + feeToken.mint(address(this), type(uint128).max); + feeToken.approve(address(gateway), type(uint256).max); + } + + function testInitialized() public { + // state in OwnableUpgradeable + assertEq(address(this), gateway.owner()); + + // state in ScrollGatewayBase + assertEq(address(l1Messenger), gateway.messenger()); + assertEq(address(0), gateway.router()); + assertEq(address(counterpartGateway), gateway.counterpart()); + + // state in L1ERC20GatewayValidium + assertEq(address(template), gateway.l2TokenImplementation()); + assertEq(address(factory), gateway.l2TokenFactory()); + + // revert when initializing again + hevm.expectRevert("Initializable: contract is already initialized"); + gateway.initialize(); + } + + function testGetL2ERC20Address(address l1Address) public { + assertEq( + gateway.getL2ERC20Address(l1Address), + factory.computeL2TokenAddress(address(counterpartGateway), l1Address) + ); + } + + function testDepositERC20( + uint256 amount, + bytes memory recipient, + uint256 gasLimit + ) public { + _deposit(address(this), amount, recipient, gasLimit); + } + + function testDepositERC20WithSender( + address sender, + uint256 amount, + bytes memory recipient, + uint256 gasLimit + ) public { + _deposit(sender, amount, recipient, gasLimit); + } + + function testDepositReentrantToken(uint256 amount) public { + // should revert, reentrant before transfer + reentrantToken.setReentrantCall( + address(gateway), + 0, + abi.encodeWithSignature( + "depositERC20(address,bytes,uint256,uint256)", + address(reentrantToken), + new bytes(0), + amount, + defaultGasLimit + ), + true + ); + amount = bound(amount, 1, reentrantToken.balanceOf(address(this))); + hevm.expectRevert("ReentrancyGuard: reentrant call"); + gateway.depositERC20(address(reentrantToken), new bytes(0), amount, defaultGasLimit); + + // should revert, reentrant after transfer + reentrantToken.setReentrantCall( + address(gateway), + 0, + abi.encodeWithSignature( + "depositERC20(address,bytes,uint256,uint256)", + address(reentrantToken), + new bytes(0), + amount, + defaultGasLimit + ), + false + ); + amount = bound(amount, 1, reentrantToken.balanceOf(address(this))); + hevm.expectRevert("ReentrancyGuard: reentrant call"); + gateway.depositERC20(address(reentrantToken), new bytes(0), amount, defaultGasLimit); + } + + function testFeeOnTransferTokenFailed(uint256 amount) public { + feeToken.setFeeRate(1e9); + amount = bound(amount, 1, feeToken.balanceOf(address(this))); + hevm.expectRevert(L1ERC20GatewayValidium.ErrorAmountIsZero.selector); + gateway.depositERC20(address(feeToken), new bytes(0), amount, defaultGasLimit); + } + + function testFeeOnTransferTokenSucceed(uint256 amount, uint256 feeRate) public { + feeRate = bound(feeRate, 0, 1e9 - 1); + amount = bound(amount, 1e9, feeToken.balanceOf(address(this))); + feeToken.setFeeRate(feeRate); + + // should succeed, for valid amount + uint256 balanceBefore = feeToken.balanceOf(address(gateway)); + uint256 fee = (amount * feeRate) / 1e9; + gateway.depositERC20(address(feeToken), new bytes(0), amount, defaultGasLimit); + uint256 balanceAfter = feeToken.balanceOf(address(gateway)); + assertEq(balanceBefore + amount - fee, balanceAfter); + } + + function testFinalizeWithdrawERC20FailedMocking( + address sender, + address recipient, + uint256 amount, + bytes memory dataToCall + ) public { + amount = bound(amount, 1, 100000); + + // revert when caller is not messenger + hevm.expectRevert(ErrorCallerIsNotMessenger.selector); + gateway.finalizeWithdrawERC20(address(l1Token), address(l2Token), sender, recipient, amount, dataToCall); + + MockScrollMessenger mockMessenger = new MockScrollMessenger(); + gateway = _deployGateway(address(mockMessenger)); + gateway.initialize(); + + // only call by counterpart + hevm.expectRevert(ErrorCallerIsNotCounterpartGateway.selector); + mockMessenger.callTarget( + address(gateway), + abi.encodeWithSelector( + gateway.finalizeWithdrawERC20.selector, + address(l1Token), + address(l2Token), + sender, + recipient, + amount, + dataToCall + ) + ); + + mockMessenger.setXDomainMessageSender(address(counterpartGateway)); + + // msg.value mismatch + hevm.expectRevert(L1ERC20GatewayValidium.ErrorMsgValueNotZero.selector); + mockMessenger.callTarget{value: 1}( + address(gateway), + abi.encodeWithSelector( + gateway.finalizeWithdrawERC20.selector, + address(l1Token), + address(l2Token), + sender, + recipient, + amount, + dataToCall + ) + ); + } + + function testFinalizeWithdrawERC20Failed( + address sender, + address recipient, + uint256 amount, + bytes memory dataToCall + ) public { + // blacklist some addresses + hevm.assume(recipient != address(0)); + + amount = bound(amount, 1, l1Token.balanceOf(address(this))); + + // deposit some token to L1ERC20GatewayValidium + gateway.depositERC20(address(l1Token), new bytes(0), amount, defaultGasLimit); + + // do finalize withdraw token + bytes memory message = abi.encodeWithSelector( + IL1ERC20GatewayValidium.finalizeWithdrawERC20.selector, + address(l1Token), + address(l2Token), + sender, + recipient, + amount, + dataToCall + ); + bytes memory xDomainCalldata = abi.encodeWithSignature( + "relayMessage(address,address,uint256,uint256,bytes)", + address(uint160(address(counterpartGateway)) + 1), + address(gateway), + 0, + 0, + message + ); + + prepareL2MessageRoot(keccak256(xDomainCalldata)); + + IL1ScrollMessenger.L2MessageProof memory proof; + proof.batchIndex = rollup.lastFinalizedBatchIndex(); + + // counterpart is not L2WETHGateway + // emit FailedRelayedMessage from L1ScrollMessenger + hevm.expectEmit(true, false, false, true); + emit FailedRelayedMessage(keccak256(xDomainCalldata)); + + uint256 gatewayBalance = l1Token.balanceOf(address(gateway)); + uint256 recipientBalance = l1Token.balanceOf(recipient); + assertBoolEq(false, l1Messenger.isL2MessageExecuted(keccak256(xDomainCalldata))); + l1Messenger.relayMessageWithProof( + address(uint160(address(counterpartGateway)) + 1), + address(gateway), + 0, + 0, + message, + proof + ); + assertEq(gatewayBalance, l1Token.balanceOf(address(gateway))); + assertEq(recipientBalance, l1Token.balanceOf(recipient)); + assertBoolEq(false, l1Messenger.isL2MessageExecuted(keccak256(xDomainCalldata))); + } + + function testFinalizeWithdrawERC20( + address sender, + uint256 amount, + bytes memory dataToCall + ) public { + MockGatewayRecipient recipient = new MockGatewayRecipient(); + + amount = bound(amount, 1, l1Token.balanceOf(address(this))); + + // deposit some token to L1ERC20GatewayValidium + gateway.depositERC20(address(l1Token), new bytes(0), amount, defaultGasLimit); + + // do finalize withdraw token + bytes memory message = abi.encodeWithSelector( + IL1ERC20GatewayValidium.finalizeWithdrawERC20.selector, + address(l1Token), + address(l2Token), + sender, + address(recipient), + amount, + dataToCall + ); + bytes memory xDomainCalldata = abi.encodeWithSignature( + "relayMessage(address,address,uint256,uint256,bytes)", + address(counterpartGateway), + address(gateway), + 0, + 0, + message + ); + + prepareL2MessageRoot(keccak256(xDomainCalldata)); + + IL1ScrollMessenger.L2MessageProof memory proof; + proof.batchIndex = rollup.lastFinalizedBatchIndex(); + + // emit FinalizeWithdrawERC20 from L1ERC20GatewayValidium + { + hevm.expectEmit(true, true, true, true); + emit FinalizeWithdrawERC20( + address(l1Token), + address(l2Token), + sender, + address(recipient), + amount, + dataToCall + ); + } + + // emit RelayedMessage from L1ScrollMessenger + { + hevm.expectEmit(true, false, false, true); + emit RelayedMessage(keccak256(xDomainCalldata)); + } + + uint256 gatewayBalance = l1Token.balanceOf(address(gateway)); + uint256 recipientBalance = l1Token.balanceOf(address(recipient)); + assertBoolEq(false, l1Messenger.isL2MessageExecuted(keccak256(xDomainCalldata))); + l1Messenger.relayMessageWithProof(address(counterpartGateway), address(gateway), 0, 0, message, proof); + assertEq(gatewayBalance - amount, l1Token.balanceOf(address(gateway))); + assertEq(recipientBalance + amount, l1Token.balanceOf(address(recipient))); + assertBoolEq(true, l1Messenger.isL2MessageExecuted(keccak256(xDomainCalldata))); + } + + function _deposit( + address from, + uint256 amount, + bytes memory recipient, + uint256 gasLimit + ) private { + amount = bound(amount, 0, l1Token.balanceOf(address(this))); + gasLimit = bound(gasLimit, defaultGasLimit / 2, defaultGasLimit); + setL2BaseFee(0); + + bytes memory message = abi.encodeWithSelector( + IL2ERC20GatewayValidium.finalizeDepositERC20Encrypted.selector, + address(l1Token), + address(l2Token), + from, + recipient, + amount, + abi.encode(true, abi.encode(new bytes(0), abi.encode(l1Token.symbol(), l1Token.name(), l1Token.decimals()))) + ); + bytes memory xDomainCalldata = abi.encodeWithSignature( + "relayMessage(address,address,uint256,uint256,bytes)", + address(gateway), + address(counterpartGateway), + 0, + 0, + message + ); + + if (amount == 0) { + hevm.expectRevert(L1ERC20GatewayValidium.ErrorAmountIsZero.selector); + if (from == address(this)) { + gateway.depositERC20(address(l1Token), recipient, amount, gasLimit); + } else { + gateway.depositERC20(address(l1Token), from, recipient, amount, gasLimit); + } + } else { + // emit QueueTransaction from L1MessageQueueV2 + { + hevm.expectEmit(true, true, false, true); + address sender = AddressAliasHelper.applyL1ToL2Alias(address(l1Messenger)); + emit QueueTransaction(sender, address(l2Messenger), 0, 0, gasLimit, xDomainCalldata); + } + + // emit SentMessage from L1ScrollMessenger + { + hevm.expectEmit(true, true, false, true); + emit SentMessage(address(gateway), address(counterpartGateway), 0, 0, gasLimit, message); + } + + // emit DepositERC20 from L1ERC20GatewayValidium + hevm.expectEmit(true, true, true, true); + emit DepositERC20(address(l1Token), address(l2Token), from, recipient, amount, new bytes(0)); + + uint256 gatewayBalance = l1Token.balanceOf(address(gateway)); + uint256 feeVaultBalance = address(feeVault).balance; + assertEq(l1Messenger.messageSendTimestamp(keccak256(xDomainCalldata)), 0); + if (from == address(this)) { + gateway.depositERC20(address(l1Token), recipient, amount, gasLimit); + } else { + gateway.depositERC20(address(l1Token), from, recipient, amount, gasLimit); + } + assertEq(amount + gatewayBalance, l1Token.balanceOf(address(gateway))); + assertEq(feeVaultBalance, address(feeVault).balance); + assertGt(l1Messenger.messageSendTimestamp(keccak256(xDomainCalldata)), 0); + } + } + + function _deployGateway(address messenger) internal returns (L1ERC20GatewayValidium _gateway) { + _gateway = L1ERC20GatewayValidium(_deployProxy(address(0))); + + admin.upgrade( + ITransparentUpgradeableProxy(address(_gateway)), + address( + new L1ERC20GatewayValidium( + address(counterpartGateway), + address(messenger), + address(template), + address(factory) + ) + ) + ); + } +} diff --git a/src/test/validium/L1WETHGatewayValidium.t.sol b/src/test/validium/L1WETHGatewayValidium.t.sol new file mode 100644 index 00000000..1a61fe5d --- /dev/null +++ b/src/test/validium/L1WETHGatewayValidium.t.sol @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: MIT + +pragma solidity =0.8.24; + +import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; + +import {ITransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +import {IL1ScrollMessenger} from "../../L1/IL1ScrollMessenger.sol"; +import {L2StandardERC20Gateway} from "../../L2/gateways/L2StandardERC20Gateway.sol"; +import {WrappedEther} from "../../L2/predeploys/WrappedEther.sol"; +import {ScrollStandardERC20} from "../../libraries/token/ScrollStandardERC20.sol"; +import {ScrollStandardERC20Factory} from "../../libraries/token/ScrollStandardERC20Factory.sol"; +import {AddressAliasHelper} from "../../libraries/common/AddressAliasHelper.sol"; +import {IL1ERC20GatewayValidium} from "../../validium/IL1ERC20GatewayValidium.sol"; +import {IL2ERC20GatewayValidium} from "../../validium/IL2ERC20GatewayValidium.sol"; +import {L1ERC20GatewayValidium} from "../../validium/L1ERC20GatewayValidium.sol"; +import {L1WETHGatewayValidium} from "../../validium/L1WETHGatewayValidium.sol"; + +import {TransferReentrantToken} from "../mocks/tokens/TransferReentrantToken.sol"; +import {FeeOnTransferToken} from "../mocks/tokens/FeeOnTransferToken.sol"; +import {MockScrollMessenger} from "../mocks/MockScrollMessenger.sol"; +import {MockGatewayRecipient} from "../mocks/MockGatewayRecipient.sol"; + +import {ValidiumTestBase} from "./ValidiumTestBase.t.sol"; + +contract L1WETHGatewayValidiumTest is ValidiumTestBase { + event FinalizeWithdrawERC20( + address indexed _l1Token, + address indexed _l2Token, + address indexed _from, + address _to, + uint256 _amount, + bytes _data + ); + event DepositERC20( + address indexed l1Token, + address indexed l2Token, + address indexed from, + bytes to, + uint256 amount, + bytes data + ); + + L1WETHGatewayValidium private wethGateway; + L1ERC20GatewayValidium private gateway; + + ScrollStandardERC20 private template; + ScrollStandardERC20Factory private factory; + L2StandardERC20Gateway private counterpartGateway; + + WrappedEther private weth; + MockERC20 private l2Token; + + function setUp() public { + __ValidiumTestBase_setUp(1233); + + // Deploy tokens + weth = new WrappedEther(); + + // Deploy L2 contracts + template = new ScrollStandardERC20(); + factory = new ScrollStandardERC20Factory(address(template)); + counterpartGateway = new L2StandardERC20Gateway(address(1), address(1), address(1), address(factory)); + + // Deploy L1 contracts + gateway = _deployGateway(address(l1Messenger)); + wethGateway = new L1WETHGatewayValidium(address(weth), address(gateway)); + + // Initialize L1 contracts + gateway.initialize(); + + address[] memory addresses = new address[](1); + addresses[0] = address(gateway); + gatewayWhitelist.updateWhitelistStatus(addresses, true); + + l2Token = MockERC20(gateway.getL2ERC20Address(address(weth))); + } + + function testDeposit(uint256 amount, bytes memory recipient) public { + _deposit(address(this), amount, recipient, 1000000); + } + + function _deposit( + address from, + uint256 amount, + bytes memory recipient, + uint256 gasLimit + ) private { + amount = bound(amount, 0, address(this).balance / 2); + setL2BaseFee(0); + + bytes memory message = abi.encodeWithSelector( + IL2ERC20GatewayValidium.finalizeDepositERC20Encrypted.selector, + address(weth), + address(l2Token), + from, + recipient, + amount, + abi.encode(true, abi.encode(new bytes(0), abi.encode(weth.symbol(), weth.name(), weth.decimals()))) + ); + bytes memory xDomainCalldata = abi.encodeWithSignature( + "relayMessage(address,address,uint256,uint256,bytes)", + address(gateway), + address(counterpartGateway), + 0, + 0, + message + ); + + if (amount == 0) { + hevm.expectRevert(L1ERC20GatewayValidium.ErrorAmountIsZero.selector); + wethGateway.deposit(recipient, amount); + } else { + // revert when ErrorInsufficientValue + hevm.expectRevert(L1WETHGatewayValidium.ErrorInsufficientValue.selector); + wethGateway.deposit{value: amount - 1}(recipient, amount); + + // emit QueueTransaction from L1MessageQueueV2 + { + hevm.expectEmit(true, true, false, true); + address sender = AddressAliasHelper.applyL1ToL2Alias(address(l1Messenger)); + emit QueueTransaction(sender, address(l2Messenger), 0, 0, gasLimit, xDomainCalldata); + } + + // emit SentMessage from L1ScrollMessenger + { + hevm.expectEmit(true, true, false, true); + emit SentMessage(address(gateway), address(counterpartGateway), 0, 0, gasLimit, message); + } + + // emit DepositERC20 from L1ERC20GatewayValidium + hevm.expectEmit(true, true, true, true); + emit DepositERC20(address(weth), address(l2Token), from, recipient, amount, new bytes(0)); + + uint256 ethBalance = address(this).balance; + uint256 gatewayBalance = weth.balanceOf(address(gateway)); + uint256 feeVaultBalance = address(feeVault).balance; + assertEq(l1Messenger.messageSendTimestamp(keccak256(xDomainCalldata)), 0); + wethGateway.deposit{value: amount}(recipient, amount); + assertEq(ethBalance - amount, address(this).balance); + assertEq(amount + gatewayBalance, weth.balanceOf(address(gateway))); + assertEq(feeVaultBalance, address(feeVault).balance); + assertGt(l1Messenger.messageSendTimestamp(keccak256(xDomainCalldata)), 0); + } + } + + function _deployGateway(address messenger) internal returns (L1ERC20GatewayValidium _gateway) { + _gateway = L1ERC20GatewayValidium(_deployProxy(address(0))); + + admin.upgrade( + ITransparentUpgradeableProxy(address(_gateway)), + address( + new L1ERC20GatewayValidium( + address(counterpartGateway), + address(messenger), + address(template), + address(factory) + ) + ) + ); + } +} diff --git a/src/test/validium/ValidiumTestBase.t.sol b/src/test/validium/ValidiumTestBase.t.sol new file mode 100644 index 00000000..3cb89861 --- /dev/null +++ b/src/test/validium/ValidiumTestBase.t.sol @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: MIT + +pragma solidity =0.8.24; + +import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol"; + +import {ITransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +import {L1MessageQueueV2} from "../../L1/rollup/L1MessageQueueV2.sol"; +import {SystemConfig} from "../../L1/system-contract/SystemConfig.sol"; +import {Whitelist} from "../../L2/predeploys/Whitelist.sol"; +import {L2ScrollMessenger} from "../../L2/L2ScrollMessenger.sol"; + +import {EmptyL1MessageQueueV1} from "../../validium/EmptyL1MessageQueueV1.sol"; +import {L1ScrollMessengerValidium} from "../../validium/L1ScrollMessengerValidium.sol"; +import {ScrollChainValidium} from "../../validium/ScrollChainValidium.sol"; + +import {ScrollChainValidiumMock} from "../../mocks/ScrollChainValidiumMock.sol"; +import {MockRollupVerifier} from "../mocks/MockRollupVerifier.sol"; +import {ScrollTestBase} from "../ScrollTestBase.t.sol"; + +// solhint-disable no-inline-assembly + +abstract contract ValidiumTestBase is ScrollTestBase { + // from L1MessageQueueV2 + event QueueTransaction( + address indexed sender, + address indexed target, + uint256 value, + uint64 queueIndex, + uint256 gasLimit, + bytes data + ); + + // from L1ScrollMessenger + event SentMessage( + address indexed sender, + address indexed target, + uint256 value, + uint256 messageNonce, + uint256 gasLimit, + bytes message + ); + event RelayedMessage(bytes32 indexed messageHash); + event FailedRelayedMessage(bytes32 indexed messageHash); + + /********** + * Errors * + **********/ + + // from IScrollGateway + error ErrorZeroAddress(); + error ErrorCallerIsNotMessenger(); + error ErrorCallerIsNotCounterpartGateway(); + error ErrorNotInDropMessageContext(); + + uint32 internal constant defaultGasLimit = 1000000; + + SystemConfig internal systemConfig; + Whitelist internal gatewayWhitelist; + L1ScrollMessengerValidium internal l1Messenger; + EmptyL1MessageQueueV1 internal messageQueueV1; + L1MessageQueueV2 internal messageQueueV2; + ScrollChainValidium internal rollup; + + MockRollupVerifier internal verifier; + + address internal feeVault; + + L2ScrollMessenger internal l2Messenger; + + function __ValidiumTestBase_setUp(uint32 _chainId) internal { + __ScrollTestBase_setUp(); + + feeVault = address(uint160(address(this)) - 1); + + // deploy proxy and contracts in L1 + systemConfig = SystemConfig(_deployProxy(address(0))); + l1Messenger = L1ScrollMessengerValidium(payable(_deployProxy(address(0)))); + messageQueueV1 = new EmptyL1MessageQueueV1(); + messageQueueV2 = L1MessageQueueV2(_deployProxy(address(0))); + rollup = ScrollChainValidiumMock(_deployProxy(address(0))); + gatewayWhitelist = new Whitelist(address(this)); + verifier = new MockRollupVerifier(); + + // deploy proxy and contracts in L2 + l2Messenger = L2ScrollMessenger(payable(_deployProxy(address(0)))); + + // Upgrade the SystemConfig implementation and initialize + admin.upgrade(ITransparentUpgradeableProxy(address(systemConfig)), address(new SystemConfig())); + systemConfig.initialize( + address(this), + address(uint160(1)), + SystemConfig.MessageQueueParameters({maxGasLimit: 1000000, baseFeeOverhead: 0, baseFeeScalar: 0}), + SystemConfig.EnforcedBatchParameters({maxDelayEnterEnforcedMode: 0, maxDelayMessageQueue: 0}) + ); + + // Upgrade the L1ScrollMessengerValidium implementation and initialize + admin.upgrade( + ITransparentUpgradeableProxy(address(l1Messenger)), + address( + new L1ScrollMessengerValidium( + address(l2Messenger), + address(rollup), + address(messageQueueV2), + address(gatewayWhitelist) + ) + ) + ); + l1Messenger.initialize(address(0), feeVault, address(0), address(0)); + + // Upgrade the L1MessageQueueV2 implementation and initialize + admin.upgrade( + ITransparentUpgradeableProxy(address(messageQueueV2)), + address( + new L1MessageQueueV2( + address(l1Messenger), + address(rollup), + address(0), + address(messageQueueV1), + address(systemConfig) + ) + ) + ); + messageQueueV2.initialize(); + + // Upgrade the ScrollChain implementation and initialize + admin.upgrade( + ITransparentUpgradeableProxy(address(rollup)), + address(new ScrollChainValidium(_chainId, address(messageQueueV2), address(verifier))) + ); + rollup.initialize(address(this)); + + // Make nonzero block.timestamp + hevm.warp(1); + } + + function prepareL2MessageRoot(bytes32 messageHash) internal { + rollup.grantRole(rollup.SEQUENCER_ROLE(), address(0)); + rollup.grantRole(rollup.PROVER_ROLE(), address(0)); + + // import genesis batch + bytes memory batchHeader0 = abi.encodePacked( + bytes1(uint8(0)), // version + uint64(0), // batchIndex + bytes32(0), // parentBatchHash + keccak256("0"), // postStateRoot + bytes32(0), // withdrawRoot + bytes32(0) // commitment + ); + rollup.grantRole(rollup.GENESIS_IMPORTER_ROLE(), address(this)); + rollup.importGenesisBatch(batchHeader0); + bytes32 batchHash0 = rollup.committedBatches(0); + + // commit one batch + bytes[] memory chunks = new bytes[](1); + bytes memory chunk0 = new bytes(1 + 60); + chunk0[0] = bytes1(uint8(1)); // one block in this chunk + chunks[0] = chunk0; + hevm.startPrank(address(0)); + rollup.commitBatch(4, batchHash0, keccak256("1"), messageHash, new bytes(32)); + hevm.stopPrank(); + + bytes memory batchHeader1 = abi.encodePacked( + bytes1(uint8(4)), // version + uint64(1), // batchIndex + batchHash0, // parentBatchHash + keccak256("1"), // postStateRoot + messageHash, // withdrawRoot + bytes32(0) // commitment + ); + hevm.startPrank(address(0)); + rollup.finalizeBundle(batchHeader1, 0, new bytes(0)); + hevm.stopPrank(); + + rollup.lastFinalizedBatchIndex(); + } + + function setL2BaseFee(uint256 feePerGas) internal { + setL2BaseFee(feePerGas, 1000000); + } + + function setL2BaseFee(uint256 feePerGas, uint256 gasLimit) internal { + systemConfig.updateMessageQueueParameters( + SystemConfig.MessageQueueParameters({ + maxGasLimit: uint32(gasLimit), + baseFeeOverhead: uint112(0), + baseFeeScalar: uint112(1 ether) + }) + ); + hevm.fee(feePerGas); + assertEq(messageQueueV2.estimateL2BaseFee(), feePerGas); + } +} diff --git a/src/validium/EmptyL1MessageQueueV1.sol b/src/validium/EmptyL1MessageQueueV1.sol new file mode 100644 index 00000000..1a3bf614 --- /dev/null +++ b/src/validium/EmptyL1MessageQueueV1.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity =0.8.24; + +contract EmptyL1MessageQueueV1 { + function nextCrossDomainMessageIndex() external pure returns (uint256) { + return 0; + } +} diff --git a/src/validium/FastWithdrawVault.sol b/src/validium/FastWithdrawVault.sol new file mode 100644 index 00000000..94240a9e --- /dev/null +++ b/src/validium/FastWithdrawVault.sol @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: MIT + +pragma solidity =0.8.24; + +import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; +import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import {SafeERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; +import {AddressUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; +import {EIP712Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol"; +import {ECDSAUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/ECDSAUpgradeable.sol"; + +import {IL1ERC20Gateway} from "../L1/gateways/IL1ERC20Gateway.sol"; +import {IWETH} from "../interfaces/IWETH.sol"; + +/// @title FastWithdrawVault +/// @notice The vault for fast withdrawals from L2 to L1. +/// @dev For consistency with our existing contracts, we use "L1" for the host layer, and "L2" for the validium layer. +/// For most deployments, these should be mapped as "L1" = Scroll (L2), "L2" = Validium (L3). +/// @dev This contract is used to fast withdraw tokens from L2 to L1 with a permit from sequencer. +/// The process for a fast withdrawal is: +/// 1. The user on L2 initiates a withdraw request and sets the recipient address as this `FastWithdrawVault` contract, +/// also sending the proper amount of tokens. +/// 2. The sequencer signs the withdraw request and sends it to the vault. +/// 3. The vault verifies the signature and the message hash, and then withdraws the tokens from L2 to L1. +contract FastWithdrawVault is AccessControlUpgradeable, ReentrancyGuardUpgradeable, EIP712Upgradeable { + using SafeERC20Upgradeable for IERC20Upgradeable; + + /********** + * Events * + **********/ + + /// @notice Emitted when a withdraw is processed. + /// @param l1Token The address of the L1 token. + /// @param l2Token The address of the L2 token. + /// @param to The address of the recipient. + /// @param amount The amount of tokens withdrawn. + /// @param messageHash The hash of the message. + event Withdraw(address indexed l1Token, address indexed l2Token, address to, uint256 amount, bytes32 messageHash); + + /********** + * Errors * + **********/ + + /// @dev Thrown when the given withdraw message has already been processed. + error ErrorWithdrawAlreadyProcessed(); + + /************* + * Constants * + *************/ + + /// @dev The typehash of the `Withdraw` struct. + // solhint-disable-next-line var-name-mixedcase + bytes32 private constant _WITHDRAW_TYPEHASH = + keccak256("Withdraw(address l1Token,address l2Token,address to,uint256 amount,bytes32 messageHash)"); + + /// @notice The role of the sequencer. + bytes32 public constant SEQUENCER_ROLE = keccak256("SEQUENCER_ROLE"); + + /*********************** + * Immutable Variables * + ***********************/ + + /// @notice The address of the WETH token. + address public immutable weth; + + /// @notice The address of the `L1ERC20Gateway` contract. + address public immutable gateway; + + /********************* + * Storage Variables * + *********************/ + + /// @notice Mapping from message hash to whether the message has been withdrawn. + mapping(bytes32 => bool) public isWithdrawn; + + /*************** + * Constructor * + ***************/ + + /// @notice Initializes the implementation contract. + /// @param _gateway The address of the `L1ERC20Gateway` contract. + constructor(address _weth, address _gateway) { + weth = _weth; + gateway = _gateway; + + _disableInitializers(); + } + + /// @notice Initializes the contract storage. + /// @param _admin The address of the admin. + /// @param _sequencer The address of the sequencer. + function initialize(address _admin, address _sequencer) external initializer { + __Context_init(); + __ERC165_init(); + __AccessControl_init(); + __ReentrancyGuard_init(); + __EIP712_init("FastWithdrawVault", "1"); + + _grantRole(DEFAULT_ADMIN_ROLE, _admin); + _grantRole(SEQUENCER_ROLE, _sequencer); + } + + /***************************** + * Public Mutating Functions * + *****************************/ + + receive() external payable {} + + /// @notice Fast withdraw some tokens from L2 to L1 with signature from sequencer. + /// @param l1Token The address of the L1 token. + /// @param to The address of the recipient. + /// @param amount The amount of tokens to withdraw. + /// @param messageHash The hash of the message, which is the corresponding withdraw message hash in L2. + /// @param signature The signature of the message from sequencer. + function claimFastWithdraw( + address l1Token, + address to, + uint256 amount, + bytes32 messageHash, + bytes memory signature + ) external nonReentrant { + address l2Token = IL1ERC20Gateway(gateway).getL2ERC20Address(l1Token); + bytes32 structHash = keccak256(abi.encode(_WITHDRAW_TYPEHASH, l1Token, l2Token, to, amount, messageHash)); + if (isWithdrawn[structHash]) revert ErrorWithdrawAlreadyProcessed(); + isWithdrawn[structHash] = true; + + bytes32 hash = _hashTypedDataV4(structHash); + address signer = ECDSAUpgradeable.recover(hash, signature); + _checkRole(SEQUENCER_ROLE, signer); + + if (l1Token == weth) { + IWETH(weth).withdraw(amount); + AddressUpgradeable.sendValue(payable(to), amount); + } else { + IERC20Upgradeable(l1Token).safeTransfer(to, amount); + } + + emit Withdraw(l1Token, l2Token, to, amount, messageHash); + } + + /************************ + * Restricted Functions * + ************************/ + + /// @notice Withdraw some tokens from the vault by admin. + /// @param token The address of the token. + /// @param recipient The address of the recipient. + /// @param amount The amount of tokens to withdraw. + function withdraw( + address token, + address recipient, + uint256 amount + ) external nonReentrant onlyRole(DEFAULT_ADMIN_ROLE) { + IERC20Upgradeable(token).safeTransfer(recipient, amount); + } +} diff --git a/src/validium/IL1ERC20GatewayValidium.sol b/src/validium/IL1ERC20GatewayValidium.sol new file mode 100644 index 00000000..314e8c55 --- /dev/null +++ b/src/validium/IL1ERC20GatewayValidium.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +interface IL1ERC20GatewayValidium { + /********** + * Events * + **********/ + + /// @notice Emitted when ERC20 token is withdrawn from L2 to L1 and transfer to recipient. + /// @param l1Token The address of the token in L1. + /// @param l2Token The address of the token in L2. + /// @param from The address of sender in L2. + /// @param to The address of recipient in L1. + /// @param amount The amount of token withdrawn from L2 to L1. + /// @param data The optional calldata passed to recipient in L1. + event FinalizeWithdrawERC20( + address indexed l1Token, + address indexed l2Token, + address indexed from, + address to, + uint256 amount, + bytes data + ); + + /// @notice Emitted when someone deposit ERC20 token from L1 to L2. + /// @param l1Token The address of the token in L1. + /// @param l2Token The address of the token in L2. + /// @param from The address of sender in L1. + /// @param to The encrypted address of recipient in L2. + /// @param amount The amount of token will be deposited from L1 to L2. + /// @param data The optional calldata passed to recipient in L2. + event DepositERC20( + address indexed l1Token, + address indexed l2Token, + address indexed from, + bytes to, + uint256 amount, + bytes data + ); + + /************************* + * Public View Functions * + *************************/ + + /// @notice Return the corresponding l2 token address given l1 token address. + /// @param _l1Token The address of l1 token. + function getL2ERC20Address(address _l1Token) external view returns (address); + + /***************************** + * Public Mutating Functions * + *****************************/ + + /// @notice Deposit some token to a recipient's account on L2. + /// @dev Make this function payable to send relayer fee in Ether. + /// @param _token The address of token in L1. + /// @param _to The encrypted address of recipient's account on L2. + /// @param _amount The amount of token to transfer. + /// @param _gasLimit Gas limit required to complete the deposit on L2. + function depositERC20( + address _token, + bytes memory _to, + uint256 _amount, + uint256 _gasLimit + ) external payable; + + /// @notice Deposit some token to a recipient's account on L2. + /// @dev Make this function payable to send relayer fee in Ether. + /// @param _token The address of token in L1. + /// @param _realSender The address of real sender in L1. + /// @param _to The encrypted address of recipient's account on L2. + /// @param _amount The amount of token to transfer. + /// @param _gasLimit Gas limit required to complete the deposit on L2. + function depositERC20( + address _token, + address _realSender, + bytes memory _to, + uint256 _amount, + uint256 _gasLimit + ) external payable; + + /// @notice Complete ERC20 withdraw from L2 to L1 and send fund to recipient's account in L1. + /// @dev Make this function payable to handle WETH deposit/withdraw. + /// The function should only be called by L1ScrollMessenger. + /// The function should also only be called by L2ERC20Gateway in L2. + /// @param _l1Token The address of corresponding L1 token. + /// @param _l2Token The address of corresponding L2 token. + /// @param _from The address of account who withdraw the token in L2. + /// @param _to The address of recipient in L1 to receive the token. + /// @param _amount The amount of the token to withdraw. + /// @param _data Optional data to forward to recipient's account. + function finalizeWithdrawERC20( + address _l1Token, + address _l2Token, + address _from, + address _to, + uint256 _amount, + bytes calldata _data + ) external payable; +} diff --git a/src/validium/IL2ERC20GatewayValidium.sol b/src/validium/IL2ERC20GatewayValidium.sol new file mode 100644 index 00000000..aecf1f91 --- /dev/null +++ b/src/validium/IL2ERC20GatewayValidium.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +import {IL2ERC20Gateway} from "../L2/gateways/IL2ERC20Gateway.sol"; + +interface IL2ERC20GatewayValidium is IL2ERC20Gateway { + /// @notice Complete a deposit from L1 to L2 and send fund to recipient's account in L2. + /// @dev Make this function payable to handle WETH deposit/withdraw. + /// The function should only be called by L2ScrollMessenger. + /// The function should also only be called by L1ERC20Gateway in L1. + /// @dev This function is not implemented. Instead, it is used to signal to the sequencer + // that the target address is encrypted. The sequencer should then decrypt the address + // and call the standard `finalizeDepositERC20` function with the decrypted address. + /// @param l1Token The address of corresponding L1 token. + /// @param l2Token The address of corresponding L2 token. + /// @param from The address of account who deposits the token in L1. + /// @param to The encrypted address of recipient in L2 to receive the token. + /// @param amount The amount of the token to deposit. + /// @param data Optional data to forward to recipient's account. + function finalizeDepositERC20Encrypted( + address l1Token, + address l2Token, + address from, + bytes memory to, + uint256 amount, + bytes calldata data + ) external payable; +} diff --git a/src/validium/IScrollChainValidium.sol b/src/validium/IScrollChainValidium.sol new file mode 100644 index 00000000..f7013d18 --- /dev/null +++ b/src/validium/IScrollChainValidium.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +interface IScrollChainValidium { + /********** + * Events * + **********/ + + /// @notice Emitted when a new batch is committed. + /// @param batchIndex The index of the batch. + /// @param batchHash The hash of the batch. + event CommitBatch(uint256 indexed batchIndex, bytes32 indexed batchHash); + + /// @notice revert a range of batches. + /// @param startBatchIndex The start batch index of the range (inclusive). + /// @param finishBatchIndex The finish batch index of the range (inclusive). + event RevertBatch(uint256 indexed startBatchIndex, uint256 indexed finishBatchIndex); + + /// @notice Emitted when a batch is finalized. + /// @param batchIndex The index of the batch. + /// @param batchHash The hash of the batch + /// @param stateRoot The state root on layer 2 after this batch. + /// @param withdrawRoot The merkle root on layer2 after this batch. + event FinalizeBatch(uint256 indexed batchIndex, bytes32 indexed batchHash, bytes32 stateRoot, bytes32 withdrawRoot); + + /************************* + * Public View Functions * + *************************/ + + /// @return The latest finalized batch index. + function lastFinalizedBatchIndex() external view returns (uint256); + + /// @return The latest committed batch index. + function lastCommittedBatchIndex() external view returns (uint256); + + /// @param batchIndex The index of the batch. + /// @return The batch hash of a committed batch. + function committedBatches(uint256 batchIndex) external view returns (bytes32); + + /// @param batchIndex The index of the batch. + /// @return The state root of a committed batch. + function stateRoots(uint256 batchIndex) external view returns (bytes32); + + /// @param batchIndex The index of the batch. + /// @return The message root of a committed batch. + function withdrawRoots(uint256 batchIndex) external view returns (bytes32); + + /// @param batchIndex The index of the batch. + /// @return Whether the batch is finalized by batch index. + function isBatchFinalized(uint256 batchIndex) external view returns (bool); + + /***************************** + * Public Mutating Functions * + *****************************/ + + /// @notice Commit a pending batch. + /// @param version The version of this batch. + /// @param parentBatchHash The hash of parent batch. + /// @param stateRoot The state root after this batch. + /// @param withdrawRoot The withdraw trie root after this batch. + /// @param commitment The data commitment. + function commitBatch( + uint8 version, + bytes32 parentBatchHash, + bytes32 stateRoot, + bytes32 withdrawRoot, + bytes calldata commitment + ) external; + + /// @notice Revert pending batches. + /// @dev one can only revert unfinalized batches. + /// @param batchHeader The header of the first batch we want to revert. + function revertBatch(bytes calldata batchHeader) external; + + /// @notice Finalize a list of committed batches (i.e. bundle) on layer 1. + /// @param batchHeader The header of the last batch in this bundle. + /// @param totalL1MessagesPoppedOverall The number of messages processed after this bundle. + /// @param aggrProof The aggregation proof for current bundle. + function finalizeBundle( + bytes calldata batchHeader, + uint256 totalL1MessagesPoppedOverall, + bytes calldata aggrProof + ) external; +} diff --git a/src/validium/L1ERC20GatewayValidium.sol b/src/validium/L1ERC20GatewayValidium.sol new file mode 100644 index 00000000..8bdba236 --- /dev/null +++ b/src/validium/L1ERC20GatewayValidium.sol @@ -0,0 +1,228 @@ +// SPDX-License-Identifier: MIT + +pragma solidity =0.8.24; + +import {ClonesUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/ClonesUpgradeable.sol"; +import {IERC20MetadataUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; +import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import {SafeERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; + +import {IL1ScrollMessenger} from "../L1/IL1ScrollMessenger.sol"; +import {IL1ERC20GatewayValidium} from "./IL1ERC20GatewayValidium.sol"; +import {IL2ERC20GatewayValidium} from "./IL2ERC20GatewayValidium.sol"; + +import {ScrollGatewayBase} from "../libraries/gateway/ScrollGatewayBase.sol"; + +/// @title L1ERC20GatewayValidium +contract L1ERC20GatewayValidium is ScrollGatewayBase, IL1ERC20GatewayValidium { + using SafeERC20Upgradeable for IERC20Upgradeable; + + /********** + * Errors * + **********/ + + /// @dev Error thrown when msg.value is not zero. + error ErrorMsgValueNotZero(); + + /// @dev Error thrown when l2 token address is zero. + error ErrorL2TokenAddressIsZero(); + + /// @dev Error thrown when l2 token address mismatch. + error ErrorL2TokenMismatch(); + + /// @dev Error thrown when amount is zero. + error ErrorAmountIsZero(); + + /************* + * Constants * + *************/ + + /// @notice The address of ScrollStandardERC20 implementation in L2. + address public immutable l2TokenImplementation; + + /// @notice The address of ScrollStandardERC20Factory contract in L2. + address public immutable l2TokenFactory; + + /************* + * Variables * + *************/ + + /// @notice Mapping from l1 token address to l2 token address. + /// @dev This is not necessary, since we can compute the address directly. But, we use this mapping + /// to keep track on whether we have deployed the token in L2 using the L2ScrollStandardERC20Factory and + /// pass deploy data on first call to the token. + mapping(address => address) private tokenMapping; + + /*************** + * Constructor * + ***************/ + + /// @notice Constructor for `L1StandardERC20Gateway` implementation contract. + /// + /// @param _counterpart The address of `L2StandardERC20Gateway` contract in L2. + /// @param _messenger The address of `L1ScrollMessenger` contract in L1. + /// @param _l2TokenImplementation The address of `ScrollStandardERC20` implementation in L2. + /// @param _l2TokenFactory The address of `ScrollStandardERC20Factory` contract in L2. + constructor( + address _counterpart, + address _messenger, + address _l2TokenImplementation, + address _l2TokenFactory + ) ScrollGatewayBase(_counterpart, address(0), _messenger) { + _disableInitializers(); + + l2TokenImplementation = _l2TokenImplementation; + l2TokenFactory = _l2TokenFactory; + } + + /// @notice Initialize the storage of L1ERC20GatewayValidium. + function initialize() external initializer { + ScrollGatewayBase._initialize(address(0), address(0), address(0)); + } + + /************************* + * Public View Functions * + *************************/ + + /// @inheritdoc IL1ERC20GatewayValidium + function getL2ERC20Address(address _l1Token) public view override returns (address) { + // In StandardERC20Gateway, all corresponding l2 tokens are depoyed by Create2 with salt, + // we can calculate the l2 address directly. + bytes32 _salt = keccak256(abi.encodePacked(counterpart, keccak256(abi.encodePacked(_l1Token)))); + + return ClonesUpgradeable.predictDeterministicAddress(l2TokenImplementation, _salt, l2TokenFactory); + } + + /***************************** + * Public Mutating Functions * + *****************************/ + + /// @inheritdoc IL1ERC20GatewayValidium + function depositERC20( + address _token, + bytes memory _to, + uint256 _amount, + uint256 _gasLimit + ) external payable override { + _deposit(_token, _msgSender(), _to, _amount, new bytes(0), _gasLimit); + } + + /// @inheritdoc IL1ERC20GatewayValidium + function depositERC20( + address _token, + address _realSender, + bytes memory _to, + uint256 _amount, + uint256 _gasLimit + ) external payable override { + _deposit(_token, _realSender, _to, _amount, new bytes(0), _gasLimit); + } + + /// @inheritdoc IL1ERC20GatewayValidium + function finalizeWithdrawERC20( + address _l1Token, + address _l2Token, + address _from, + address _to, + uint256 _amount, + bytes calldata _data + ) external payable virtual override onlyCallByCounterpart nonReentrant { + _beforeFinalizeWithdrawERC20(_l1Token, _l2Token, _from, _to, _amount); + + IERC20Upgradeable(_l1Token).safeTransfer(_to, _amount); + + emit FinalizeWithdrawERC20(_l1Token, _l2Token, _from, _to, _amount, _data); + } + + /********************** + * Internal Functions * + **********************/ + + /// @dev Internal function hook to perform checks and actions before finalizing the withdrawal. + /// @param _l1Token The address of corresponding L1 token in L1. + /// @param _l2Token The address of corresponding L2 token in L2. + function _beforeFinalizeWithdrawERC20( + address _l1Token, + address _l2Token, + address, + address, + uint256 + ) internal virtual { + if (msg.value > 0) revert ErrorMsgValueNotZero(); + if (_l2Token == address(0)) revert ErrorL2TokenAddressIsZero(); + if (getL2ERC20Address(_l1Token) != _l2Token) revert ErrorL2TokenMismatch(); + + // update `tokenMapping` on first withdraw + address _storedL2Token = tokenMapping[_l1Token]; + if (_storedL2Token == address(0)) { + tokenMapping[_l1Token] = _l2Token; + } else { + if (_storedL2Token != _l2Token) revert ErrorL2TokenMismatch(); + } + } + + /// @dev Internal function to transfer ERC20 token to this contract. + /// @param _token The address of token to transfer. + /// @param _amount The amount of token to transfer. + function _transferERC20In( + address _from, + address _token, + uint256 _amount + ) internal returns (uint256) { + // common practice to handle fee on transfer token. + uint256 _before = IERC20Upgradeable(_token).balanceOf(address(this)); + IERC20Upgradeable(_token).safeTransferFrom(_from, address(this), _amount); + uint256 _after = IERC20Upgradeable(_token).balanceOf(address(this)); + // no unchecked here, since some weird token may return arbitrary balance. + _amount = _after - _before; + + return _amount; + } + + /// @dev Internal function to do all the deposit operations. + /// + /// @param _token The token to deposit. + /// @param _to The recipient address to recieve the token in L2. + /// @param _amount The amount of token to deposit. + /// @param _data Optional data to forward to recipient's account. It is always empty for now. + /// @param _gasLimit Gas limit required to complete the deposit on L2. + function _deposit( + address _token, + address _from, + bytes memory _to, + uint256 _amount, + bytes memory _data, + uint256 _gasLimit + ) internal virtual nonReentrant { + // 1. Transfer token into this contract. + _amount = _transferERC20In(_msgSender(), _token, _amount); + if (_amount == 0) revert ErrorAmountIsZero(); + + // 2. Generate message passed to L2StandardERC20Gateway. + address _l2Token = tokenMapping[_token]; + bytes memory _l2Data; + if (_l2Token == address(0)) { + // @note we won't update `tokenMapping` here but update the `tokenMapping` on + // first successful withdraw. This will prevent user to set arbitrary token + // metadata by setting a very small `_gasLimit` on the first tx. + _l2Token = getL2ERC20Address(_token); + + // passing symbol/name/decimal in order to deploy in L2. + string memory _symbol = IERC20MetadataUpgradeable(_token).symbol(); + string memory _name = IERC20MetadataUpgradeable(_token).name(); + uint8 _decimals = IERC20MetadataUpgradeable(_token).decimals(); + _l2Data = abi.encode(true, abi.encode(_data, abi.encode(_symbol, _name, _decimals))); + } else { + _l2Data = abi.encode(false, _data); + } + bytes memory _message = abi.encodeCall( + IL2ERC20GatewayValidium.finalizeDepositERC20Encrypted, + (_token, _l2Token, _from, _to, _amount, _l2Data) + ); + + // 3. Send message to L1ScrollMessenger. + IL1ScrollMessenger(messenger).sendMessage{value: msg.value}(counterpart, 0, _message, _gasLimit, _from); + + emit DepositERC20(_token, _l2Token, _from, _to, _amount, _data); + } +} diff --git a/src/validium/L1ScrollMessengerValidium.sol b/src/validium/L1ScrollMessengerValidium.sol new file mode 100644 index 00000000..81d3bb10 --- /dev/null +++ b/src/validium/L1ScrollMessengerValidium.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +pragma solidity =0.8.24; + +import {IScrollMessenger} from "../libraries/IScrollMessenger.sol"; +import {IWhitelist} from "../libraries/common/IWhitelist.sol"; + +import {L1ScrollMessenger} from "../L1/L1ScrollMessenger.sol"; + +// solhint-disable avoid-low-level-calls +// solhint-disable not-rely-on-time +// solhint-disable reason-string + +/// @title L1ScrollMessengerValidium +contract L1ScrollMessengerValidium is L1ScrollMessenger { + /********** + * Errors * + **********/ + + /// @dev Thrown when the sender is not allowed to send message. + error ErrorSenderNotAllowed(); + + /************* + * Constants * + *************/ + + /// @notice The address of whitelist contract. + address public immutable whitelist; + + /*************** + * Constructor * + ***************/ + + constructor( + address _counterpart, + address _rollup, + address _messageQueueV2, + address _whitelist + ) L1ScrollMessenger(_counterpart, _rollup, address(0), _messageQueueV2, address(0)) { + _disableInitializers(); + whitelist = _whitelist; + } + + /********************** + * Internal Functions * + **********************/ + + /// @inheritdoc L1ScrollMessenger + function _sendMessage( + address _to, + uint256 _value, + bytes memory _message, + uint256 _gasLimit, + address _refundAddress + ) internal virtual override { + if (!IWhitelist(whitelist).isSenderAllowed(_msgSender())) revert ErrorSenderNotAllowed(); + + super._sendMessage(_to, _value, _message, _gasLimit, _refundAddress); + } +} diff --git a/src/validium/L1WETHGatewayValidium.sol b/src/validium/L1WETHGatewayValidium.sol new file mode 100644 index 00000000..1aa9be27 --- /dev/null +++ b/src/validium/L1WETHGatewayValidium.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT + +pragma solidity =0.8.24; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import {IWETH} from "../interfaces/IWETH.sol"; +import {IL1ERC20GatewayValidium} from "./IL1ERC20GatewayValidium.sol"; + +contract L1WETHGatewayValidium { + using SafeERC20 for IERC20; + + /********** + * Errors * + **********/ + + /// @notice The error thrown when the value is insufficient. + error ErrorInsufficientValue(); + + /************* + * Constants * + *************/ + + /// @dev The gas limit for the deposit. + uint256 private constant GAS_LIMIT = 1000000; + + /*********************** + * Immutable Variables * + ***********************/ + + /// @notice The address of `WETH` token. + address public immutable WETH; + + /// @notice The address of `L1ERC20GatewayValidium` contract. + address public immutable gateway; + + /*************** + * Constructor * + ***************/ + + constructor(address _WETH, address _gateway) { + WETH = _WETH; + gateway = _gateway; + } + + /***************************** + * Public Mutating Functions * + *****************************/ + + /// @notice Deposit ETH to L2 through the `L1ERC20GatewayValidium` contract. + /// @param _to The encrypted address of recipient in L2 to receive the token. + function deposit(bytes memory _to, uint256 _amount) external payable { + if (msg.value < _amount) revert ErrorInsufficientValue(); + + // WETH deposit is safe. + // slither-disable-next-line arbitrary-send-eth + IWETH(WETH).deposit{value: _amount}(); + IERC20(WETH).safeApprove(gateway, _amount); + IL1ERC20GatewayValidium(gateway).depositERC20{value: msg.value - _amount}( + WETH, + msg.sender, + _to, + _amount, + GAS_LIMIT + ); + } +} diff --git a/src/validium/ScrollChainValidium.sol b/src/validium/ScrollChainValidium.sol new file mode 100644 index 00000000..e703beb7 --- /dev/null +++ b/src/validium/ScrollChainValidium.sol @@ -0,0 +1,367 @@ +// SPDX-License-Identifier: MIT + +pragma solidity =0.8.24; + +import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; + +import {IL1MessageQueueV2} from "../L1/rollup/IL1MessageQueueV2.sol"; +import {IRollupVerifier} from "../libraries/verifier/IRollupVerifier.sol"; +import {IScrollChainValidium} from "./IScrollChainValidium.sol"; + +import {BatchHeaderValidiumV0Codec} from "./codec/BatchHeaderValidiumV0Codec.sol"; + +// solhint-disable no-inline-assembly +// solhint-disable reason-string + +/// @title ScrollChainValidium +contract ScrollChainValidium is AccessControlUpgradeable, PausableUpgradeable, IScrollChainValidium { + /********** + * Errors * + **********/ + + /// @dev Thrown when the given genesis batch is invalid. + error ErrorInvalidGenesisBatch(); + + /// @dev Thrown when finalizing a verified batch. + error ErrorBatchIsAlreadyVerified(); + + /// @dev Thrown when importing genesis batch twice. + error ErrorGenesisBatchImported(); + + /// @dev Thrown when the batch hash is incorrect. + error ErrorIncorrectBatchHash(); + + /// @dev Thrown when reverting a finalized batch. + error ErrorRevertFinalizedBatch(); + + /// @dev Thrown when the given state root is zero. + error ErrorStateRootIsZero(); + + /// @dev Thrown when given batch is not committed before. + error ErrorBatchNotCommitted(); + + /************* + * Constants * + *************/ + + /// @notice The role for import genesis batch. + bytes32 public constant GENESIS_IMPORTER_ROLE = keccak256("GENESIS_IMPORTER_ROLE"); + + /// @notice The role for sequencer who can commit batch. + bytes32 public constant SEQUENCER_ROLE = keccak256("SEQUENCER_ROLE"); + + /// @notice The role for prover who can finalize batch. + bytes32 public constant PROVER_ROLE = keccak256("PROVER_ROLE"); + + /*********************** + * Immutable Variables * + ***********************/ + + /// @notice The chain id of the corresponding layer 2 chain. + uint64 public immutable layer2ChainId; + + /// @notice The address of `L1MessageQueueV2`. + address public immutable messageQueueV2; + + /// @notice The address of `MultipleVersionRollupVerifier`. + address public immutable verifier; + + /********************* + * Storage Variables * + *********************/ + + /// @inheritdoc IScrollChainValidium + uint256 public override lastFinalizedBatchIndex; + + /// @inheritdoc IScrollChainValidium + uint256 public override lastCommittedBatchIndex; + + /// @dev Mapping from batch index to batch hash. + mapping(uint256 => bytes32) public override committedBatches; + + /// @dev Mapping from batch index to corresponding state root in Validium L3. + mapping(uint256 => bytes32) public override stateRoots; + + /// @dev Mapping from batch index to corresponding withdraw root in Validium L3. + mapping(uint256 => bytes32) public override withdrawRoots; + + /*************** + * Constructor * + ***************/ + + /// @notice Constructor for `ScrollChainValidium` implementation contract. + /// + /// @param _chainId The chain id of L2. + /// @param _messageQueueV2 The address of `L1MessageQueueV2`. + /// @param _verifier The address of `MultipleVersionRollupVerifier`. + constructor( + uint64 _chainId, + address _messageQueueV2, + address _verifier + ) { + _disableInitializers(); + + layer2ChainId = _chainId; + messageQueueV2 = _messageQueueV2; + verifier = _verifier; + } + + /// @notice Initialize the storage of ScrollChainValidium. + /// @param _admin The address of the admin. + function initialize(address _admin) external initializer { + __Context_init(); + __ERC165_init(); + __AccessControl_init(); + __Pausable_init(); + + _grantRole(DEFAULT_ADMIN_ROLE, _admin); + } + + /************************* + * Public View Functions * + *************************/ + + /// @inheritdoc IScrollChainValidium + function isBatchFinalized(uint256 _batchIndex) external view override returns (bool) { + return _batchIndex <= lastFinalizedBatchIndex; + } + + /***************************** + * Public Mutating Functions * + *****************************/ + + /// @notice Import layer 2 genesis block + /// @param _batchHeader The header of the genesis batch. + function importGenesisBatch(bytes calldata _batchHeader) external onlyRole(GENESIS_IMPORTER_ROLE) { + (uint256 batchPtr, uint256 _length) = BatchHeaderValidiumV0Codec.loadAndValidate(_batchHeader); + // batch index should be 0 for genesis batch + if (BatchHeaderValidiumV0Codec.getBatchIndex(batchPtr) != 0) { + revert ErrorInvalidGenesisBatch(); + } + // parant batch hash should be 0 for genesis batch + if (BatchHeaderValidiumV0Codec.getParentBatchHash(batchPtr) != bytes32(0)) { + revert ErrorInvalidGenesisBatch(); + } + // withdraw root should be 0 for genesis batch + if (BatchHeaderValidiumV0Codec.getWithdrawRoot(batchPtr) != bytes32(0)) { + revert ErrorInvalidGenesisBatch(); + } + + bytes32 _postStateRoot = BatchHeaderValidiumV0Codec.getPostStateRoot(batchPtr); + + // check state root + if (_postStateRoot == bytes32(0)) revert ErrorStateRootIsZero(); + + // check whether the genesis batch is imported + if (stateRoots[0] != bytes32(0)) revert ErrorGenesisBatchImported(); + + bytes32 _batchHash = BatchHeaderValidiumV0Codec.computeBatchHash(batchPtr, _length); + + committedBatches[0] = _batchHash; + stateRoots[0] = _postStateRoot; + + emit CommitBatch(0, _batchHash); + emit FinalizeBatch(0, _batchHash, _postStateRoot, bytes32(0)); + } + + /// @inheritdoc IScrollChainValidium + function commitBatch( + uint8 version, + bytes32 parentBatchHash, + bytes32 postStateRoot, + bytes32 withdrawRoot, + bytes calldata commitment + ) external onlyRole(SEQUENCER_ROLE) whenNotPaused { + if (postStateRoot == bytes32(0)) revert ErrorStateRootIsZero(); + + uint256 cachedLastCommittedBatchIndex = lastCommittedBatchIndex; + if (parentBatchHash != committedBatches[cachedLastCommittedBatchIndex]) { + revert ErrorIncorrectBatchHash(); + } + + cachedLastCommittedBatchIndex += 1; + bytes memory batchHeader = BatchHeaderValidiumV0Codec.encode( + version, + uint64(cachedLastCommittedBatchIndex), + parentBatchHash, + postStateRoot, + withdrawRoot, + commitment + ); + bytes32 batchHash = BatchHeaderValidiumV0Codec.computeBatchHash(batchHeader); + + lastCommittedBatchIndex = cachedLastCommittedBatchIndex; + committedBatches[cachedLastCommittedBatchIndex] = batchHash; + stateRoots[cachedLastCommittedBatchIndex] = postStateRoot; + withdrawRoots[cachedLastCommittedBatchIndex] = withdrawRoot; + + emit CommitBatch(cachedLastCommittedBatchIndex, batchHash); + } + + /// @inheritdoc IScrollChainValidium + function revertBatch(bytes calldata batchHeader) external onlyRole(DEFAULT_ADMIN_ROLE) { + uint256 lastBatchIndex = lastCommittedBatchIndex; + (, , uint256 startBatchIndex) = _loadBatchHeader(batchHeader, lastBatchIndex); + + // check finalization + if (startBatchIndex <= lastFinalizedBatchIndex) revert ErrorRevertFinalizedBatch(); + + // actual revert + for (uint256 i = lastBatchIndex; i >= startBatchIndex; --i) { + delete committedBatches[i]; + delete stateRoots[i]; + delete withdrawRoots[i]; + } + emit RevertBatch(startBatchIndex, lastBatchIndex); + + // update `lastCommittedBatchIndex` + lastCommittedBatchIndex = startBatchIndex - 1; + } + + /// @inheritdoc IScrollChainValidium + function finalizeBundle( + bytes calldata batchHeader, + uint256 totalL1MessagesPoppedOverall, + bytes calldata aggrProof + ) external override onlyRole(PROVER_ROLE) whenNotPaused { + _finalizeBundle(batchHeader, totalL1MessagesPoppedOverall, aggrProof); + } + + /************************ + * Restricted Functions * + ************************/ + + /// @notice Pause the contract + /// @param _status The pause status to update. + function setPause(bool _status) external onlyRole(DEFAULT_ADMIN_ROLE) { + if (_status) { + _pause(); + } else { + _unpause(); + } + } + + /********************** + * Internal Functions * + **********************/ + + /// @dev Internal function to do common actions before actual batch finalization. + function _beforeFinalizeBatch(bytes calldata batchHeader) + internal + view + returns ( + uint256 version, + bytes32 batchHash, + uint256 batchIndex, + uint256 prevBatchIndex + ) + { + uint256 batchPtr; + // compute pending batch hash and verify + (batchPtr, batchHash, batchIndex) = _loadBatchHeader(batchHeader, lastCommittedBatchIndex); + + // make sure don't finalize batch multiple times + prevBatchIndex = lastFinalizedBatchIndex; + if (batchIndex <= prevBatchIndex) revert ErrorBatchIsAlreadyVerified(); + + version = BatchHeaderValidiumV0Codec.getVersion(batchPtr); + } + + /// @dev Internal function to do common actions after actual batch finalization. + function _afterFinalizeBatch( + uint256 batchIndex, + bytes32 batchHash, + uint256 totalL1MessagesPoppedOverall, + bytes32 postStateRoot, + bytes32 withdrawRoot + ) internal { + lastFinalizedBatchIndex = batchIndex; + + if (totalL1MessagesPoppedOverall > 0) { + IL1MessageQueueV2(messageQueueV2).finalizePoppedCrossDomainMessage(totalL1MessagesPoppedOverall); + } + + emit FinalizeBatch(batchIndex, batchHash, postStateRoot, withdrawRoot); + } + + /// @dev Internal function to finalize a bundle. + /// @param batchHeader The header of the last batch in this bundle. + /// @param totalL1MessagesPoppedOverall The number of messages processed after this bundle. + /// @param aggrProof The bundle proof for this bundle. + function _finalizeBundle( + bytes calldata batchHeader, + uint256 totalL1MessagesPoppedOverall, + bytes calldata aggrProof + ) internal virtual { + // actions before verification + (uint256 version, bytes32 batchHash, uint256 batchIndex, uint256 prevBatchIndex) = _beforeFinalizeBatch( + batchHeader + ); + + // L1 message hashes are chained, + // this hash commits to the whole queue up to and including `totalL1MessagesPoppedOverall-1` + bytes32 messageQueueHash = totalL1MessagesPoppedOverall == 0 + ? bytes32(0) + : IL1MessageQueueV2(messageQueueV2).getMessageRollingHash(totalL1MessagesPoppedOverall - 1); + + bytes32 postStateRoot = stateRoots[batchIndex]; + bytes32 withdrawRoot = withdrawRoots[batchIndex]; + + // @todo public inputs TBD + bytes memory publicInputs = abi.encodePacked( + layer2ChainId, + messageQueueHash, + uint32(batchIndex - prevBatchIndex), // numBatches + stateRoots[prevBatchIndex], // _prevStateRoot + committedBatches[prevBatchIndex], // _prevBatchHash + postStateRoot, + batchHash, + withdrawRoot + ); + + // verify bundle, choose the correct verifier based on the last batch + // our off-chain service will make sure all unfinalized batches have the same batch version. + IRollupVerifier(verifier).verifyBundleProof(version, batchIndex, aggrProof, publicInputs); + + // actions after verification + _afterFinalizeBatch(batchIndex, batchHash, totalL1MessagesPoppedOverall, postStateRoot, withdrawRoot); + } + + /// @dev Internal function to load batch header from calldata to memory. + /// @param _batchHeader The batch header in calldata. + /// @param _lastCommittedBatchIndex The index of the last committed batch. + /// @return batchPtr The start memory offset of loaded batch header. + /// @return _batchHash The hash of the loaded batch header. + /// @return _batchIndex The index of this batch. + /// @dev This function only works with batches whose hashes are stored in `committedBatches`. + function _loadBatchHeader(bytes calldata _batchHeader, uint256 _lastCommittedBatchIndex) + internal + view + virtual + returns ( + uint256 batchPtr, + bytes32 _batchHash, + uint256 _batchIndex + ) + { + // load version from batch header, it is always the first byte. + uint256 version; + assembly { + version := shr(248, calldataload(_batchHeader.offset)) + } + + uint256 length; + (batchPtr, length) = BatchHeaderValidiumV0Codec.loadAndValidate(_batchHeader); + + _batchIndex = BatchHeaderValidiumV0Codec.getBatchIndex(batchPtr); + + if (_batchIndex > _lastCommittedBatchIndex) revert ErrorBatchNotCommitted(); + + // check against local storage + _batchHash = BatchHeaderValidiumV0Codec.computeBatchHash(batchPtr, length); + if (committedBatches[_batchIndex] != _batchHash) { + revert ErrorIncorrectBatchHash(); + } + } +} diff --git a/src/validium/codec/BatchHeaderValidiumV0Codec.sol b/src/validium/codec/BatchHeaderValidiumV0Codec.sol new file mode 100644 index 00000000..4b8252a6 --- /dev/null +++ b/src/validium/codec/BatchHeaderValidiumV0Codec.sol @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +// solhint-disable no-inline-assembly + +/// @dev Below is the encoding for `BatchHeaderValidium` V0, total 105 + dynamic bytes. +/// ```text +/// * Field Bytes Type Index Comments +/// * version 1 uint8 0 The batch version. +/// * batchIndex 8 uint64 1 The index of the batch. +/// * parentBatchHash 32 bytes32 9 The parent batch hash. +/// * postStateRoot 32 bytes32 41 The state root after this batch. +/// * withdrawRoot 32 bytes32 73 The withdraw root after this batch. +/// * commitment dynamic bytes 105 A dynamic data commitment. +/// ``` +library BatchHeaderValidiumV0Codec { + /// @dev Thrown when the length of batch header is smaller than 105 + error ErrorBatchHeaderV0LengthTooSmall(); + + /// @dev The length of fixed parts of the batch header. + uint256 internal constant BATCH_HEADER_FIXED_LENGTH = 105; + + /// @notice Load batch header in calldata to memory. + /// @param _batchHeader The encoded batch header bytes in calldata. + /// @return batchPtr The start memory offset of the batch header in memory. + /// @return length The length in bytes of the batch header. + function loadAndValidate(bytes calldata _batchHeader) internal pure returns (uint256 batchPtr, uint256 length) { + length = _batchHeader.length; + if (length < BATCH_HEADER_FIXED_LENGTH) revert ErrorBatchHeaderV0LengthTooSmall(); + + // copy batch header to memory. + assembly { + batchPtr := mload(0x40) + calldatacopy(batchPtr, _batchHeader.offset, length) + mstore(0x40, add(batchPtr, length)) + } + } + + /// @notice Get the version of the batch header. + /// @param batchPtr The start memory offset of the batch header in memory. + /// @return _version The version of the batch header. + function getVersion(uint256 batchPtr) internal pure returns (uint256 _version) { + assembly { + _version := shr(248, mload(batchPtr)) + } + } + + /// @notice Get the batch index of the batch. + /// @param batchPtr The start memory offset of the batch header in memory. + /// @return _batchIndex The batch index of the batch. + function getBatchIndex(uint256 batchPtr) internal pure returns (uint256 _batchIndex) { + assembly { + _batchIndex := shr(192, mload(add(batchPtr, 1))) + } + } + + /// @notice Get the parent batch hash of the batch. + /// @param batchPtr The start memory offset of the batch header in memory. + /// @return _parentBatchHash The parent batch hash. + function getParentBatchHash(uint256 batchPtr) internal pure returns (bytes32 _parentBatchHash) { + assembly { + _parentBatchHash := mload(add(batchPtr, 9)) + } + } + + /// @notice Get the batch index of the batch. + /// @param batchPtr The start memory offset of the batch header in memory. + /// @return _postStateRoot The state root after of the batch. + function getPostStateRoot(uint256 batchPtr) internal pure returns (bytes32 _postStateRoot) { + assembly { + _postStateRoot := mload(add(batchPtr, 41)) + } + } + + /// @notice Get the withdraw root of the batch. + /// @param batchPtr The start memory offset of the batch header in memory. + /// @return _withdrawRoot The withdraw root of the batch. + function getWithdrawRoot(uint256 batchPtr) internal pure returns (bytes32 _withdrawRoot) { + assembly { + _withdrawRoot := mload(add(batchPtr, 73)) + } + } + + /// @notice Encode necessary fields to batch header bytes. + /// + /// @param version The batch version + /// @param batchIndex The index of the batch + /// @param parentBatchHash The parent batch hash + /// @param postStateRoot The state root after this batch. + /// @param withdrawRoot The withdraw root after this batch. + /// @param commitment A dynamic data commitment. + function encode( + uint8 version, + uint64 batchIndex, + bytes32 parentBatchHash, + bytes32 postStateRoot, + bytes32 withdrawRoot, + bytes memory commitment + ) internal pure returns (bytes memory) { + return abi.encodePacked(version, batchIndex, parentBatchHash, postStateRoot, withdrawRoot, commitment); + } + + /// @notice Compute the batch hash. + /// @dev Caller should make sure that the encoded batch header is correct. + /// + /// @param header The bytes of batch header in memory. + /// @return batchHash The hash of the corresponding batch. + function computeBatchHash(bytes memory header) internal pure returns (bytes32 batchHash) { + uint256 dataPtr; + uint256 length; + // in the current version, the hash is: keccak(BatchHeader without timestamp) + assembly { + dataPtr := header + length := mload(dataPtr) + } + batchHash = computeBatchHash(dataPtr + 32, length); + } + + /// @notice Compute the batch hash. + /// @dev Caller should make sure that the encoded batch header is correct. + /// + /// @param batchPtr The start memory offset of the batch header in memory. + /// @param length The length of the batch. + /// @return batchHash The hash of the corresponding batch. + function computeBatchHash(uint256 batchPtr, uint256 length) internal pure returns (bytes32 batchHash) { + // in the current version, the hash is: keccak(BatchHeader without timestamp) + assembly { + batchHash := keccak256(batchPtr, length) + } + } +}