diff --git a/.github/workflows/bridge.yml b/.github/workflows/bridge.yml index f61f6c7c2..9a115edeb 100644 --- a/.github/workflows/bridge.yml +++ b/.github/workflows/bridge.yml @@ -26,6 +26,7 @@ defaults: jobs: check: + if: github.event.pull_request.draft == false runs-on: ubuntu-latest steps: - name: Install Go @@ -48,6 +49,7 @@ jobs: make mock_abi make lint goimports-lint: + if: github.event.pull_request.draft == false runs-on: ubuntu-latest steps: - name: Install Go @@ -67,6 +69,7 @@ jobs: exit 1 fi # docker-build: + # if: github.event.pull_request.draft == false # runs-on: ubuntu-latest # steps: # - name: Checkout code diff --git a/.github/workflows/bridge_history_api.yml b/.github/workflows/bridge_history_api.yml index 02c527ef2..8632b1b6e 100644 --- a/.github/workflows/bridge_history_api.yml +++ b/.github/workflows/bridge_history_api.yml @@ -26,6 +26,7 @@ defaults: jobs: # check: + # if: github.event.pull_request.draft == false # runs-on: ubuntu-latest # steps: # - name: Install Go @@ -36,9 +37,10 @@ jobs: # uses: actions/checkout@v2 # - name: Lint # run: | - # rm -rf $HOME/.cache/golangci-lint + # rm -rf $HOME/.cache/golangci-lint # make lint test: + if: github.event.pull_request.draft == false runs-on: ubuntu-latest steps: - name: Install Go @@ -52,6 +54,7 @@ jobs: go get ./... make test goimports-lint: + if: github.event.pull_request.draft == false runs-on: ubuntu-latest steps: - name: Install Go @@ -70,4 +73,4 @@ jobs: if [ -n "$(git status --porcelain)" ]; then exit 1 fi - + diff --git a/.github/workflows/common.yml b/.github/workflows/common.yml index 333542dcd..e3801a3a1 100644 --- a/.github/workflows/common.yml +++ b/.github/workflows/common.yml @@ -26,6 +26,7 @@ defaults: jobs: check: + if: github.event.pull_request.draft == false runs-on: ubuntu-latest steps: - uses: actions-rs/toolchain@v1 @@ -51,9 +52,10 @@ jobs: workspaces: "common/libzkp/impl -> target" - name: Lint run: | - rm -rf $HOME/.cache/golangci-lint + rm -rf $HOME/.cache/golangci-lint make lint goimports-lint: + if: github.event.pull_request.draft == false runs-on: ubuntu-latest steps: - name: Install Go diff --git a/.github/workflows/contracts.yaml b/.github/workflows/contracts.yaml index afd14bf74..3753fc8f9 100644 --- a/.github/workflows/contracts.yaml +++ b/.github/workflows/contracts.yaml @@ -2,10 +2,20 @@ name: Contracts on: push: + branches: + - main + - staging + - develop + - alpha paths: - 'contracts/**' - '.github/workflows/contracts.yaml' pull_request: + branches: + - main + - staging + - develop + - alpha paths: - 'contracts/**' - '.github/workflows/contracts.yaml' @@ -16,6 +26,7 @@ defaults: jobs: foundry: + if: github.event.pull_request.draft == false runs-on: ubuntu-latest steps: @@ -28,7 +39,7 @@ jobs: uses: foundry-rs/foundry-toolchain@v1 with: version: nightly - + - name: Setup LCOV uses: hrishikesh-kadam/setup-lcov@v1 @@ -84,6 +95,7 @@ jobs: update-comment: true hardhat: + if: github.event.pull_request.draft == false runs-on: ubuntu-latest steps: diff --git a/.github/workflows/coordinator.yml b/.github/workflows/coordinator.yml index ad0596081..07a71c330 100644 --- a/.github/workflows/coordinator.yml +++ b/.github/workflows/coordinator.yml @@ -26,6 +26,7 @@ defaults: jobs: check: + if: github.event.pull_request.draft == false runs-on: ubuntu-latest steps: - uses: actions-rs/toolchain@v1 @@ -44,6 +45,7 @@ jobs: rm -rf $HOME/.cache/golangci-lint make lint goimports-lint: + if: github.event.pull_request.draft == false runs-on: ubuntu-latest steps: - name: Install Go @@ -63,6 +65,7 @@ jobs: exit 1 fi # docker-build: + # if: github.event.pull_request.draft == false # runs-on: ubuntu-latest # steps: # - name: Checkout code diff --git a/.github/workflows/database.yml b/.github/workflows/database.yml index fe655b651..15ab93a0a 100644 --- a/.github/workflows/database.yml +++ b/.github/workflows/database.yml @@ -26,6 +26,7 @@ defaults: jobs: check: + if: github.event.pull_request.draft == false runs-on: ubuntu-latest steps: - name: Install Go @@ -36,9 +37,10 @@ jobs: uses: actions/checkout@v2 - name: Lint run: | - rm -rf $HOME/.cache/golangci-lint + rm -rf $HOME/.cache/golangci-lint make lint goimports-lint: + if: github.event.pull_request.draft == false runs-on: ubuntu-latest steps: - name: Install Go diff --git a/.github/workflows/roller.yml b/.github/workflows/roller.yml index 8c1696e05..b744745b8 100644 --- a/.github/workflows/roller.yml +++ b/.github/workflows/roller.yml @@ -26,6 +26,7 @@ defaults: jobs: test: + if: github.event.pull_request.draft == false runs-on: ubuntu-latest steps: - uses: actions-rs/toolchain@v1 @@ -48,6 +49,7 @@ jobs: make roller go test -tags="mock_prover" -v ./... check: + if: github.event.pull_request.draft == false runs-on: ubuntu-latest steps: - name: Install Go @@ -61,6 +63,7 @@ jobs: rm -rf $HOME/.cache/golangci-lint make lint goimports-lint: + if: github.event.pull_request.draft == false runs-on: ubuntu-latest steps: - name: Install Go diff --git a/contracts/docs/apis/L1ScrollMessenger.md b/contracts/docs/apis/L1ScrollMessenger.md index 877910ec3..d8c41fc05 100644 --- a/contracts/docs/apis/L1ScrollMessenger.md +++ b/contracts/docs/apis/L1ScrollMessenger.md @@ -215,10 +215,10 @@ function renounceOwnership() external nonpayable ### replayMessage ```solidity -function replayMessage(address _from, address _to, uint256 _value, uint256 _queueIndex, bytes _message, uint32 _oldGasLimit, uint32 _newGasLimit, address _refundAddress) external payable +function replayMessage(address _from, address _to, uint256 _value, uint256 _queueIndex, bytes _message, uint32 _newGasLimit, address _refundAddress) external payable ``` -Replay an exsisting message. +Replay an existing message. @@ -231,7 +231,6 @@ Replay an exsisting message. | _value | uint256 | undefined | | _queueIndex | uint256 | undefined | | _message | bytes | undefined | -| _oldGasLimit | uint32 | undefined | | _newGasLimit | uint32 | undefined | | _refundAddress | address | undefined | @@ -339,39 +338,6 @@ Update fee vault contract. |---|---|---| | _newFeeVault | address | The address of new fee vault contract. | -### updateWhitelist - -```solidity -function updateWhitelist(address _newWhitelist) external nonpayable -``` - -Update whitelist contract. - -*This function can only called by contract owner.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _newWhitelist | address | The address of new whitelist contract. | - -### whitelist - -```solidity -function whitelist() external view returns (address) -``` - -The whitelist contract to track the sender who can call `sendMessage` in ScrollMessenger. - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined | - ### xDomainMessageSender ```solidity @@ -512,22 +478,5 @@ Emitted when owner updates fee vault contract. | _oldFeeVault | address | undefined | | _newFeeVault | address | undefined | -### UpdateWhitelist - -```solidity -event UpdateWhitelist(address _oldWhitelist, address _newWhitelist) -``` - -Emitted when owner updates whitelist contract. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _oldWhitelist | address | undefined | -| _newWhitelist | address | undefined | - diff --git a/contracts/docs/apis/L2ScrollMessenger.md b/contracts/docs/apis/L2ScrollMessenger.md index bddedf167..110be84d0 100644 --- a/contracts/docs/apis/L2ScrollMessenger.md +++ b/contracts/docs/apis/L2ScrollMessenger.md @@ -284,7 +284,7 @@ function retryMessageWithProof(address _from, address _to, uint256 _value, uint2 ### sendMessage ```solidity -function sendMessage(address _to, uint256 _value, bytes _message, uint256 _gasLimit, address _refundAddress) external payable +function sendMessage(address _to, uint256 _value, bytes _message, uint256 _gasLimit, address) external payable ``` Send cross chain message from L1 to L2 or L2 to L1. @@ -299,7 +299,7 @@ Send cross chain message from L1 to L2 or L2 to L1. | _value | uint256 | undefined | | _message | bytes | undefined | | _gasLimit | uint256 | undefined | -| _refundAddress | address | undefined | +| _4 | address | undefined | ### sendMessage @@ -374,23 +374,7 @@ Update fee vault contract. function updateMaxFailedExecutionTimes(uint256 _maxFailedExecutionTimes) external nonpayable ``` - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _maxFailedExecutionTimes | uint256 | undefined | - -### updateWhitelist - -```solidity -function updateWhitelist(address _newWhitelist) external nonpayable -``` - -Update whitelist contract. +Update max failed execution times. *This function can only called by contract owner.* @@ -398,7 +382,7 @@ Update whitelist contract. | Name | Type | Description | |---|---|---| -| _newWhitelist | address | The address of new whitelist contract. | +| _maxFailedExecutionTimes | uint256 | The new max failed execution times. | ### verifyMessageExecutionStatus @@ -448,23 +432,6 @@ Check whether the l1 message is included in the corresponding L1 block. |---|---|---| | _0 | bool | bool Return true is the message is included in L1, otherwise return false. | -### whitelist - -```solidity -function whitelist() external view returns (address) -``` - -The whitelist contract to track the sender who can call `sendMessage` in ScrollMessenger. - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined | - ### xDomainMessageSender ```solidity @@ -621,22 +588,5 @@ Emitted when the maximum number of times each message can fail in L2 is updated. |---|---|---| | maxFailedExecutionTimes | uint256 | The new maximum number of times each message can fail in L2. | -### UpdateWhitelist - -```solidity -event UpdateWhitelist(address _oldWhitelist, address _newWhitelist) -``` - -Emitted when owner updates whitelist contract. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _oldWhitelist | address | undefined | -| _newWhitelist | address | undefined | - diff --git a/contracts/integration-test/EnforcedTxGateway.spec.ts b/contracts/integration-test/EnforcedTxGateway.spec.ts index 03b41ec39..8d6a3b5a2 100644 --- a/contracts/integration-test/EnforcedTxGateway.spec.ts +++ b/contracts/integration-test/EnforcedTxGateway.spec.ts @@ -34,7 +34,7 @@ describe("EnforcedTxGateway.spec", async () => { caller = await MockCaller.deploy(); await caller.deployed(); - await queue.initialize(constants.AddressZero, gateway.address, oracle.address, 10000000); + await queue.initialize(constants.AddressZero, constants.AddressZero, gateway.address, oracle.address, 10000000); await gateway.initialize(queue.address, feeVault.address); await oracle.initialize(21000, 0, 8, 16); @@ -55,7 +55,7 @@ describe("EnforcedTxGateway.spec", async () => { expect(await gateway.paused()).to.eq(false); }); - it("should revert, when initlaize again", async () => { + it("should revert, when initialize again", async () => { await expect(gateway.initialize(constants.AddressZero, constants.AddressZero)).to.revertedWith( "Initializable: contract is already initialized" ); @@ -195,7 +195,7 @@ describe("EnforcedTxGateway.spec", async () => { ).to.revertedWith("Pausable: paused"); }); - it("should revert, when signatue is wrong", async () => { + it("should revert, when signature is wrong", async () => { const signature = await signer.signMessage("0x00"); await expect( gateway diff --git a/contracts/integration-test/L1MessageQueue.spec.ts b/contracts/integration-test/L1MessageQueue.spec.ts index 78996ea9e..e4676332e 100644 --- a/contracts/integration-test/L1MessageQueue.spec.ts +++ b/contracts/integration-test/L1MessageQueue.spec.ts @@ -9,6 +9,7 @@ import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; describe("L1MessageQueue", async () => { let deployer: SignerWithAddress; + let scrollChain: SignerWithAddress; let messenger: SignerWithAddress; let gateway: SignerWithAddress; let signer: SignerWithAddress; @@ -17,7 +18,7 @@ describe("L1MessageQueue", async () => { let queue: L1MessageQueue; beforeEach(async () => { - [deployer, messenger, gateway, signer] = await ethers.getSigners(); + [deployer, scrollChain, messenger, gateway, signer] = await ethers.getSigners(); const L1MessageQueue = await ethers.getContractFactory("L1MessageQueue", deployer); queue = await L1MessageQueue.deploy(); @@ -27,21 +28,22 @@ describe("L1MessageQueue", async () => { oracle = await L2GasPriceOracle.deploy(); await oracle.initialize(21000, 0, 8, 16); - await queue.initialize(messenger.address, gateway.address, oracle.address, 10000000); + await queue.initialize(messenger.address, scrollChain.address, gateway.address, oracle.address, 10000000); }); context("auth", async () => { it("should initialize correctly", async () => { expect(await queue.owner()).to.eq(deployer.address); expect(await queue.messenger()).to.eq(messenger.address); + expect(await queue.scrollChain()).to.eq(scrollChain.address); expect(await queue.enforcedTxGateway()).to.eq(gateway.address); expect(await queue.gasOracle()).to.eq(oracle.address); expect(await queue.maxGasLimit()).to.eq(10000000); }); - it("should revert, when initlaize again", async () => { + it("should revert, when initialize again", async () => { await expect( - queue.initialize(constants.AddressZero, constants.AddressZero, constants.AddressZero, 0) + queue.initialize(constants.AddressZero, constants.AddressZero, constants.AddressZero, constants.AddressZero, 0) ).to.revertedWith("Initializable: contract is already initialized"); }); @@ -217,4 +219,59 @@ describe("L1MessageQueue", async () => { expect(await queue.getCrossDomainMessage(0)).to.eq(hash); }); }); + + context("#popCrossDomainMessage", async () => { + it("should revert, when non-scrollChain call", async () => { + await expect(queue.connect(signer).popCrossDomainMessage(0, 0, 0)).to.revertedWith( + "Only callable by the ScrollChain" + ); + }); + + it("should revert, when pop too many messages", async () => { + await expect(queue.connect(scrollChain).popCrossDomainMessage(0, 257, 0)).to.revertedWith( + "pop too many messages" + ); + }); + + it("should revert, when start index mismatch", async () => { + await expect(queue.connect(scrollChain).popCrossDomainMessage(1, 256, 0)).to.revertedWith("start index mismatch"); + }); + + it("should succeed", async () => { + // append 100 messages + for (let i = 0; i < 100; i++) { + await queue.connect(messenger).appendCrossDomainMessage(constants.AddressZero, 1000000, "0x"); + } + + // pop 50 messages with no skip + await expect(queue.connect(scrollChain).popCrossDomainMessage(0, 50, 0)) + .to.emit(queue, "DequeueTransaction") + .withArgs(0, 50, 0); + for (let i = 0; i < 50; i++) { + expect(await queue.getCrossDomainMessage(i)).to.eq(constants.HashZero); + } + expect(await queue.pendingQueueIndex()).to.eq(50); + + // pop 10 messages all skip + await expect(queue.connect(scrollChain).popCrossDomainMessage(50, 10, 1023)) + .to.emit(queue, "DequeueTransaction") + .withArgs(50, 10, 1023); + expect(await queue.pendingQueueIndex()).to.eq(60); + for (let i = 50; i < 60; i++) { + expect(BigNumber.from(await queue.getCrossDomainMessage(i))).to.gt(constants.Zero); + } + + // pop 20 messages, skip first 5 + await expect(queue.connect(scrollChain).popCrossDomainMessage(60, 20, 31)) + .to.emit(queue, "DequeueTransaction") + .withArgs(60, 20, 31); + expect(await queue.pendingQueueIndex()).to.eq(80); + for (let i = 60; i < 65; i++) { + expect(BigNumber.from(await queue.getCrossDomainMessage(i))).to.gt(constants.Zero); + } + for (let i = 65; i < 80; i++) { + expect(await queue.getCrossDomainMessage(i)).to.eq(constants.HashZero); + } + }); + }); }); diff --git a/contracts/integration-test/ScrollChain.spec.ts b/contracts/integration-test/ScrollChain.spec.ts index 99435375c..a2a3e459c 100644 --- a/contracts/integration-test/ScrollChain.spec.ts +++ b/contracts/integration-test/ScrollChain.spec.ts @@ -24,14 +24,22 @@ describe("ScrollChain", async () => { signer: deployer, libraries: { RollupVerifier: verifier.address }, }); - chain = await ScrollChain.deploy(0, 25, "0xb5baa665b2664c3bfed7eb46e00ebc110ecf2ebd257854a9bf2b9dbc9b2c08f6"); + chain = await ScrollChain.deploy(0); await chain.deployed(); - await chain.initialize(queue.address); + await chain.initialize(queue.address, constants.AddressZero, 44); await chain.updateSequencer(deployer.address, true); - await queue.initialize(constants.AddressZero, constants.AddressZero, constants.AddressZero, 10000000); + await queue.initialize( + constants.AddressZero, + chain.address, + constants.AddressZero, + constants.AddressZero, + 10000000 + ); }); + // @note skip this benchmark tests + /* it("should succeed", async () => { await chain.importGenesisBatch({ blocks: [ @@ -105,4 +113,5 @@ describe("ScrollChain", async () => { } } }); + */ }); diff --git a/contracts/scripts/foundry/DeployL1BridgeContracts.s.sol b/contracts/scripts/foundry/DeployL1BridgeContracts.s.sol index 3200475e1..c818b08a5 100644 --- a/contracts/scripts/foundry/DeployL1BridgeContracts.s.sol +++ b/contracts/scripts/foundry/DeployL1BridgeContracts.s.sol @@ -27,11 +27,6 @@ contract DeployL1BridgeContracts is Script { uint256 CHAIN_ID_L2 = vm.envUint("CHAIN_ID_L2"); - uint256 MAX_TX_IN_ONE_BATCH = vm.envOr("MAX_TX_IN_ONE_BATCH", uint256(44)); - - bytes32 PADDING_TX_HASH = - vm.envOr("PADDING_TX_HASH", bytes32(0x0000000000000000000000000000000000000000000000000000000000000000)); - address L1_WETH_ADDR = vm.envAddress("L1_WETH_ADDR"); address L2_WETH_ADDR = vm.envAddress("L2_WETH_ADDR"); @@ -74,7 +69,7 @@ contract DeployL1BridgeContracts is Script { } function deployScrollChain() internal { - ScrollChain impl = new ScrollChain(CHAIN_ID_L2, MAX_TX_IN_ONE_BATCH, PADDING_TX_HASH); + ScrollChain impl = new ScrollChain(CHAIN_ID_L2); TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( address(impl), address(proxyAdmin), diff --git a/contracts/scripts/foundry/InitializeL1BridgeContracts.s.sol b/contracts/scripts/foundry/InitializeL1BridgeContracts.s.sol index b6342318a..3d96f801b 100644 --- a/contracts/scripts/foundry/InitializeL1BridgeContracts.s.sol +++ b/contracts/scripts/foundry/InitializeL1BridgeContracts.s.sol @@ -20,12 +20,14 @@ contract InitializeL1BridgeContracts is Script { uint256 L1_DEPLOYER_PRIVATE_KEY = vm.envUint("L1_DEPLOYER_PRIVATE_KEY"); uint256 CHAIN_ID_L2 = vm.envUint("CHAIN_ID_L2"); + uint256 MAX_L2_TX_IN_CHUNK = vm.envOr("MAX_L2_TX_IN_CHUNK", uint256(44)); address L1_ROLLUP_OPERATOR_ADDR = vm.envAddress("L1_ROLLUP_OPERATOR_ADDR"); address L1_FEE_VAULT_ADDR = vm.envAddress("L1_FEE_VAULT_ADDR"); address L1_WHITELIST_ADDR = vm.envAddress("L1_WHITELIST_ADDR"); address L1_ZK_ROLLUP_PROXY_ADDR = vm.envAddress("L1_ZK_ROLLUP_PROXY_ADDR"); + address L1_ROLLUP_VERIFIER_ADDR = vm.envAddress("L1_ROLLUP_VERIFIER_ADDR"); address L1_MESSAGE_QUEUE_PROXY_ADDR = vm.envAddress("L1_MESSAGE_QUEUE_PROXY_ADDR"); address L2_GAS_PRICE_ORACLE_PROXY_ADDR = vm.envAddress("L2_GAS_PRICE_ORACLE_PROXY_ADDR"); address L1_SCROLL_MESSENGER_PROXY_ADDR = vm.envAddress("L1_SCROLL_MESSENGER_PROXY_ADDR"); @@ -53,7 +55,7 @@ contract InitializeL1BridgeContracts is Script { vm.startBroadcast(L1_DEPLOYER_PRIVATE_KEY); // initialize ScrollChain - ScrollChain(L1_ZK_ROLLUP_PROXY_ADDR).initialize(L1_MESSAGE_QUEUE_PROXY_ADDR); + ScrollChain(L1_ZK_ROLLUP_PROXY_ADDR).initialize(L1_MESSAGE_QUEUE_PROXY_ADDR, L1_ROLLUP_VERIFIER_ADDR, MAX_L2_TX_IN_CHUNK); ScrollChain(L1_ZK_ROLLUP_PROXY_ADDR).updateSequencer(L1_ROLLUP_OPERATOR_ADDR, true); // initialize L2GasPriceOracle @@ -63,6 +65,7 @@ contract InitializeL1BridgeContracts is Script { // initialize L1MessageQueue L1MessageQueue(L1_MESSAGE_QUEUE_PROXY_ADDR).initialize( L1_SCROLL_MESSENGER_PROXY_ADDR, + L1_ZK_ROLLUP_PROXY_ADDR, ENFORCED_TX_GATEWAY_PROXY_ADDR, L2_GAS_PRICE_ORACLE_PROXY_ADDR, 10000000 @@ -75,7 +78,6 @@ contract InitializeL1BridgeContracts is Script { L1_ZK_ROLLUP_PROXY_ADDR, L1_MESSAGE_QUEUE_PROXY_ADDR ); - L1ScrollMessenger(payable(L1_SCROLL_MESSENGER_PROXY_ADDR)).updateWhitelist(L1_WHITELIST_ADDR); // initialize EnforcedTxGateway EnforcedTxGateway(payable(ENFORCED_TX_GATEWAY_PROXY_ADDR)).initialize( diff --git a/contracts/scripts/foundry/InitializeL2BridgeContracts.s.sol b/contracts/scripts/foundry/InitializeL2BridgeContracts.s.sol index 606ae9ae9..ebe91c94f 100644 --- a/contracts/scripts/foundry/InitializeL2BridgeContracts.s.sol +++ b/contracts/scripts/foundry/InitializeL2BridgeContracts.s.sol @@ -67,7 +67,6 @@ contract InitializeL2BridgeContracts is Script { L1_SCROLL_MESSENGER_PROXY_ADDR, L2_TX_FEE_VAULT_ADDR ); - L2ScrollMessenger(payable(L2_SCROLL_MESSENGER_PROXY_ADDR)).updateWhitelist(L2_WHITELIST_ADDR); // initialize L2GatewayRouter L2GatewayRouter(L2_GATEWAY_ROUTER_PROXY_ADDR).initialize( diff --git a/contracts/scripts/initialize_zkrollup.ts b/contracts/scripts/initialize_zkrollup.ts index 8924a569b..6c3187887 100644 --- a/contracts/scripts/initialize_zkrollup.ts +++ b/contracts/scripts/initialize_zkrollup.ts @@ -18,7 +18,7 @@ async function main() { const ScrollChain = await ethers.getContractAt("ScrollChain", addressFile.get("ScrollChain.proxy"), deployer); if ((await ScrollChain.owner()) === constants.AddressZero) { - const tx = await ScrollChain.initialize(L1_MESSAGE_QUEUE); + const tx = await ScrollChain.initialize(L1_MESSAGE_QUEUE, constants.AddressZero); console.log("initialize ScrollChain, hash:", tx.hash); const receipt = await tx.wait(); console.log(`✅ Done, gas used: ${receipt.gasUsed}`); diff --git a/contracts/src/L1/IL1ScrollMessenger.sol b/contracts/src/L1/IL1ScrollMessenger.sol index baee3b782..0afdbc2b2 100644 --- a/contracts/src/L1/IL1ScrollMessenger.sol +++ b/contracts/src/L1/IL1ScrollMessenger.sol @@ -10,8 +10,8 @@ interface IL1ScrollMessenger is IScrollMessenger { ***********/ struct L2MessageProof { - // The hash of the batch where the message belongs to. - bytes32 batchHash; + // The index of the batch where the message belongs to. + uint256 batchIndex; // Concatenation of merkle proof for withdraw merkle trie. bytes merkleProof; } @@ -36,13 +36,12 @@ interface IL1ScrollMessenger is IScrollMessenger { L2MessageProof memory proof ) external; - /// @notice Replay an exsisting message. + /// @notice Replay an existing message. /// @param from The address of the sender of the message. /// @param to The address of the recipient of the message. /// @param value The msg.value passed to the message call. /// @param queueIndex The queue index for the message to replay. /// @param message The content of the message. - /// @param oldGasLimit Original gas limit used to send the message. /// @param newGasLimit New gas limit to be used for this message. /// @param refundAddress The address of account who will receive the refunded fee. function replayMessage( @@ -51,7 +50,6 @@ interface IL1ScrollMessenger is IScrollMessenger { uint256 value, uint256 queueIndex, bytes memory message, - uint32 oldGasLimit, uint32 newGasLimit, address refundAddress ) external payable; diff --git a/contracts/src/L1/L1ScrollMessenger.sol b/contracts/src/L1/L1ScrollMessenger.sol index 2ab7fe1bf..874bfacd8 100644 --- a/contracts/src/L1/L1ScrollMessenger.sol +++ b/contracts/src/L1/L1ScrollMessenger.sol @@ -125,7 +125,7 @@ contract L1ScrollMessenger is PausableUpgradeable, ScrollMessengerBase, IL1Scrol uint256 _nonce, bytes memory _message, L2MessageProof memory _proof - ) external override whenNotPaused onlyWhitelistedSender(msg.sender) { + ) external override whenNotPaused { require( xDomainMessageSender == ScrollConstants.DEFAULT_XDOMAIN_MESSAGE_SENDER, "Message is already in execution" @@ -136,8 +136,8 @@ contract L1ScrollMessenger is PausableUpgradeable, ScrollMessengerBase, IL1Scrol { address _rollup = rollup; - require(IScrollChain(_rollup).isBatchFinalized(_proof.batchHash), "Batch is not finalized"); - bytes32 _messageRoot = IScrollChain(_rollup).getL2MessageRoot(_proof.batchHash); + require(IScrollChain(_rollup).isBatchFinalized(_proof.batchIndex), "Batch is not finalized"); + bytes32 _messageRoot = IScrollChain(_rollup).withdrawRoots(_proof.batchIndex); require( WithdrawTrieVerifier.verifyMerkleProof(_messageRoot, _xDomainCalldataHash, _nonce, _proof.merkleProof), "Invalid proof" @@ -174,7 +174,6 @@ contract L1ScrollMessenger is PausableUpgradeable, ScrollMessengerBase, IL1Scrol uint256 _value, uint256 _queueIndex, bytes memory _message, - uint32 _oldGasLimit, uint32 _newGasLimit, address _refundAddress ) external payable override whenNotPaused { @@ -185,22 +184,9 @@ contract L1ScrollMessenger is PausableUpgradeable, ScrollMessengerBase, IL1Scrol address _messageQueue = messageQueue; address _counterpart = counterpart; bytes memory _xDomainCalldata = _encodeXDomainCalldata(_from, _to, _value, _queueIndex, _message); + bytes32 _xDomainCalldataHash = keccak256(_xDomainCalldata); - // compute the expected transaction hash - bytes32 _computedTransactionHash = IL1MessageQueue(_messageQueue).computeTransactionHash( - AddressAliasHelper.applyL1ToL2Alias(address(this)), - _queueIndex, - 0, - _counterpart, - _oldGasLimit, - _xDomainCalldata - ); - - // check the provided message matching with enqueued one. - require( - _computedTransactionHash == IL1MessageQueue(_messageQueue).getCrossDomainMessage(_queueIndex), - "Provided message has not been enqueued" - ); + require(isL1MessageSent[_xDomainCalldataHash], "Provided message has not been enqueued"); // compute and deduct the messaging fee to fee vault. uint256 _fee = IL1MessageQueue(_messageQueue).estimateCrossDomainMessageFee(_newGasLimit); diff --git a/contracts/src/L1/rollup/IL1MessageQueue.sol b/contracts/src/L1/rollup/IL1MessageQueue.sol index b91e7b209..95cde92e8 100644 --- a/contracts/src/L1/rollup/IL1MessageQueue.sol +++ b/contracts/src/L1/rollup/IL1MessageQueue.sol @@ -9,7 +9,7 @@ interface IL1MessageQueue { /// @notice Emitted when a new L1 => L2 transaction is appended to the queue. /// @param sender The address of account who initiates the transaction. - /// @param target The address of account who will recieve the transaction. + /// @param target The address of account who will receive the transaction. /// @param value The value passed with the transaction. /// @param queueIndex The index of this transaction in the queue. /// @param gasLimit Gas limit required to complete the message relay on L2. @@ -23,10 +23,19 @@ interface IL1MessageQueue { bytes data ); + /// @notice Emitted when some L1 => L2 transactions are included in L1. + /// @param startIndex The start index of messages popped. + /// @param count The number of messages popped. + /// @param skippedBitmap A bitmap indicates whether a message is skipped. + event DequeueTransaction(uint256 startIndex, uint256 count, uint256 skippedBitmap); + /************************* * Public View Functions * *************************/ + /// @notice The start index of all pending inclusion messages. + function pendingQueueIndex() external view returns (uint256); + /// @notice Return the index of next appended message. /// @dev Also the total number of appended messages. function nextCrossDomainMessageIndex() external view returns (uint256); @@ -87,4 +96,18 @@ interface IL1MessageQueue { uint256 gasLimit, bytes calldata data ) external; + + /// @notice Pop finalized messages from queue. + /// + /// @dev We can pop at most 256 messages each time. And if the message is not skipped, + /// the corresponding entry will be cleared. + /// + /// @param startIndex The start index to pop. + /// @param count The number of messages to pop. + /// @param skippedBitmap A bitmap indicates whether a message is skipped. + function popCrossDomainMessage( + uint256 startIndex, + uint256 count, + uint256 skippedBitmap + ) external; } diff --git a/contracts/src/L1/rollup/IScrollChain.sol b/contracts/src/L1/rollup/IScrollChain.sol index 385193da0..da1225f53 100644 --- a/contracts/src/L1/rollup/IScrollChain.sol +++ b/contracts/src/L1/rollup/IScrollChain.sol @@ -7,98 +7,73 @@ interface IScrollChain { * Events * **********/ - /// @notice Emitted when a new batch is commited. - /// @param batchHash The hash of the batch + /// @notice Emitted when a new batch is committed. + /// @param batchHash The hash of the batch. event CommitBatch(bytes32 indexed batchHash); - /// @notice Emitted when a batch is reverted. - /// @param batchHash The identification of the batch. + /// @notice revert a pending batch. + /// @param batchHash The hash of the batch event RevertBatch(bytes32 indexed batchHash); /// @notice Emitted when a batch is finalized. /// @param batchHash The hash of the batch - event FinalizeBatch(bytes32 indexed batchHash); - - /*********** - * Structs * - ***********/ - - struct BlockContext { - // The hash of this block. - bytes32 blockHash; - // The parent hash of this block. - bytes32 parentHash; - // The height of this block. - uint64 blockNumber; - // The timestamp of this block. - uint64 timestamp; - // The base fee of this block. - // Currently, it is not used, because we disable EIP-1559. - // We keep it for future proof. - uint256 baseFee; - // The gas limit of this block. - uint64 gasLimit; - // The number of transactions in this block, both L1 & L2 txs. - uint16 numTransactions; - // The number of l1 messages in this block. - uint16 numL1Messages; - } - - struct Batch { - // The list of blocks in this batch - BlockContext[] blocks; // MAX_NUM_BLOCKS = 100, about 5 min - // The state root of previous batch. - // The first batch will use 0x0 for prevStateRoot - bytes32 prevStateRoot; - // The state root of the last block in this batch. - bytes32 newStateRoot; - // The withdraw trie root of the last block in this batch. - bytes32 withdrawTrieRoot; - // The index of the batch. - uint64 batchIndex; - // The parent batch hash. - bytes32 parentBatchHash; - // Concatenated raw data of RLP encoded L2 txs - bytes l2Transactions; - } + /// @param stateRoot The state root in layer 2 after this batch. + /// @param withdrawRoot The merkle root in layer2 after this batch. + event FinalizeBatch(bytes32 indexed batchHash, bytes32 stateRoot, bytes32 withdrawRoot); /************************* * Public View Functions * *************************/ - /// @notice Return whether the batch is finalized by batch hash. - /// @param batchHash The hash of the batch to query. - function isBatchFinalized(bytes32 batchHash) external view returns (bool); + /// @notice Return the batch hash of a committed batch. + /// @param batchIndex The index of the batch. + function committedBatches(uint256 batchIndex) external view returns (bytes32); - /// @notice Return the merkle root of L2 message tree. - /// @param batchHash The hash of the batch to query. - function getL2MessageRoot(bytes32 batchHash) external view returns (bytes32); + /// @notice Return the state root of a committed batch. + /// @param batchIndex The index of the batch. + function finalizedStateRoots(uint256 batchIndex) external view returns (bytes32); + + /// @notice Return the message root of a committed batch. + /// @param batchIndex The index of the batch. + function withdrawRoots(uint256 batchIndex) external view returns (bytes32); + + /// @notice Return whether the batch is finalized by batch index. + /// @param batchIndex The index of the batch. + function isBatchFinalized(uint256 batchIndex) external view returns (bool); /***************************** * Public Mutating Functions * *****************************/ - /// @notice commit a batch in layer 1 - /// @param batch The layer2 batch to commit. - function commitBatch(Batch memory batch) external; + /// @notice Commit a batch of transactions on layer 1. + /// + /// @param version The version of current batch. + /// @param parentBatchHeader The header of parent batch, see the comments of `BatchHeaderV0Codec`. + /// @param chunks The list of encoded chunks, see the comments of `ChunkCodec`. + /// @param skippedL1MessageBitmap The bitmap indicates whether each L1 message is skipped or not. + function commitBatch( + uint8 version, + bytes calldata parentBatchHeader, + bytes[] memory chunks, + bytes calldata skippedL1MessageBitmap + ) external; - /// @notice commit a list of batches in layer 1 - /// @param batches The list of layer2 batches to commit. - function commitBatches(Batch[] memory batches) external; - - /// @notice revert a pending batch. + /// @notice Revert a pending batch. /// @dev one can only revert unfinalized batches. - /// @param batchId The identification of the batch. - function revertBatch(bytes32 batchId) external; + /// @param batchHeader The header of current batch, see the encoding in comments of `commitBatch`. + function revertBatch(bytes calldata batchHeader) external; - /// @notice finalize commited batch in layer 1 - /// @dev will add more parameters if needed. - /// @param batchId The identification of the commited batch. - /// @param proof The corresponding proof of the commited batch. - /// @param instances Instance used to verify, generated from batch. + /// @notice Finalize a committed batch on layer 1. + /// @param batchHeader The header of current batch, see the encoding in comments of `commitBatch. + /// @param prevStateRoot The state root of parent batch. + /// @param postStateRoot The state root of current batch. + /// @param withdrawRoot The withdraw trie root of current batch. + /// @param aggrProof The aggregation proof for current batch. function finalizeBatchWithProof( - bytes32 batchId, - uint256[] memory proof, - uint256[] memory instances + bytes calldata batchHeader, + bytes32 prevStateRoot, + bytes32 postStateRoot, + bytes32 withdrawRoot, + bytes calldata aggrProof ) external; } diff --git a/contracts/src/L1/rollup/L1MessageQueue.sol b/contracts/src/L1/rollup/L1MessageQueue.sol index 3fc83ae40..3b62f8fe5 100644 --- a/contracts/src/L1/rollup/L1MessageQueue.sol +++ b/contracts/src/L1/rollup/L1MessageQueue.sol @@ -39,14 +39,20 @@ contract L1MessageQueue is OwnableUpgradeable, IL1MessageQueue { /// @notice The address of L1ScrollMessenger contract. address public messenger; + /// @notice The address of ScrollChain contract. + address public scrollChain; + + /// @notice The address EnforcedTxGateway contract. + address public enforcedTxGateway; + /// @notice The address of GasOracle contract. address public gasOracle; /// @notice The list of queued cross domain messages. bytes32[] public messageQueue; - /// @notice The address EnforcedTxGateway contract. - address public enforcedTxGateway; + /// @inheritdoc IL1MessageQueue + uint256 public pendingQueueIndex; /// @notice The max gas limit of L1 transactions. uint256 public maxGasLimit; @@ -57,6 +63,7 @@ contract L1MessageQueue is OwnableUpgradeable, IL1MessageQueue { function initialize( address _messenger, + address _scrollChain, address _enforcedTxGateway, address _gasOracle, uint256 _maxGasLimit @@ -64,6 +71,7 @@ contract L1MessageQueue is OwnableUpgradeable, IL1MessageQueue { OwnableUpgradeable.__Ownable_init(); messenger = _messenger; + scrollChain = _scrollChain; enforcedTxGateway = _enforcedTxGateway; gasOracle = _gasOracle; maxGasLimit = _maxGasLimit; @@ -261,6 +269,30 @@ contract L1MessageQueue is OwnableUpgradeable, IL1MessageQueue { _queueTransaction(_sender, _target, _value, _gasLimit, _data); } + /// @inheritdoc IL1MessageQueue + function popCrossDomainMessage( + uint256 _startIndex, + uint256 _count, + uint256 _skippedBitmap + ) external { + require(msg.sender == scrollChain, "Only callable by the ScrollChain"); + + require(_count <= 256, "pop too many messages"); + require(pendingQueueIndex == _startIndex, "start index mismatch"); + + unchecked { + for (uint256 i = 0; i < _count; i++) { + if ((_skippedBitmap >> i) & 1 == 0) { + messageQueue[_startIndex + i] = bytes32(0); + } + } + + pendingQueueIndex = _startIndex + _count; + } + + emit DequeueTransaction(_startIndex, _count, _skippedBitmap); + } + /************************ * Restricted Functions * ************************/ diff --git a/contracts/src/L1/rollup/ScrollChain.sol b/contracts/src/L1/rollup/ScrollChain.sol index 030f282b4..1557af040 100644 --- a/contracts/src/L1/rollup/ScrollChain.sol +++ b/contracts/src/L1/rollup/ScrollChain.sol @@ -6,17 +6,14 @@ import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Own import {IL1MessageQueue} from "./IL1MessageQueue.sol"; import {IScrollChain} from "./IScrollChain.sol"; -import {RollupVerifier} from "../../libraries/verifier/RollupVerifier.sol"; +import {BatchHeaderV0Codec} from "../../libraries/codec/BatchHeaderV0Codec.sol"; +import {ChunkCodec} from "../../libraries/codec/ChunkCodec.sol"; +import {IRollupVerifier} from "../../libraries/verifier/IRollupVerifier.sol"; // solhint-disable reason-string /// @title ScrollChain -/// @notice This contract maintains essential data for scroll rollup, including: -/// -/// 1. a list of pending messages, which will be relayed to layer 2; -/// 2. the block tree generated by layer 2 and it's status. -/// -/// @dev the message queue is not used yet, the offline relayer only use events in `L1ScrollMessenger`. +/// @notice This contract maintains data for the Scroll rollup. contract ScrollChain is OwnableUpgradeable, IScrollChain { /********** * Events * @@ -27,61 +24,50 @@ contract ScrollChain is OwnableUpgradeable, IScrollChain { /// @param status The status of the account updated. event UpdateSequencer(address indexed account, bool status); + /// @notice Emitted when the address of rollup verifier is updated. + /// @param oldVerifier The address of old rollup verifier. + /// @param newVerifier The address of new rollup verifier. + event UpdateVerifier(address oldVerifier, address newVerifier); + + /// @notice Emitted when the value of `maxNumL2TxInChunk` is updated. + /// @param oldMaxNumL2TxInChunk The old value of `maxNumL2TxInChunk`. + /// @param newMaxNumL2TxInChunk The new value of `maxNumL2TxInChunk`. + event UpdateMaxNumL2TxInChunk(uint256 oldMaxNumL2TxInChunk, uint256 newMaxNumL2TxInChunk); + /************* * Constants * *************/ - /// @dev The maximum number of transaction in on batch. - uint256 public immutable maxNumTxInBatch; - - /// @dev The hash used for padding public inputs. - bytes32 public immutable paddingTxHash; - /// @notice The chain id of the corresponding layer 2 chain. uint256 public immutable layer2ChainId; - /*********** - * Structs * - ***********/ - - // subject to change - struct BatchStored { - // The state root of the last block in this batch. - bytes32 newStateRoot; - // The withdraw trie root of the last block in this batch. - bytes32 withdrawTrieRoot; - // The parent batch hash. - bytes32 parentBatchHash; - // The index of the batch. - uint64 batchIndex; - // The timestamp of the last block in this batch. - uint64 timestamp; - // The number of transactions in this batch, both L1 & L2 txs. - uint64 numTransactions; - // The total number of L1 messages included after this batch. - uint64 totalL1Messages; - // Whether the batch is finalized. - bool finalized; - } - /************* * Variables * *************/ + /// @notice The maximum number of transactions allowed in each chunk. + uint256 public maxNumL2TxInChunk; + /// @notice The address of L1MessageQueue. address public messageQueue; + /// @notice The address of RollupVerifier. + address public verifier; + /// @notice Whether an account is a sequencer. mapping(address => bool) public isSequencer; - /// @notice The latest finalized batch hash. - bytes32 public lastFinalizedBatchHash; + /// @notice The latest finalized batch index. + uint256 public lastFinalizedBatchIndex; - /// @notice Mapping from batch id to batch struct. - mapping(bytes32 => BatchStored) public batches; + /// @inheritdoc IScrollChain + mapping(uint256 => bytes32) public override committedBatches; - /// @notice Mapping from batch index to finalized batch hash. - mapping(uint256 => bytes32) public finalizedBatches; + /// @inheritdoc IScrollChain + mapping(uint256 => bytes32) public override finalizedStateRoots; + + /// @inheritdoc IScrollChain + mapping(uint256 => bytes32) public override withdrawRoots; /********************** * Function Modifiers * @@ -97,20 +83,23 @@ contract ScrollChain is OwnableUpgradeable, IScrollChain { * Constructor * ***************/ - constructor( - uint256 _chainId, - uint256 _maxNumTxInBatch, - bytes32 _paddingTxHash - ) { + constructor(uint256 _chainId) { layer2ChainId = _chainId; - maxNumTxInBatch = _maxNumTxInBatch; - paddingTxHash = _paddingTxHash; } - function initialize(address _messageQueue) public initializer { + function initialize( + address _messageQueue, + address _verifier, + uint256 _maxNumL2TxInChunk + ) public initializer { OwnableUpgradeable.__Ownable_init(); messageQueue = _messageQueue; + verifier = _verifier; + maxNumL2TxInChunk = _maxNumL2TxInChunk; + + emit UpdateVerifier(address(0), _verifier); + emit UpdateMaxNumL2TxInChunk(0, _maxNumL2TxInChunk); } /************************* @@ -118,17 +107,8 @@ contract ScrollChain is OwnableUpgradeable, IScrollChain { *************************/ /// @inheritdoc IScrollChain - function isBatchFinalized(bytes32 _batchHash) external view override returns (bool) { - BatchStored storage _batch = batches[_batchHash]; - if (_batch.newStateRoot == bytes32(0)) { - return false; - } - return batches[lastFinalizedBatchHash].batchIndex >= _batch.batchIndex; - } - - /// @inheritdoc IScrollChain - function getL2MessageRoot(bytes32 _batchHash) external view override returns (bytes32) { - return batches[_batchHash].withdrawTrieRoot; + function isBatchFinalized(uint256 _batchIndex) external view override returns (bool) { + return _batchIndex <= lastFinalizedBatchIndex; } /***************************** @@ -136,80 +116,212 @@ contract ScrollChain is OwnableUpgradeable, IScrollChain { *****************************/ /// @notice Import layer 2 genesis block - function importGenesisBatch(Batch memory _genesisBatch) external { - require(lastFinalizedBatchHash == bytes32(0), "Genesis batch imported"); - require(_genesisBatch.blocks.length == 1, "Not exact one block in genesis"); - require(_genesisBatch.prevStateRoot == bytes32(0), "Nonzero prevStateRoot"); + /// @dev Although `_withdrawRoot` is always zero, we add this parameter for the convenience of unit testing. + function importGenesisBatch( + bytes calldata _batchHeader, + bytes32 _stateRoot, + bytes32 _withdrawRoot + ) external { + // check genesis batch header length + require(_stateRoot != bytes32(0), "zero state root"); - BlockContext memory _genesisBlock = _genesisBatch.blocks[0]; + // check whether the genesis batch is imported + require(finalizedStateRoots[0] == bytes32(0), "Genesis batch imported"); - require(_genesisBlock.blockHash != bytes32(0), "Block hash is zero"); - require(_genesisBlock.blockNumber == 0, "Block is not genesis"); - require(_genesisBlock.parentHash == bytes32(0), "Parent hash not empty"); + (uint256 memPtr, bytes32 _batchHash) = _loadBatchHeader(_batchHeader); - bytes32 _batchHash = _commitBatch(_genesisBatch); - - lastFinalizedBatchHash = _batchHash; - finalizedBatches[0] = _batchHash; - batches[_batchHash].finalized = true; - - emit FinalizeBatch(_batchHash); - } - - /// @inheritdoc IScrollChain - function commitBatch(Batch memory _batch) public override OnlySequencer { - _commitBatch(_batch); - } - - /// @inheritdoc IScrollChain - function commitBatches(Batch[] memory _batches) public override OnlySequencer { - for (uint256 i = 0; i < _batches.length; i++) { - _commitBatch(_batches[i]); + // check all fields except `dataHash` and `lastBlockHash` are zero + unchecked { + uint256 sum = BatchHeaderV0Codec.version(memPtr) + + BatchHeaderV0Codec.batchIndex(memPtr) + + BatchHeaderV0Codec.l1MessagePopped(memPtr) + + BatchHeaderV0Codec.totalL1MessagePopped(memPtr); + require(sum == 0, "not all fields are zero"); } + require(BatchHeaderV0Codec.dataHash(memPtr) != bytes32(0), "zero data hash"); + require(BatchHeaderV0Codec.parentBatchHash(memPtr) == bytes32(0), "nonzero parent batch hash"); + + committedBatches[0] = _batchHash; + finalizedStateRoots[0] = _stateRoot; + withdrawRoots[0] = _withdrawRoot; + + emit CommitBatch(_batchHash); + emit FinalizeBatch(_batchHash, _stateRoot, _withdrawRoot); } /// @inheritdoc IScrollChain - function revertBatch(bytes32 _batchHash) external override OnlySequencer { - BatchStored storage _batch = batches[_batchHash]; + function commitBatch( + uint8 _version, + bytes calldata _parentBatchHeader, + bytes[] memory _chunks, + bytes calldata _skippedL1MessageBitmap + ) external override OnlySequencer { + require(_version == 0, "invalid version"); - require(_batch.newStateRoot != bytes32(0), "No such batch"); - require(!_batch.finalized, "Unable to revert finalized batch"); + // check whether the batch is empty + uint256 _chunksLength = _chunks.length; + require(_chunksLength > 0, "batch is empty"); - // delete committed batch - delete batches[_batchHash]; + // The overall memory layout in this function is organized as follows + // +---------------------+-------------------+------------------+ + // | parent batch header | chunk data hashes | new batch header | + // +---------------------+-------------------+------------------+ + // ^ ^ ^ + // batchPtr dataPtr newBatchPtr (re-use var batchPtr) + // + // 1. We copy the parent batch header from calldata to memory starting at batchPtr + // 2. We store `_chunksLength` number of Keccak hashes starting at `dataPtr`. Each Keccak + // hash corresponds to the data hash of a chunk. So we reserve the memory region from + // `dataPtr` to `dataPtr + _chunkLength * 32` for the chunk data hashes. + // 3. The memory starting at `newBatchPtr` is used to store the new batch header and compute + // the batch hash. + + // the variable `batchPtr` will be reused later for the current batch + (uint256 batchPtr, bytes32 _parentBatchHash) = _loadBatchHeader(_parentBatchHeader); + + uint256 _batchIndex = BatchHeaderV0Codec.batchIndex(batchPtr); + uint256 _totalL1MessagesPoppedOverall = BatchHeaderV0Codec.totalL1MessagePopped(batchPtr); + require(committedBatches[_batchIndex] == _parentBatchHash, "incorrect parent batch hash"); + require(committedBatches[_batchIndex + 1] == 0, "batch already committed"); + + // load `dataPtr` and reserve the memory region for chunk data hashes + uint256 dataPtr; + assembly { + dataPtr := mload(0x40) + mstore(0x40, add(dataPtr, mul(_chunksLength, 32))) + } + + // compute the data hash for each chunk + uint256 _totalL1MessagesPoppedInBatch; + for (uint256 i = 0; i < _chunksLength; i++) { + uint256 _totalNumL1MessagesInChunk = _commitChunk( + dataPtr, + _chunks[i], + _totalL1MessagesPoppedInBatch, + _totalL1MessagesPoppedOverall, + _skippedL1MessageBitmap + ); + + unchecked { + _totalL1MessagesPoppedInBatch += _totalNumL1MessagesInChunk; + _totalL1MessagesPoppedOverall += _totalNumL1MessagesInChunk; + dataPtr += 32; + } + } + + // check the length of bitmap + unchecked { + require( + ((_totalL1MessagesPoppedInBatch + 255) / 256) * 32 == _skippedL1MessageBitmap.length, + "wrong bitmap length" + ); + } + + // compute the data hash for current batch + bytes32 _dataHash; + assembly { + let dataLen := mul(_chunksLength, 0x20) + _dataHash := keccak256(sub(dataPtr, dataLen), dataLen) + + batchPtr := mload(0x40) // reset batchPtr + _batchIndex := add(_batchIndex, 1) // increase batch index + } + + // store entries, the order matters + BatchHeaderV0Codec.storeVersion(batchPtr, _version); + BatchHeaderV0Codec.storeBatchIndex(batchPtr, _batchIndex); + BatchHeaderV0Codec.storeL1MessagePopped(batchPtr, _totalL1MessagesPoppedInBatch); + BatchHeaderV0Codec.storeTotalL1MessagePopped(batchPtr, _totalL1MessagesPoppedOverall); + BatchHeaderV0Codec.storeDataHash(batchPtr, _dataHash); + BatchHeaderV0Codec.storeParentBatchHash(batchPtr, _parentBatchHash); + BatchHeaderV0Codec.storeSkippedBitmap(batchPtr, _skippedL1MessageBitmap); + + // compute batch hash + bytes32 _batchHash = BatchHeaderV0Codec.computeBatchHash(batchPtr, 89 + _skippedL1MessageBitmap.length); + + committedBatches[_batchIndex] = _batchHash; + emit CommitBatch(_batchHash); + } + + /// @inheritdoc IScrollChain + function revertBatch(bytes calldata _batchHeader) external onlyOwner { + (uint256 memPtr, bytes32 _batchHash) = _loadBatchHeader(_batchHeader); + + // check batch hash + uint256 _batchIndex = BatchHeaderV0Codec.batchIndex(memPtr); + require(committedBatches[_batchIndex] == _batchHash, "incorrect batch hash"); + + // check finalization + require(_batchIndex > lastFinalizedBatchIndex, "can only revert unfinalized batch"); + + committedBatches[_batchIndex] = bytes32(0); emit RevertBatch(_batchHash); } /// @inheritdoc IScrollChain function finalizeBatchWithProof( - bytes32 _batchHash, - uint256[] memory _proof, - uint256[] memory _instances + bytes calldata _batchHeader, + bytes32 _prevStateRoot, + bytes32 _postStateRoot, + bytes32 _withdrawRoot, + bytes calldata _aggrProof ) external override OnlySequencer { - BatchStored storage _batch = batches[_batchHash]; - require(_batch.newStateRoot != bytes32(0), "No such batch"); - require(!_batch.finalized, "Batch is already finalized"); + require(_prevStateRoot != bytes32(0), "previous state root is zero"); + require(_postStateRoot != bytes32(0), "new state root is zero"); - // @note skip parent check for now, since we may not prove blocks in order. - // bytes32 _parentHash = _block.header.parentHash; - // require(lastFinalizedBlockHash == _parentHash, "parent not latest finalized"); - // this check below is not needed, just incase - // require(blocks[_parentHash].verified, "parent not verified"); + // compute batch hash and verify + (uint256 memPtr, bytes32 _batchHash) = _loadBatchHeader(_batchHeader); - // @todo add verification logic - RollupVerifier.verify(_proof, _instances); + bytes32 _dataHash = BatchHeaderV0Codec.dataHash(memPtr); + uint256 _batchIndex = BatchHeaderV0Codec.batchIndex(memPtr); + require(committedBatches[_batchIndex] == _batchHash, "incorrect batch hash"); - uint256 _batchIndex = _batch.batchIndex; - finalizedBatches[_batchIndex] = _batchHash; - _batch.finalized = true; + // verify previous state root. + require(finalizedStateRoots[_batchIndex - 1] == _prevStateRoot, "incorrect previous state root"); - BatchStored storage _finalizedBatch = batches[lastFinalizedBatchHash]; - if (_batchIndex > _finalizedBatch.batchIndex) { - lastFinalizedBatchHash = _batchHash; + // avoid duplicated verification + require(finalizedStateRoots[_batchIndex] == bytes32(0), "batch already verified"); + + // compute public input hash + bytes32 _publicInputHash = keccak256(abi.encode(_prevStateRoot, _postStateRoot, _withdrawRoot, _dataHash)); + + // verify batch + IRollupVerifier(verifier).verifyAggregateProof(_aggrProof, _publicInputHash); + + // check and update lastFinalizedBatchIndex + unchecked { + require(lastFinalizedBatchIndex + 1 == _batchIndex, "incorrect batch index"); + lastFinalizedBatchIndex = _batchIndex; } - emit FinalizeBatch(_batchHash); + // record state root and withdraw root + finalizedStateRoots[_batchIndex] = _postStateRoot; + withdrawRoots[_batchIndex] = _withdrawRoot; + + // Pop finalized and non-skipped message from L1MessageQueue. + uint256 _l1MessagePopped = BatchHeaderV0Codec.l1MessagePopped(memPtr); + if (_l1MessagePopped > 0) { + IL1MessageQueue _queue = IL1MessageQueue(messageQueue); + + unchecked { + uint256 _startIndex = BatchHeaderV0Codec.totalL1MessagePopped(memPtr) - _l1MessagePopped; + + for (uint256 i = 0; i < _l1MessagePopped; i += 256) { + uint256 _count = 256; + if (_l1MessagePopped - i < _count) { + _count = _l1MessagePopped - i; + } + uint256 _skippedBitmap = BatchHeaderV0Codec.skippedBitmap(memPtr, i / 256); + + _queue.popCrossDomainMessage(_startIndex, _count, _skippedBitmap); + + _startIndex += 256; + } + } + } + + emit FinalizeBatch(_batchHash, _postStateRoot, _withdrawRoot); } /************************ @@ -226,197 +338,173 @@ contract ScrollChain is OwnableUpgradeable, IScrollChain { emit UpdateSequencer(_account, _status); } + /// @notice Update the address verifier contract. + /// @param _newVerifier The address of new verifier contract. + function updateVerifier(address _newVerifier) external onlyOwner { + address _oldVerifier = verifier; + verifier = _newVerifier; + + emit UpdateVerifier(_oldVerifier, _newVerifier); + } + + /// @notice Update the value of `maxNumL2TxInChunk`. + /// @param _maxNumL2TxInChunk The new value of `maxNumL2TxInChunk`. + function updateMaxNumL2TxInChunk(uint256 _maxNumL2TxInChunk) external onlyOwner { + uint256 _oldMaxNumL2TxInChunk = maxNumL2TxInChunk; + maxNumL2TxInChunk = _maxNumL2TxInChunk; + + emit UpdateMaxNumL2TxInChunk(_oldMaxNumL2TxInChunk, _maxNumL2TxInChunk); + } + /********************** * Internal Functions * **********************/ - /// @dev Internal function to commit a batch. - /// @param _batch The batch to commit. - function _commitBatch(Batch memory _batch) internal returns (bytes32) { - // check whether the batch is empty - require(_batch.blocks.length > 0, "Batch is empty"); + /// @dev Internal function to load batch header from calldata to memory. + /// @param _batchHeader The batch header in calldata. + /// @return memPtr The start memory offset of loaded batch header. + /// @return _batchHash The hash of the loaded batch header. + function _loadBatchHeader(bytes calldata _batchHeader) internal pure returns (uint256 memPtr, bytes32 _batchHash) { + // load to memory + uint256 _length; + (memPtr, _length) = BatchHeaderV0Codec.loadAndValidate(_batchHeader); - BatchStored storage _parentBatch = batches[_batch.parentBatchHash]; - require( - _parentBatch.newStateRoot == _batch.prevStateRoot, - "prevStateRoot is different from newStateRoot in the parent batch" - ); - uint64 accTotalL1Messages = _parentBatch.totalL1Messages; - - bytes32 publicInputHash; - uint64 numTransactionsInBatch; - uint64 lastBlockTimestamp; - (publicInputHash, numTransactionsInBatch, accTotalL1Messages, lastBlockTimestamp) = _computePublicInputHash( - accTotalL1Messages, - _batch - ); - - BatchStored storage _batchInStorage = batches[publicInputHash]; - - require(_batchInStorage.newStateRoot == bytes32(0), "Batch already commited"); - _batchInStorage.newStateRoot = _batch.newStateRoot; - _batchInStorage.withdrawTrieRoot = _batch.withdrawTrieRoot; - _batchInStorage.batchIndex = _batch.batchIndex; - _batchInStorage.parentBatchHash = _batch.parentBatchHash; - _batchInStorage.timestamp = lastBlockTimestamp; - _batchInStorage.numTransactions = numTransactionsInBatch; - _batchInStorage.totalL1Messages = accTotalL1Messages; - - emit CommitBatch(publicInputHash); - - return publicInputHash; + // compute batch hash + _batchHash = BatchHeaderV0Codec.computeBatchHash(memPtr, _length); } - /// @dev Internal function to compute the public input hash. - /// @param accTotalL1Messages The number of total L1 messages in previous batch. - /// @param batch The batch to compute. - function _computePublicInputHash(uint64 accTotalL1Messages, Batch memory batch) - internal - view - returns ( - bytes32, - uint64, - uint64, - uint64 - ) - { - uint256 publicInputsPtr; - // 1. append prevStateRoot, newStateRoot and withdrawTrieRoot to public inputs - { - bytes32 prevStateRoot = batch.prevStateRoot; - bytes32 newStateRoot = batch.newStateRoot; - bytes32 withdrawTrieRoot = batch.withdrawTrieRoot; - // number of bytes in public inputs: 32 * 3 + 124 * blocks + 32 * MAX_NUM_TXS - uint256 publicInputsSize = 32 * 3 + batch.blocks.length * 124 + 32 * maxNumTxInBatch; - assembly { - publicInputsPtr := mload(0x40) - mstore(0x40, add(publicInputsPtr, publicInputsSize)) - mstore(publicInputsPtr, prevStateRoot) - publicInputsPtr := add(publicInputsPtr, 0x20) - mstore(publicInputsPtr, newStateRoot) - publicInputsPtr := add(publicInputsPtr, 0x20) - mstore(publicInputsPtr, withdrawTrieRoot) - publicInputsPtr := add(publicInputsPtr, 0x20) + /// @dev Internal function to commit a chunk. + /// @param memPtr The start memory offset to store list of `dataHash`. + /// @param _chunk The encoded chunk to commit. + /// @param _totalL1MessagesPoppedInBatch The total number of L1 messages popped in current batch. + /// @param _totalL1MessagesPoppedOverall The total number of L1 messages popped in all batches including current batch. + /// @param _skippedL1MessageBitmap The bitmap indicates whether each L1 message is skipped or not. + /// @return _totalNumL1MessagesInChunk The total number of L1 message popped in current chunk + function _commitChunk( + uint256 memPtr, + bytes memory _chunk, + uint256 _totalL1MessagesPoppedInBatch, + uint256 _totalL1MessagesPoppedOverall, + bytes calldata _skippedL1MessageBitmap + ) internal view returns (uint256 _totalNumL1MessagesInChunk) { + uint256 chunkPtr; + uint256 dataPtr; + uint256 blockPtr; + + assembly { + dataPtr := mload(0x40) + chunkPtr := add(_chunk, 0x20) // skip chunkLength + blockPtr := add(chunkPtr, 1) // skip numBlocks + } + + uint256 _numBlocks = ChunkCodec.validateChunkLength(chunkPtr, _chunk.length); + + // concatenate block contexts + for (uint256 i = 0; i < _numBlocks; i++) { + dataPtr = ChunkCodec.copyBlockContext(chunkPtr, dataPtr, i); + } + + // concatenate tx hashes + uint256 l2TxPtr = ChunkCodec.l2TxPtr(chunkPtr, _numBlocks); + + // avoid stack too deep on forge coverage + uint256 _totalTransactionsInChunk; + while (_numBlocks > 0) { + // concatenate l1 message hashes + uint256 _numL1MessagesInBlock = ChunkCodec.numL1Messages(blockPtr); + dataPtr = _loadL1MessageHashes( + dataPtr, + _numL1MessagesInBlock, + _totalL1MessagesPoppedInBatch, + _totalL1MessagesPoppedOverall, + _skippedL1MessageBitmap + ); + + // concatenate l2 transaction hashes + uint256 _numTransactionsInBlock = ChunkCodec.numTransactions(blockPtr); + for (uint256 j = _numL1MessagesInBlock; j < _numTransactionsInBlock; j++) { + bytes32 txHash; + (txHash, l2TxPtr) = ChunkCodec.loadL2TxHash(l2TxPtr); + assembly { + mstore(dataPtr, txHash) + dataPtr := add(dataPtr, 0x20) + } + } + + unchecked { + _totalTransactionsInChunk += _numTransactionsInBlock; + _totalNumL1MessagesInChunk += _numL1MessagesInBlock; + _totalL1MessagesPoppedInBatch += _numL1MessagesInBlock; + _totalL1MessagesPoppedOverall += _numL1MessagesInBlock; + + _numBlocks -= 1; + blockPtr += ChunkCodec.BLOCK_CONTEXT_LENGTH; } } - uint64 numTransactionsInBatch; - BlockContext memory _block; - // 2. append block information to public inputs. - for (uint256 i = 0; i < batch.blocks.length; i++) { - // validate blocks, we won't check first block against previous batch. - { - BlockContext memory _currentBlock = batch.blocks[i]; - if (i > 0) { - require(_block.blockHash == _currentBlock.parentHash, "Parent hash mismatch"); - require(_block.blockNumber + 1 == _currentBlock.blockNumber, "Block number mismatch"); - } - _block = _currentBlock; - } + // check the number of L2 transactions in the chunk + require( + _totalTransactionsInChunk - _totalNumL1MessagesInChunk <= maxNumL2TxInChunk, + "too many L2 txs in one chunk" + ); - // append blockHash and parentHash to public inputs - { - bytes32 blockHash = _block.blockHash; - bytes32 parentHash = _block.parentHash; - assembly { - mstore(publicInputsPtr, blockHash) - publicInputsPtr := add(publicInputsPtr, 0x20) - mstore(publicInputsPtr, parentHash) - publicInputsPtr := add(publicInputsPtr, 0x20) - } - } - // append blockNumber and blockTimestamp to public inputs - { - uint256 blockNumber = _block.blockNumber; - uint256 blockTimestamp = _block.timestamp; - assembly { - mstore(publicInputsPtr, shl(192, blockNumber)) - publicInputsPtr := add(publicInputsPtr, 0x8) - mstore(publicInputsPtr, shl(192, blockTimestamp)) - publicInputsPtr := add(publicInputsPtr, 0x8) - } - } - // append baseFee to public inputs - { - uint256 baseFee = _block.baseFee; - assembly { - mstore(publicInputsPtr, baseFee) - publicInputsPtr := add(publicInputsPtr, 0x20) - } - } - uint64 numTransactionsInBlock = _block.numTransactions; - // gasLimit, numTransactions and numL1Messages to public inputs - { - uint256 gasLimit = _block.gasLimit; - uint256 numL1MessagesInBlock = _block.numL1Messages; - assembly { - mstore(publicInputsPtr, shl(192, gasLimit)) - publicInputsPtr := add(publicInputsPtr, 0x8) - mstore(publicInputsPtr, shl(240, numTransactionsInBlock)) - publicInputsPtr := add(publicInputsPtr, 0x2) - mstore(publicInputsPtr, shl(240, numL1MessagesInBlock)) - publicInputsPtr := add(publicInputsPtr, 0x2) - } - } - numTransactionsInBatch += numTransactionsInBlock; - } - require(numTransactionsInBatch <= maxNumTxInBatch, "Too many transactions in batch"); + // check chunk has correct length + require(l2TxPtr - chunkPtr == _chunk.length, "incomplete l2 transaction data"); - // 3. append transaction hash to public inputs. - address _messageQueue = messageQueue; - uint256 _l2TxnPtr; - { - bytes memory l2Transactions = batch.l2Transactions; - assembly { - _l2TxnPtr := add(l2Transactions, 0x20) - } + // compute data hash and store to memory + assembly { + let startPtr := mload(0x40) + let dataHash := keccak256(startPtr, sub(dataPtr, startPtr)) + + mstore(memPtr, dataHash) } - for (uint256 i = 0; i < batch.blocks.length; i++) { - uint256 numL1MessagesInBlock = batch.blocks[i].numL1Messages; - while (numL1MessagesInBlock > 0) { - bytes32 hash = IL1MessageQueue(_messageQueue).getCrossDomainMessage(uint64(accTotalL1Messages)); - assembly { - mstore(publicInputsPtr, hash) - publicInputsPtr := add(publicInputsPtr, 0x20) + + return _totalNumL1MessagesInChunk; + } + + /// @dev Internal function to load L1 message hashes from the message queue. + /// @param _ptr The memory offset to store the transaction hash. + /// @param _numL1Messages The number of L1 messages to load. + /// @param _totalL1MessagesPoppedInBatch The total number of L1 messages popped in current batch. + /// @param _totalL1MessagesPoppedOverall The total number of L1 messages popped in all batches including current batch. + /// @param _skippedL1MessageBitmap The bitmap indicates whether each L1 message is skipped or not. + /// @return uint256 The new memory offset after loading. + function _loadL1MessageHashes( + uint256 _ptr, + uint256 _numL1Messages, + uint256 _totalL1MessagesPoppedInBatch, + uint256 _totalL1MessagesPoppedOverall, + bytes calldata _skippedL1MessageBitmap + ) internal view returns (uint256) { + if (_numL1Messages == 0) return _ptr; + IL1MessageQueue _messageQueue = IL1MessageQueue(messageQueue); + + unchecked { + uint256 _bitmap; + for (uint256 i = 0; i < _numL1Messages; i++) { + uint256 quo = _totalL1MessagesPoppedInBatch >> 8; + uint256 rem = _totalL1MessagesPoppedInBatch & 0xff; + + // load bitmap every 256 bits + if (i == 0 || rem == 0) { + assembly { + _bitmap := calldataload(add(_skippedL1MessageBitmap.offset, mul(0x20, quo))) + } } - unchecked { - accTotalL1Messages += 1; - numL1MessagesInBlock -= 1; - } - } - numL1MessagesInBlock = batch.blocks[i].numL1Messages; - uint256 numTransactionsInBlock = batch.blocks[i].numTransactions; - for (uint256 j = numL1MessagesInBlock; j < numTransactionsInBlock; ++j) { - bytes32 hash; - assembly { - let txPayloadLength := shr(224, mload(_l2TxnPtr)) - _l2TxnPtr := add(_l2TxnPtr, 4) - _l2TxnPtr := add(_l2TxnPtr, txPayloadLength) - hash := keccak256(sub(_l2TxnPtr, txPayloadLength), txPayloadLength) - mstore(publicInputsPtr, hash) - publicInputsPtr := add(publicInputsPtr, 0x20) + if (((_bitmap >> rem) & 1) == 0) { + // message not skipped + bytes32 _hash = _messageQueue.getCrossDomainMessage(_totalL1MessagesPoppedOverall); + assembly { + mstore(_ptr, _hash) + _ptr := add(_ptr, 0x20) + } } + + _totalL1MessagesPoppedInBatch += 1; + _totalL1MessagesPoppedOverall += 1; } } - // 4. append padding transaction to public inputs. - bytes32 txHashPadding = paddingTxHash; - for (uint256 i = numTransactionsInBatch; i < maxNumTxInBatch; i++) { - assembly { - mstore(publicInputsPtr, txHashPadding) - publicInputsPtr := add(publicInputsPtr, 0x20) - } - } - - // 5. compute public input hash - bytes32 publicInputHash; - { - uint256 publicInputsSize = 32 * 3 + batch.blocks.length * 124 + 32 * maxNumTxInBatch; - assembly { - publicInputHash := keccak256(sub(publicInputsPtr, publicInputsSize), publicInputsSize) - } - } - - return (publicInputHash, numTransactionsInBatch, accTotalL1Messages, _block.timestamp); + return _ptr; } } diff --git a/contracts/src/L1/rollup/ScrollChainCommitmentVerifier.sol b/contracts/src/L1/rollup/ScrollChainCommitmentVerifier.sol index 3d349dc7c..3cb3ed6fc 100644 --- a/contracts/src/L1/rollup/ScrollChainCommitmentVerifier.sol +++ b/contracts/src/L1/rollup/ScrollChainCommitmentVerifier.sol @@ -38,22 +38,22 @@ contract ScrollChainCommitmentVerifier { } /// @notice Verifies a batch inclusion proof. - /// @param batchHash The hash of the batch. + /// @param batchIndex The index of the batch. /// @param account The address of the contract in L2. /// @param storageKey The storage key inside the contract in L2. /// @param proof The rlp encoding result of eth_getProof. /// @return storageValue The value of `storageKey`. function verifyStateCommitment( - bytes32 batchHash, + uint256 batchIndex, address account, bytes32 storageKey, bytes calldata proof ) external view returns (bytes32 storageValue) { - require(ScrollChain(rollup).isBatchFinalized(batchHash), "Batch not finalized"); + require(ScrollChain(rollup).isBatchFinalized(batchIndex), "Batch not finalized"); bytes32 computedStateRoot; (computedStateRoot, storageValue) = ZkTrieVerifier.verifyZkTrieProof(poseidon, account, storageKey, proof); - (bytes32 expectedStateRoot, , , , , , , ) = ScrollChain(rollup).batches(batchHash); + bytes32 expectedStateRoot = ScrollChain(rollup).finalizedStateRoots(batchIndex); require(computedStateRoot == expectedStateRoot, "Invalid inclusion proof"); } } diff --git a/contracts/src/L2/L2ScrollMessenger.sol b/contracts/src/L2/L2ScrollMessenger.sol index 384c612e4..d0fb53006 100644 --- a/contracts/src/L2/L2ScrollMessenger.sol +++ b/contracts/src/L2/L2ScrollMessenger.sol @@ -37,8 +37,6 @@ contract L2ScrollMessenger is ScrollMessengerBase, PausableUpgradeable, IL2Scrol * Constants * *************/ - uint256 private constant MIN_GAS_LIMIT = 21000; - /// @notice The contract contains the list of L1 blocks. address public immutable blockContainer; @@ -194,7 +192,7 @@ contract L2ScrollMessenger is ScrollMessengerBase, PausableUpgradeable, IL2Scrol bytes memory _message, uint256 _gasLimit ) external payable override whenNotPaused { - _sendMessage(_to, _value, _message, _gasLimit, tx.origin); + _sendMessage(_to, _value, _message, _gasLimit); } /// @inheritdoc IScrollMessenger @@ -203,9 +201,9 @@ contract L2ScrollMessenger is ScrollMessengerBase, PausableUpgradeable, IL2Scrol uint256 _value, bytes calldata _message, uint256 _gasLimit, - address _refundAddress + address ) external payable override whenNotPaused { - _sendMessage(_to, _value, _message, _gasLimit, _refundAddress); + _sendMessage(_to, _value, _message, _gasLimit); } /// @inheritdoc IL2ScrollMessenger @@ -266,6 +264,9 @@ contract L2ScrollMessenger is ScrollMessengerBase, PausableUpgradeable, IL2Scrol } } + /// @notice Update max failed execution times. + /// @dev This function can only called by contract owner. + /// @param _maxFailedExecutionTimes The new max failed execution times. function updateMaxFailedExecutionTimes(uint256 _maxFailedExecutionTimes) external onlyOwner { maxFailedExecutionTimes = _maxFailedExecutionTimes; @@ -276,25 +277,18 @@ contract L2ScrollMessenger is ScrollMessengerBase, PausableUpgradeable, IL2Scrol * Internal Functions * **********************/ + /// @dev Internal function to send cross domain message. + /// @param _to The address of account who receive the message. + /// @param _value The amount of ether passed when call target contract. + /// @param _message The content of the message. + /// @param _gasLimit Optional gas limit to complete the message relay on corresponding chain. function _sendMessage( address _to, uint256 _value, bytes memory _message, - uint256 _gasLimit, - address _refundAddress + uint256 _gasLimit ) internal nonReentrant { - // by pass fee vault relay - if (feeVault != msg.sender) { - require(_gasLimit >= MIN_GAS_LIMIT, "gas limit too small"); - } - - // compute and deduct the messaging fee to fee vault. - uint256 _fee = _gasLimit * IL1GasPriceOracle(gasOracle).l1BaseFee(); - require(msg.value >= _value + _fee, "Insufficient msg.value"); - if (_fee > 0) { - (bool _success, ) = feeVault.call{value: _fee}(""); - require(_success, "Failed to deduct the fee"); - } + require(msg.value == _value, "msg.value mismatch"); uint256 _nonce = L2MessageQueue(messageQueue).nextMessageIndex(); bytes32 _xDomainCalldataHash = keccak256(_encodeXDomainCalldata(msg.sender, _to, _value, _nonce, _message)); @@ -306,17 +300,14 @@ contract L2ScrollMessenger is ScrollMessengerBase, PausableUpgradeable, IL2Scrol L2MessageQueue(messageQueue).appendMessage(_xDomainCalldataHash); emit SentMessage(msg.sender, _to, _value, _nonce, _gasLimit, _message); - - // refund fee to tx.origin - unchecked { - uint256 _refund = msg.value - _fee - _value; - if (_refund > 0) { - (bool _success, ) = _refundAddress.call{value: _refund}(""); - require(_success, "Failed to refund the fee"); - } - } } + /// @dev Internal function to execute a L1 => L2 message. + /// @param _from The address of the sender of the message. + /// @param _to The address of the recipient of the message. + /// @param _value The msg.value passed to the message call. + /// @param _message The content of the message. + /// @param _xDomainCalldataHash The hash of the message. function _executeMessage( address _from, address _to, diff --git a/contracts/src/L2/gateways/L2CustomERC20Gateway.sol b/contracts/src/L2/gateways/L2CustomERC20Gateway.sol index 575470f90..d8dc8a979 100644 --- a/contracts/src/L2/gateways/L2CustomERC20Gateway.sol +++ b/contracts/src/L2/gateways/L2CustomERC20Gateway.sol @@ -9,7 +9,7 @@ import {IL2ERC20Gateway, L2ERC20Gateway} from "./L2ERC20Gateway.sol"; import {IL2ScrollMessenger} from "../IL2ScrollMessenger.sol"; import {IL1ERC20Gateway} from "../../L1/gateways/IL1ERC20Gateway.sol"; import {ScrollGatewayBase, IScrollGateway} from "../../libraries/gateway/ScrollGatewayBase.sol"; -import {IScrollStandardERC20} from "../../libraries/token/IScrollStandardERC20.sol"; +import {IScrollERC20} from "../../libraries/token/IScrollERC20.sol"; /// @title L2ERC20Gateway /// @notice The `L2ERC20Gateway` is used to withdraw custom ERC20 compatible tokens in layer 2 and @@ -80,7 +80,7 @@ contract L2CustomERC20Gateway is OwnableUpgradeable, ScrollGatewayBase, L2ERC20G require(_l1Token != address(0), "token address cannot be 0"); require(_l1Token == tokenMapping[_l2Token], "l1 token mismatch"); - IScrollStandardERC20(_l2Token).mint(_to, _amount); + IScrollERC20(_l2Token).mint(_to, _amount); _doCallback(_to, _data); @@ -126,7 +126,7 @@ contract L2CustomERC20Gateway is OwnableUpgradeable, ScrollGatewayBase, L2ERC20G } // 2. Burn token. - IScrollStandardERC20(_token).burn(_from, _amount); + IScrollERC20(_token).burn(_from, _amount); // 3. Generate message passed to L1StandardERC20Gateway. bytes memory _message = abi.encodeWithSelector( diff --git a/contracts/src/L2/gateways/L2GatewayRouter.sol b/contracts/src/L2/gateways/L2GatewayRouter.sol index 7a6a47c36..096aaeff9 100644 --- a/contracts/src/L2/gateways/L2GatewayRouter.sol +++ b/contracts/src/L2/gateways/L2GatewayRouter.sol @@ -10,7 +10,6 @@ import {IL2ERC20Gateway} from "./IL2ERC20Gateway.sol"; import {IL2ScrollMessenger} from "../IL2ScrollMessenger.sol"; import {IL1ETHGateway} from "../../L1/gateways/IL1ETHGateway.sol"; import {IScrollGateway} from "../../libraries/gateway/IScrollGateway.sol"; -import {IScrollStandardERC20} from "../../libraries/token/IScrollStandardERC20.sol"; /// @title L2GatewayRouter /// @notice The `L2GatewayRouter` is the main entry for withdrawing Ether and ERC20 tokens. diff --git a/contracts/src/L2/gateways/L2StandardERC20Gateway.sol b/contracts/src/L2/gateways/L2StandardERC20Gateway.sol index 47939e6a3..663ea45e3 100644 --- a/contracts/src/L2/gateways/L2StandardERC20Gateway.sol +++ b/contracts/src/L2/gateways/L2StandardERC20Gateway.sol @@ -10,7 +10,7 @@ import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {IL2ERC20Gateway, L2ERC20Gateway} from "./L2ERC20Gateway.sol"; import {IL2ScrollMessenger} from "../IL2ScrollMessenger.sol"; import {IL1ERC20Gateway} from "../../L1/gateways/IL1ERC20Gateway.sol"; -import {IScrollStandardERC20} from "../../libraries/token/IScrollStandardERC20.sol"; +import {IScrollERC20} from "../../libraries/token/IScrollERC20.sol"; import {ScrollStandardERC20} from "../../libraries/token/ScrollStandardERC20.sol"; import {IScrollStandardERC20Factory} from "../../libraries/token/IScrollStandardERC20Factory.sol"; import {ScrollGatewayBase, IScrollGateway} from "../../libraries/gateway/ScrollGatewayBase.sol"; @@ -107,7 +107,7 @@ contract L2StandardERC20Gateway is Initializable, ScrollGatewayBase, L2ERC20Gate _deployL2Token(_deployData, _l1Token); } - IScrollStandardERC20(_l2Token).mint(_to, _amount); + IScrollERC20(_l2Token).mint(_to, _amount); _doCallback(_to, _callData); @@ -138,7 +138,7 @@ contract L2StandardERC20Gateway is Initializable, ScrollGatewayBase, L2ERC20Gate require(_l1Token != address(0), "no corresponding l1 token"); // 2. Burn token. - IScrollStandardERC20(_token).burn(_from, _amount); + IScrollERC20(_token).burn(_from, _amount); // 3. Generate message passed to L1StandardERC20Gateway. bytes memory _message = abi.encodeWithSelector( diff --git a/contracts/src/L2/predeploys/IL1GasPriceOracle.sol b/contracts/src/L2/predeploys/IL1GasPriceOracle.sol index 6aa59d6c3..f876463a7 100644 --- a/contracts/src/L2/predeploys/IL1GasPriceOracle.sol +++ b/contracts/src/L2/predeploys/IL1GasPriceOracle.sol @@ -40,7 +40,7 @@ interface IL1GasPriceOracle { /// @notice Computes the amount of L1 gas used for a transaction. Adds the overhead which /// represents the per-transaction gas overhead of posting the transaction and state - /// roots to L1. Adds 68 bytes of padding to account for the fact that the input does + /// roots to L1. Adds 74 bytes of padding to account for the fact that the input does /// not have a signature. /// @param data Unsigned fully RLP-encoded transaction to get the L1 gas for. /// @return Amount of L1 gas used to publish the transaction. diff --git a/contracts/src/L2/predeploys/L1GasPriceOracle.sol b/contracts/src/L2/predeploys/L1GasPriceOracle.sol index 1bc627364..5b3553263 100644 --- a/contracts/src/L2/predeploys/L1GasPriceOracle.sol +++ b/contracts/src/L2/predeploys/L1GasPriceOracle.sol @@ -69,8 +69,14 @@ contract L1GasPriceOracle is OwnableBase, IL1GasPriceOracle { } /// @inheritdoc IL1GasPriceOracle - /// @dev See the comments in `OVM_GasPriceOracle1` for more details - /// https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts/contracts/L2/predeploys/OVM_GasPriceOracle.sol + /// @dev The extra 74 bytes on top of the RLP encoded unsigned transaction data consist of + /// 4 bytes Add 4 bytes in the beginning for transaction data length + /// 1 byte RLP V prefix + /// 3 bytes V + /// 1 bytes RLP R prefix + /// 32 bytes R + /// 1 bytes RLP S prefix + /// 32 bytes S function getL1GasUsed(bytes memory _data) public view override returns (uint256) { uint256 _total = 0; uint256 _length = _data.length; @@ -83,7 +89,7 @@ contract L1GasPriceOracle is OwnableBase, IL1GasPriceOracle { } } uint256 _unsigned = _total + overhead; - return _unsigned + (68 * 16); + return _unsigned + (74 * 16); } } diff --git a/contracts/src/libraries/ScrollMessengerBase.sol b/contracts/src/libraries/ScrollMessengerBase.sol index 820e9c463..714c5cd1b 100644 --- a/contracts/src/libraries/ScrollMessengerBase.sol +++ b/contracts/src/libraries/ScrollMessengerBase.sol @@ -4,7 +4,6 @@ pragma solidity ^0.8.0; import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; -import {IWhitelist} from "./common/IWhitelist.sol"; import {ScrollConstants} from "./constants/ScrollConstants.sol"; import {IScrollMessenger} from "./IScrollMessenger.sol"; @@ -13,11 +12,6 @@ abstract contract ScrollMessengerBase is OwnableUpgradeable, IScrollMessenger { * Events * **********/ - /// @notice Emitted when owner updates whitelist contract. - /// @param _oldWhitelist The address of old whitelist contract. - /// @param _newWhitelist The address of new whitelist contract. - event UpdateWhitelist(address _oldWhitelist, address _newWhitelist); - /// @notice Emitted when owner updates fee vault contract. /// @param _oldFeeVault The address of old fee vault contract. /// @param _newFeeVault The address of new fee vault contract. @@ -38,25 +32,12 @@ abstract contract ScrollMessengerBase is OwnableUpgradeable, IScrollMessenger { /// @notice See {IScrollMessenger-xDomainMessageSender} address public override xDomainMessageSender; - /// @notice The whitelist contract to track the sender who can call `sendMessage` in ScrollMessenger. - address public whitelist; - /// @notice The address of counterpart ScrollMessenger contract in L1/L2. address public counterpart; /// @notice The address of fee vault, collecting cross domain messaging fee. address public feeVault; - /********************** - * Function Modifiers * - **********************/ - - modifier onlyWhitelistedSender(address _sender) { - address _whitelist = whitelist; - require(_whitelist == address(0) || IWhitelist(_whitelist).isSenderAllowed(_sender), "sender not whitelisted"); - _; - } - /*************** * Constructor * ***************/ @@ -78,21 +59,11 @@ abstract contract ScrollMessengerBase is OwnableUpgradeable, IScrollMessenger { * Restricted Functions * ************************/ - /// @notice Update whitelist contract. - /// @dev This function can only called by contract owner. - /// @param _newWhitelist The address of new whitelist contract. - function updateWhitelist(address _newWhitelist) external onlyOwner { - address _oldWhitelist = whitelist; - - whitelist = _newWhitelist; - emit UpdateWhitelist(_oldWhitelist, _newWhitelist); - } - /// @notice Update fee vault contract. /// @dev This function can only called by contract owner. /// @param _newFeeVault The address of new fee vault contract. function updateFeeVault(address _newFeeVault) external onlyOwner { - address _oldFeeVault = whitelist; + address _oldFeeVault = feeVault; feeVault = _newFeeVault; emit UpdateFeeVault(_oldFeeVault, _newFeeVault); diff --git a/contracts/src/libraries/codec/BatchHeaderV0Codec.sol b/contracts/src/libraries/codec/BatchHeaderV0Codec.sol new file mode 100644 index 000000000..1480434d6 --- /dev/null +++ b/contracts/src/libraries/codec/BatchHeaderV0Codec.sol @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/// @dev Below is the encoding for `BatchHeader` V0, total 89 + ceil(l1MessagePopped / 256) * 32 bytes. +/// ```text +/// * Field Bytes Type Index Comments +/// * version 1 uint8 0 The batch version +/// * batchIndex 8 uint64 1 The index of the batch +/// * l1MessagePopped 8 uint64 9 Number of L1 messages popped in the batch +/// * totalL1MessagePopped 8 uint64 17 Number of total L1 message popped after the batch +/// * dataHash 32 bytes32 25 The data hash of the batch +/// * parentBatchHash 32 bytes32 57 The parent batch hash +/// * skippedL1MessageBitmap dynamic uint256[] 89 A bitmap to indicate which L1 messages are skipped in the batch +/// ``` +library BatchHeaderV0Codec { + /// @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; + require(length >= 89, "batch header length too small"); + + // copy batch header to memory. + assembly { + batchPtr := mload(0x40) + calldatacopy(batchPtr, _batchHeader.offset, length) + mstore(0x40, add(batchPtr, length)) + } + + // check batch header length + uint256 _l1MessagePopped = BatchHeaderV0Codec.l1MessagePopped(batchPtr); + + unchecked { + require(length == 89 + ((_l1MessagePopped + 255) / 256) * 32, "wrong bitmap 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 version(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 batchIndex(uint256 batchPtr) internal pure returns (uint256 _batchIndex) { + assembly { + _batchIndex := shr(192, mload(add(batchPtr, 1))) + } + } + + /// @notice Get the number of L1 messages of the batch. + /// @param batchPtr The start memory offset of the batch header in memory. + /// @return _l1MessagePopped The number of L1 messages of the batch. + function l1MessagePopped(uint256 batchPtr) internal pure returns (uint256 _l1MessagePopped) { + assembly { + _l1MessagePopped := shr(192, mload(add(batchPtr, 9))) + } + } + + /// @notice Get the number of L1 messages popped before this batch. + /// @param batchPtr The start memory offset of the batch header in memory. + /// @return _totalL1MessagePopped The the number of L1 messages popped before this batch. + function totalL1MessagePopped(uint256 batchPtr) internal pure returns (uint256 _totalL1MessagePopped) { + assembly { + _totalL1MessagePopped := shr(192, mload(add(batchPtr, 17))) + } + } + + /// @notice Get the data hash of the batch header. + /// @param batchPtr The start memory offset of the batch header in memory. + /// @return _dataHash The data hash of the batch header. + function dataHash(uint256 batchPtr) internal pure returns (bytes32 _dataHash) { + assembly { + _dataHash := mload(add(batchPtr, 25)) + } + } + + /// @notice Get the parent batch hash of the batch header. + /// @param batchPtr The start memory offset of the batch header in memory. + /// @return _parentBatchHash The parent batch hash of the batch header. + function parentBatchHash(uint256 batchPtr) internal pure returns (bytes32 _parentBatchHash) { + assembly { + _parentBatchHash := mload(add(batchPtr, 57)) + } + } + + /// @notice Get the skipped L1 messages bitmap. + /// @param batchPtr The start memory offset of the batch header in memory. + /// @param index The index of bitmap to load. + /// @return _bitmap The bitmap from bits `index * 256` to `index * 256 + 255`. + function skippedBitmap(uint256 batchPtr, uint256 index) internal pure returns (uint256 _bitmap) { + assembly { + batchPtr := add(batchPtr, 89) + _bitmap := mload(add(batchPtr, mul(index, 32))) + } + } + + /// @notice Store the version of batch header. + /// @param batchPtr The start memory offset of the batch header in memory. + /// @param _version The version of batch header. + function storeVersion(uint256 batchPtr, uint256 _version) internal pure { + assembly { + mstore8(batchPtr, _version) + } + } + + /// @notice Store the batch index of batch header. + /// @dev Because this function can overwrite the subsequent fields, it must be called before + /// `storeL1MessagePopped`, `storeTotalL1MessagePopped`, and `storeDataHash`. + /// @param batchPtr The start memory offset of the batch header in memory. + /// @param _batchIndex The batch index. + function storeBatchIndex(uint256 batchPtr, uint256 _batchIndex) internal pure { + assembly { + mstore(add(batchPtr, 1), shl(192, _batchIndex)) + } + } + + /// @notice Store the number of L1 messages popped in current batch to batch header. + /// @dev Because this function can overwrite the subsequent fields, it must be called before + /// `storeTotalL1MessagePopped` and `storeDataHash`. + /// @param batchPtr The start memory offset of the batch header in memory. + /// @param _l1MessagePopped The number of L1 messages popped in current batch. + function storeL1MessagePopped(uint256 batchPtr, uint256 _l1MessagePopped) internal pure { + assembly { + mstore(add(batchPtr, 9), shl(192, _l1MessagePopped)) + } + } + + /// @notice Store the total number of L1 messages popped after current batch to batch header. + /// @dev Because this function can overwrite the subsequent fields, it must be called before + /// `storeDataHash`. + /// @param batchPtr The start memory offset of the batch header in memory. + /// @param _totalL1MessagePopped The total number of L1 messages popped after current batch. + function storeTotalL1MessagePopped(uint256 batchPtr, uint256 _totalL1MessagePopped) internal pure { + assembly { + mstore(add(batchPtr, 17), shl(192, _totalL1MessagePopped)) + } + } + + /// @notice Store the data hash of batch header. + /// @param batchPtr The start memory offset of the batch header in memory. + /// @param _dataHash The data hash. + function storeDataHash(uint256 batchPtr, bytes32 _dataHash) internal pure { + assembly { + mstore(add(batchPtr, 25), _dataHash) + } + } + + /// @notice Store the parent batch hash of batch header. + /// @param batchPtr The start memory offset of the batch header in memory. + /// @param _parentBatchHash The parent batch hash. + function storeParentBatchHash(uint256 batchPtr, bytes32 _parentBatchHash) internal pure { + assembly { + mstore(add(batchPtr, 57), _parentBatchHash) + } + } + + /// @notice Store the skipped L1 message bitmap of batch header. + /// @param batchPtr The start memory offset of the batch header in memory. + /// @param _skippedL1MessageBitmap The skipped L1 message bitmap. + function storeSkippedBitmap(uint256 batchPtr, bytes calldata _skippedL1MessageBitmap) internal pure { + assembly { + calldatacopy(add(batchPtr, 89), _skippedL1MessageBitmap.offset, _skippedL1MessageBitmap.length) + } + } + + /// @notice Compute the batch hash. + /// @dev Caller should make sure that the encoded batch header is correct. + /// + /// @param batchPtr The memory offset of the encoded batch header. + /// @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) + } + } +} diff --git a/contracts/src/libraries/codec/ChunkCodec.sol b/contracts/src/libraries/codec/ChunkCodec.sol new file mode 100644 index 000000000..baf72556e --- /dev/null +++ b/contracts/src/libraries/codec/ChunkCodec.sol @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/// @dev Below is the encoding for `Chunk`, total 60*n+1+m bytes. +/// ```text +/// * Field Bytes Type Index Comments +/// * numBlocks 1 uint8 0 The number of blocks in this chunk +/// * block[0] 60 BlockContext 1 The first block in this chunk +/// * ...... +/// * block[i] 60 BlockContext 60*i+1 The (i+1)'th block in this chunk +/// * ...... +/// * block[n-1] 60 BlockContext 60*n-59 The last block in this chunk +/// * l2Transactions dynamic bytes 60*n+1 +/// ``` +/// +/// @dev Below is the encoding for `BlockContext`, total 60 bytes. +/// ```text +/// * Field Bytes Type Index Comments +/// * blockNumber 8 uint64 0 The height of this block. +/// * timestamp 8 uint64 8 The timestamp of this block. +/// * baseFee 32 uint256 16 The base fee of this block. Currently, it is always 0, because we disable EIP-1559. +/// * gasLimit 8 uint64 48 The gas limit of this block. +/// * numTransactions 2 uint16 56 The number of transactions in this block, both L1 & L2 txs. +/// * numL1Messages 2 uint16 58 The number of l1 messages in this block. +/// ``` +library ChunkCodec { + uint256 internal constant BLOCK_CONTEXT_LENGTH = 60; + + /// @notice Validate the length of chunk. + /// @param chunkPtr The start memory offset of the chunk in memory. + /// @param _length The length of the chunk. + /// @return _numBlocks The number of blocks in current chunk. + function validateChunkLength(uint256 chunkPtr, uint256 _length) internal pure returns (uint256 _numBlocks) { + _numBlocks = numBlocks(chunkPtr); + + // should contain at least one block + require(_numBlocks > 0, "no block in chunk"); + + // should contain at least the number of the blocks and block contexts + require(_length >= 1 + _numBlocks * BLOCK_CONTEXT_LENGTH, "invalid chunk length"); + } + + /// @notice Return the start memory offset of `l2Transactions`. + /// @dev The caller should make sure `_numBlocks` is correct. + /// @param chunkPtr The start memory offset of the chunk in memory. + /// @param _numBlocks The number of blocks in current chunk. + /// @return _l2TxPtr the start memory offset of `l2Transactions`. + function l2TxPtr(uint256 chunkPtr, uint256 _numBlocks) internal pure returns (uint256 _l2TxPtr) { + unchecked { + _l2TxPtr = chunkPtr + 1 + _numBlocks * BLOCK_CONTEXT_LENGTH; + } + } + + /// @notice Return the number of blocks in current chunk. + /// @param chunkPtr The start memory offset of the chunk in memory. + /// @return _numBlocks The number of blocks in current chunk. + function numBlocks(uint256 chunkPtr) internal pure returns (uint256 _numBlocks) { + assembly { + _numBlocks := shr(248, mload(chunkPtr)) + } + } + + /// @notice Copy the block context to another memory. + /// @param chunkPtr The start memory offset of the chunk in memory. + /// @param dstPtr The destination memory offset to store the block context. + /// @param index The index of block context to copy. + /// @return uint256 The new destination memory offset after copy. + function copyBlockContext( + uint256 chunkPtr, + uint256 dstPtr, + uint256 index + ) internal pure returns (uint256) { + // only first 58 bytes is needed. + assembly { + chunkPtr := add(chunkPtr, add(1, mul(BLOCK_CONTEXT_LENGTH, index))) + mstore(dstPtr, mload(chunkPtr)) // first 32 bytes + mstore( + add(dstPtr, 0x20), + and(mload(add(chunkPtr, 0x20)), 0xffffffffffffffffffffffffffffffffffffffffffffffffffff000000000000) + ) // next 26 bytes + + dstPtr := add(dstPtr, 58) + } + + return dstPtr; + } + + /// @notice Return the number of transactions in current block. + /// @param blockPtr The start memory offset of the block context in memory. + /// @return _numTransactions The number of transactions in current block. + function numTransactions(uint256 blockPtr) internal pure returns (uint256 _numTransactions) { + assembly { + _numTransactions := shr(240, mload(add(blockPtr, 56))) + } + } + + /// @notice Return the number of L1 messages in current block. + /// @param blockPtr The start memory offset of the block context in memory. + /// @return _numL1Messages The number of L1 messages in current block. + function numL1Messages(uint256 blockPtr) internal pure returns (uint256 _numL1Messages) { + assembly { + _numL1Messages := shr(240, mload(add(blockPtr, 58))) + } + } + + /// @notice Compute and load the transaction hash. + /// @param _l2TxPtr The start memory offset of the transaction in memory. + /// @return bytes32 The transaction hash of the transaction. + /// @return uint256 The start memory offset of the next transaction in memory. + function loadL2TxHash(uint256 _l2TxPtr) internal pure returns (bytes32, uint256) { + bytes32 txHash; + assembly { + // first 4 bytes indicate the length + let txPayloadLength := shr(224, mload(_l2TxPtr)) + _l2TxPtr := add(_l2TxPtr, 4) + txHash := keccak256(_l2TxPtr, txPayloadLength) + _l2TxPtr := add(_l2TxPtr, txPayloadLength) + } + + return (txHash, _l2TxPtr); + } +} diff --git a/contracts/src/libraries/gateway/ScrollGatewayBase.sol b/contracts/src/libraries/gateway/ScrollGatewayBase.sol index 635aa6191..bd144d293 100644 --- a/contracts/src/libraries/gateway/ScrollGatewayBase.sol +++ b/contracts/src/libraries/gateway/ScrollGatewayBase.sol @@ -57,7 +57,7 @@ abstract contract ScrollGatewayBase is IScrollGateway { modifier onlyCallByCounterpart() { address _messenger = messenger; // gas saving require(msg.sender == _messenger, "only messenger can call"); - require(counterpart == IScrollMessenger(_messenger).xDomainMessageSender(), "only call by conterpart"); + require(counterpart == IScrollMessenger(_messenger).xDomainMessageSender(), "only call by counterpart"); _; } diff --git a/contracts/src/libraries/token/IScrollERC20.sol b/contracts/src/libraries/token/IScrollERC20.sol new file mode 100644 index 000000000..d967e5318 --- /dev/null +++ b/contracts/src/libraries/token/IScrollERC20.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/draft-IERC20Permit.sol"; + +// The recommended ERC20 implementation for bridge token. +// deployed in L2 when original token is on L1 +// deployed in L1 when original token is on L2 +interface IScrollERC20 is IERC20, IERC20Permit { + /// @notice Return the address of Gateway the token belongs to. + function gateway() external view returns (address); + + /// @notice Return the address of counterpart token. + function counterpart() external view returns (address); + + /// @dev ERC677 Standard, see https://github.com/ethereum/EIPs/issues/677 + /// Defi can use this method to transfer L1/L2 token to L2/L1, + /// and deposit to L2/L1 contract in one transaction + function transferAndCall( + address receiver, + uint256 amount, + bytes calldata data + ) external returns (bool success); + + /// @notice Mint some token to recipient's account. + /// @dev Gateway Utilities, only gateway contract can call + /// @param _to The address of recipient. + /// @param _amount The amount of token to mint. + function mint(address _to, uint256 _amount) external; + + /// @notice Mint some token from account. + /// @dev Gateway Utilities, only gateway contract can call + /// @param _from The address of account to burn token. + /// @param _amount The amount of token to mint. + function burn(address _from, uint256 _amount) external; +} diff --git a/contracts/src/libraries/token/IScrollStandardERC20.sol b/contracts/src/libraries/token/IScrollERC20Upgradeable.sol similarity index 95% rename from contracts/src/libraries/token/IScrollStandardERC20.sol rename to contracts/src/libraries/token/IScrollERC20Upgradeable.sol index 4668bbcbf..2ac86e46a 100644 --- a/contracts/src/libraries/token/IScrollStandardERC20.sol +++ b/contracts/src/libraries/token/IScrollERC20Upgradeable.sol @@ -8,7 +8,7 @@ import {IERC20PermitUpgradeable} from "@openzeppelin/contracts-upgradeable/token // The recommended ERC20 implementation for bridge token. // deployed in L2 when original token is on L1 // deployed in L1 when original token is on L2 -interface IScrollStandardERC20 is IERC20Upgradeable, IERC20PermitUpgradeable { +interface IScrollERC20Upgradeable is IERC20Upgradeable, IERC20PermitUpgradeable { /// @notice Return the address of Gateway the token belongs to. function gateway() external view returns (address); diff --git a/contracts/src/libraries/token/ScrollStandardERC20.sol b/contracts/src/libraries/token/ScrollStandardERC20.sol index 1d47145f8..55c59e2e3 100644 --- a/contracts/src/libraries/token/ScrollStandardERC20.sol +++ b/contracts/src/libraries/token/ScrollStandardERC20.sol @@ -4,14 +4,21 @@ pragma solidity ^0.8.0; import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; import {ERC20PermitUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/draft-ERC20PermitUpgradeable.sol"; -import {IScrollStandardERC20} from "./IScrollStandardERC20.sol"; +import {IScrollERC20Upgradeable} from "./IScrollERC20Upgradeable.sol"; import {IERC677Receiver} from "../callbacks/IERC677Receiver.sol"; -contract ScrollStandardERC20 is ERC20PermitUpgradeable, IScrollStandardERC20 { - /// @inheritdoc IScrollStandardERC20 +/// @notice The `ScrollStandardERC20` is the ERC20 token contract created by +/// `L2StandardERC20Gateway` when the first time the L1 ERC20 is bridged via +/// `L1StandardERC20Gateway`. +/// @dev The reason that `ScrollStandardERC20` inherits `IScrollERC20Upgradeable` is because we need +/// to use the `initialize` function from the `ERC20PermitUpgradeable` to initialize the ERC20 +/// token. However, the token contract is NOT upgradable afterwards because +/// `ScrollStandardERC20Factory` uses `Clones` to deploy the `ScrollStandardERC20` contract. +contract ScrollStandardERC20 is ERC20PermitUpgradeable, IScrollERC20Upgradeable { + /// @inheritdoc IScrollERC20Upgradeable address public override gateway; - /// @inheritdoc IScrollStandardERC20 + /// @inheritdoc IScrollERC20Upgradeable address public override counterpart; uint8 private decimals_; @@ -73,18 +80,12 @@ contract ScrollStandardERC20 is ERC20PermitUpgradeable, IScrollStandardERC20 { return length > 0; } - /// @notice Mint some token to recipient's account. - /// @dev Gateway Utilities, only gateway contract can call - /// @param _to The address of recipient. - /// @param _amount The amount of token to mint. + /// @inheritdoc IScrollERC20Upgradeable function mint(address _to, uint256 _amount) external onlyGateway { _mint(_to, _amount); } - /// @notice Mint some token from account. - /// @dev Gateway Utilities, only gateway contract can call - /// @param _from The address of account to burn token. - /// @param _amount The amount of token to mint. + /// @inheritdoc IScrollERC20Upgradeable function burn(address _from, uint256 _amount) external onlyGateway { _burn(_from, _amount); } diff --git a/contracts/src/libraries/verifier/IRollupVerifier.sol b/contracts/src/libraries/verifier/IRollupVerifier.sol new file mode 100644 index 000000000..e5a5aff60 --- /dev/null +++ b/contracts/src/libraries/verifier/IRollupVerifier.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +interface IRollupVerifier { + /// @notice Verify aggregate zk proof. + /// @param aggrProof The aggregated proof. + /// @param publicInputHash The public input hash. + function verifyAggregateProof(bytes calldata aggrProof, bytes32 publicInputHash) external view; +} diff --git a/contracts/src/test/L1CustomERC20Gateway.t.sol b/contracts/src/test/L1CustomERC20Gateway.t.sol index 64b128471..933ec4a8d 100644 --- a/contracts/src/test/L1CustomERC20Gateway.t.sol +++ b/contracts/src/test/L1CustomERC20Gateway.t.sol @@ -139,8 +139,8 @@ contract L1CustomERC20GatewayTest is L1GatewayTestBase { gateway = new L1CustomERC20Gateway(); gateway.initialize(address(counterpartGateway), address(router), address(mockMessenger)); - // only call by conterpart - hevm.expectRevert("only call by conterpart"); + // only call by counterpart + hevm.expectRevert("only call by counterpart"); mockMessenger.callTarget( address(gateway), abi.encodeWithSelector( @@ -226,9 +226,9 @@ contract L1CustomERC20GatewayTest is L1GatewayTestBase { prepareL2MessageRoot(keccak256(xDomainCalldata)); IL1ScrollMessenger.L2MessageProof memory proof; - proof.batchHash = rollup.lastFinalizedBatchHash(); + proof.batchIndex = rollup.lastFinalizedBatchIndex(); - // conterpart is not L2WETHGateway + // counterpart is not L2WETHGateway // emit FailedRelayedMessage from L1ScrollMessenger hevm.expectEmit(true, false, false, true); emit FailedRelayedMessage(keccak256(xDomainCalldata)); @@ -285,7 +285,7 @@ contract L1CustomERC20GatewayTest is L1GatewayTestBase { prepareL2MessageRoot(keccak256(xDomainCalldata)); IL1ScrollMessenger.L2MessageProof memory proof; - proof.batchHash = rollup.lastFinalizedBatchHash(); + proof.batchIndex = rollup.lastFinalizedBatchIndex(); // emit FinalizeWithdrawERC20 from L1StandardERC20Gateway { diff --git a/contracts/src/test/L1ERC1155Gateway.t.sol b/contracts/src/test/L1ERC1155Gateway.t.sol index 509b90ed7..e72cf70f4 100644 --- a/contracts/src/test/L1ERC1155Gateway.t.sol +++ b/contracts/src/test/L1ERC1155Gateway.t.sol @@ -172,8 +172,8 @@ contract L1ERC1155GatewayTest is L1GatewayTestBase, ERC1155TokenReceiver { gateway = new L1ERC1155Gateway(); gateway.initialize(address(counterpartGateway), address(mockMessenger)); - // only call by conterpart - hevm.expectRevert("only call by conterpart"); + // only call by counterpart + hevm.expectRevert("only call by counterpart"); mockMessenger.callTarget( address(gateway), abi.encodeWithSelector( @@ -240,9 +240,9 @@ contract L1ERC1155GatewayTest is L1GatewayTestBase, ERC1155TokenReceiver { prepareL2MessageRoot(keccak256(xDomainCalldata)); IL1ScrollMessenger.L2MessageProof memory proof; - proof.batchHash = rollup.lastFinalizedBatchHash(); + proof.batchIndex = rollup.lastFinalizedBatchIndex(); - // conterpart is not L2WETHGateway + // counterpart is not L2WETHGateway // emit FailedRelayedMessage from L1ScrollMessenger hevm.expectEmit(true, false, false, true); emit FailedRelayedMessage(keccak256(xDomainCalldata)); @@ -304,7 +304,7 @@ contract L1ERC1155GatewayTest is L1GatewayTestBase, ERC1155TokenReceiver { prepareL2MessageRoot(keccak256(xDomainCalldata)); IL1ScrollMessenger.L2MessageProof memory proof; - proof.batchHash = rollup.lastFinalizedBatchHash(); + proof.batchIndex = rollup.lastFinalizedBatchIndex(); // emit FinalizeWithdrawERC1155 from L1ERC1155Gateway { @@ -357,8 +357,8 @@ contract L1ERC1155GatewayTest is L1GatewayTestBase, ERC1155TokenReceiver { gateway = new L1ERC1155Gateway(); gateway.initialize(address(counterpartGateway), address(mockMessenger)); - // only call by conterpart - hevm.expectRevert("only call by conterpart"); + // only call by counterpart + hevm.expectRevert("only call by counterpart"); mockMessenger.callTarget( address(gateway), abi.encodeWithSelector( @@ -431,9 +431,9 @@ contract L1ERC1155GatewayTest is L1GatewayTestBase, ERC1155TokenReceiver { prepareL2MessageRoot(keccak256(xDomainCalldata)); IL1ScrollMessenger.L2MessageProof memory proof; - proof.batchHash = rollup.lastFinalizedBatchHash(); + proof.batchIndex = rollup.lastFinalizedBatchIndex(); - // conterpart is not L2WETHGateway + // counterpart is not L2WETHGateway // emit FailedRelayedMessage from L1ScrollMessenger hevm.expectEmit(true, false, false, true); emit FailedRelayedMessage(keccak256(xDomainCalldata)); @@ -507,7 +507,7 @@ contract L1ERC1155GatewayTest is L1GatewayTestBase, ERC1155TokenReceiver { prepareL2MessageRoot(keccak256(xDomainCalldata)); IL1ScrollMessenger.L2MessageProof memory proof; - proof.batchHash = rollup.lastFinalizedBatchHash(); + proof.batchIndex = rollup.lastFinalizedBatchIndex(); // emit FinalizeBatchWithdrawERC1155 from L1ERC1155Gateway { diff --git a/contracts/src/test/L1ERC721Gateway.t.sol b/contracts/src/test/L1ERC721Gateway.t.sol index ac5f2c72c..7d48499d4 100644 --- a/contracts/src/test/L1ERC721Gateway.t.sol +++ b/contracts/src/test/L1ERC721Gateway.t.sol @@ -160,8 +160,8 @@ contract L1ERC721GatewayTest is L1GatewayTestBase { gateway = new L1ERC721Gateway(); gateway.initialize(address(counterpartGateway), address(mockMessenger)); - // only call by conterpart - hevm.expectRevert("only call by conterpart"); + // only call by counterpart + hevm.expectRevert("only call by counterpart"); mockMessenger.callTarget( address(gateway), abi.encodeWithSelector( @@ -223,9 +223,9 @@ contract L1ERC721GatewayTest is L1GatewayTestBase { prepareL2MessageRoot(keccak256(xDomainCalldata)); IL1ScrollMessenger.L2MessageProof memory proof; - proof.batchHash = rollup.lastFinalizedBatchHash(); + proof.batchIndex = rollup.lastFinalizedBatchIndex(); - // conterpart is not L2WETHGateway + // counterpart is not L2WETHGateway // emit FailedRelayedMessage from L1ScrollMessenger hevm.expectEmit(true, false, false, true); emit FailedRelayedMessage(keccak256(xDomainCalldata)); @@ -286,7 +286,7 @@ contract L1ERC721GatewayTest is L1GatewayTestBase { prepareL2MessageRoot(keccak256(xDomainCalldata)); IL1ScrollMessenger.L2MessageProof memory proof; - proof.batchHash = rollup.lastFinalizedBatchHash(); + proof.batchIndex = rollup.lastFinalizedBatchIndex(); // emit FinalizeWithdrawERC721 from L1ERC721Gateway { @@ -330,8 +330,8 @@ contract L1ERC721GatewayTest is L1GatewayTestBase { gateway = new L1ERC721Gateway(); gateway.initialize(address(counterpartGateway), address(mockMessenger)); - // only call by conterpart - hevm.expectRevert("only call by conterpart"); + // only call by counterpart + hevm.expectRevert("only call by counterpart"); mockMessenger.callTarget( address(gateway), abi.encodeWithSelector( @@ -397,9 +397,9 @@ contract L1ERC721GatewayTest is L1GatewayTestBase { prepareL2MessageRoot(keccak256(xDomainCalldata)); IL1ScrollMessenger.L2MessageProof memory proof; - proof.batchHash = rollup.lastFinalizedBatchHash(); + proof.batchIndex = rollup.lastFinalizedBatchIndex(); - // conterpart is not L2WETHGateway + // counterpart is not L2WETHGateway // emit FailedRelayedMessage from L1ScrollMessenger hevm.expectEmit(true, false, false, true); emit FailedRelayedMessage(keccak256(xDomainCalldata)); @@ -468,7 +468,7 @@ contract L1ERC721GatewayTest is L1GatewayTestBase { prepareL2MessageRoot(keccak256(xDomainCalldata)); IL1ScrollMessenger.L2MessageProof memory proof; - proof.batchHash = rollup.lastFinalizedBatchHash(); + proof.batchIndex = rollup.lastFinalizedBatchIndex(); // emit FinalizeBatchWithdrawERC721 from L1ERC721Gateway { diff --git a/contracts/src/test/L1ETHGateway.t.sol b/contracts/src/test/L1ETHGateway.t.sol index bf9f4ab07..6738c6560 100644 --- a/contracts/src/test/L1ETHGateway.t.sol +++ b/contracts/src/test/L1ETHGateway.t.sol @@ -116,8 +116,8 @@ contract L1ETHGatewayTest is L1GatewayTestBase { gateway = new L1ETHGateway(); gateway.initialize(address(counterpartGateway), address(router), address(mockMessenger)); - // only call by conterpart - hevm.expectRevert("only call by conterpart"); + // only call by counterpart + hevm.expectRevert("only call by counterpart"); mockMessenger.callTarget( address(gateway), abi.encodeWithSelector(gateway.finalizeWithdrawETH.selector, sender, recipient, amount, dataToCall) @@ -171,9 +171,9 @@ contract L1ETHGatewayTest is L1GatewayTestBase { prepareL2MessageRoot(keccak256(xDomainCalldata)); IL1ScrollMessenger.L2MessageProof memory proof; - proof.batchHash = rollup.lastFinalizedBatchHash(); + proof.batchIndex = rollup.lastFinalizedBatchIndex(); - // conterpart is not L2ETHGateway + // counterpart is not L2ETHGateway // emit FailedRelayedMessage from L1ScrollMessenger hevm.expectEmit(true, false, false, true); emit FailedRelayedMessage(keccak256(xDomainCalldata)); @@ -226,7 +226,7 @@ contract L1ETHGatewayTest is L1GatewayTestBase { prepareL2MessageRoot(keccak256(xDomainCalldata)); IL1ScrollMessenger.L2MessageProof memory proof; - proof.batchHash = rollup.lastFinalizedBatchHash(); + proof.batchIndex = rollup.lastFinalizedBatchIndex(); // emit FinalizeWithdrawETH from L1ETHGateway { diff --git a/contracts/src/test/L1GasPriceOracle.t.sol b/contracts/src/test/L1GasPriceOracle.t.sol index 709f0bd44..1d3fac973 100644 --- a/contracts/src/test/L1GasPriceOracle.t.sol +++ b/contracts/src/test/L1GasPriceOracle.t.sol @@ -99,7 +99,7 @@ contract L1GasPriceOracleTest is DSTestPlus { oracle.setOverhead(_overhead); - uint256 _gasUsed = _overhead + 68 * 16; + uint256 _gasUsed = _overhead + 74 * 16; for (uint256 i = 0; i < _data.length; i++) { if (_data[i] == 0) _gasUsed += 4; else _gasUsed += 16; @@ -122,7 +122,7 @@ contract L1GasPriceOracleTest is DSTestPlus { oracle.setScalar(_scalar); oracle.setL1BaseFee(_baseFee); - uint256 _gasUsed = _overhead + 68 * 16; + uint256 _gasUsed = _overhead + 74 * 16; for (uint256 i = 0; i < _data.length; i++) { if (_data[i] == 0) _gasUsed += 4; else _gasUsed += 16; diff --git a/contracts/src/test/L1GatewayTestBase.t.sol b/contracts/src/test/L1GatewayTestBase.t.sol index 1019387cc..f571e996e 100644 --- a/contracts/src/test/L1GatewayTestBase.t.sol +++ b/contracts/src/test/L1GatewayTestBase.t.sol @@ -56,8 +56,8 @@ abstract contract L1GatewayTestBase is DSTestPlus { l1Messenger = new L1ScrollMessenger(); messageQueue = new L1MessageQueue(); gasOracle = new L2GasPriceOracle(); + rollup = new ScrollChain(1233); enforcedTxGateway = new EnforcedTxGateway(); - rollup = new ScrollChain(1233, 25, bytes32(0)); whitelist = new Whitelist(address(this)); // Deploy L2 contracts @@ -65,10 +65,16 @@ abstract contract L1GatewayTestBase is DSTestPlus { // Initialize L1 contracts l1Messenger.initialize(address(l2Messenger), feeVault, address(rollup), address(messageQueue)); - messageQueue.initialize(address(l1Messenger), address(enforcedTxGateway), address(gasOracle), 10000000); + messageQueue.initialize( + address(l1Messenger), + address(rollup), + address(enforcedTxGateway), + address(gasOracle), + 10000000 + ); gasOracle.initialize(0, 0, 0, 0); gasOracle.updateWhitelist(address(whitelist)); - rollup.initialize(address(messageQueue)); + rollup.initialize(address(messageQueue), address(0), 44); address[] memory _accounts = new address[](1); _accounts[0] = address(this); @@ -76,12 +82,11 @@ abstract contract L1GatewayTestBase is DSTestPlus { } function prepareL2MessageRoot(bytes32 messageHash) internal { - IScrollChain.Batch memory _genesisBatch; - _genesisBatch.blocks = new IScrollChain.BlockContext[](1); - _genesisBatch.newStateRoot = bytes32(uint256(2)); - _genesisBatch.withdrawTrieRoot = messageHash; - _genesisBatch.blocks[0].blockHash = bytes32(uint256(1)); + bytes memory _batchHeader = new bytes(89); + assembly { + mstore(add(_batchHeader, add(0x20, 25)), 1) + } - rollup.importGenesisBatch(_genesisBatch); + rollup.importGenesisBatch(_batchHeader, bytes32(uint256(1)), messageHash); } } diff --git a/contracts/src/test/L1ScrollMessengerTest.t.sol b/contracts/src/test/L1ScrollMessengerTest.t.sol index 63b982156..cac899317 100644 --- a/contracts/src/test/L1ScrollMessengerTest.t.sol +++ b/contracts/src/test/L1ScrollMessengerTest.t.sol @@ -29,7 +29,7 @@ contract L1ScrollMessengerTest is DSTestPlus { l2Messenger = new L2ScrollMessenger(address(0), address(0), address(0)); // Deploy L1 contracts - scrollChain = new ScrollChain(0, 0, bytes32(0)); + scrollChain = new ScrollChain(0); l1MessageQueue = new L1MessageQueue(); l1Messenger = new L1ScrollMessenger(); gasOracle = new L2GasPriceOracle(); @@ -38,9 +38,15 @@ contract L1ScrollMessengerTest is DSTestPlus { // Initialize L1 contracts l1Messenger.initialize(address(l2Messenger), feeVault, address(scrollChain), address(l1MessageQueue)); - l1MessageQueue.initialize(address(l1Messenger), address(enforcedTxGateway), address(gasOracle), 10000000); + l1MessageQueue.initialize( + address(l1Messenger), + address(scrollChain), + address(enforcedTxGateway), + address(gasOracle), + 10000000 + ); gasOracle.initialize(0, 0, 0, 0); - scrollChain.initialize(address(l1MessageQueue)); + scrollChain.initialize(address(l1MessageQueue), address(0), 44); gasOracle.updateWhitelist(address(whitelist)); address[] memory _accounts = new address[](1); @@ -49,30 +55,38 @@ contract L1ScrollMessengerTest is DSTestPlus { } function testForbidCallMessageQueueFromL2() external { - IScrollChain.Batch memory genesisBatch; - genesisBatch.newStateRoot = bytes32(uint256(1)); - genesisBatch.withdrawTrieRoot = 0x35626eedbe5d8fb495fd20259ecb6c9a5574babab0b9ada1e93652baf717b085; - genesisBatch.blocks = new IScrollChain.BlockContext[](1); - genesisBatch.blocks[0].blockHash = bytes32(uint256(1)); - scrollChain.importGenesisBatch(genesisBatch); + // import genesis batch + bytes memory _batchHeader = new bytes(89); + assembly { + mstore(add(_batchHeader, add(0x20, 25)), 1) + } + scrollChain.importGenesisBatch( + _batchHeader, + bytes32(uint256(1)), + bytes32(0x3152134c22e545ab5d345248502b4f04ef5b45f735f939c7fe6ddc0ffefc9c52) + ); IL1ScrollMessenger.L2MessageProof memory proof; - proof.batchHash = scrollChain.lastFinalizedBatchHash(); + proof.batchIndex = scrollChain.lastFinalizedBatchIndex(); hevm.expectRevert("Forbid to call message queue"); l1Messenger.relayMessageWithProof(address(this), address(l1MessageQueue), 0, 0, new bytes(0), proof); } function testForbidCallSelfFromL2() external { - IScrollChain.Batch memory genesisBatch; - genesisBatch.newStateRoot = bytes32(uint256(1)); - genesisBatch.withdrawTrieRoot = 0x6e17d04543a7177b7ab3c42130fef5a10e6f47e9dfbd4ed577706ffb9d9273cc; - genesisBatch.blocks = new IScrollChain.BlockContext[](1); - genesisBatch.blocks[0].blockHash = bytes32(uint256(1)); - scrollChain.importGenesisBatch(genesisBatch); + // import genesis batch + bytes memory _batchHeader = new bytes(89); + assembly { + mstore(add(_batchHeader, 57), 1) + } + scrollChain.importGenesisBatch( + _batchHeader, + bytes32(uint256(1)), + bytes32(0xf7c03e2b13c88e3fca1410b228b001dd94e3f5ab4b4a4a6981d09a4eb3e5b631) + ); IL1ScrollMessenger.L2MessageProof memory proof; - proof.batchHash = scrollChain.lastFinalizedBatchHash(); + proof.batchIndex = scrollChain.lastFinalizedBatchIndex(); hevm.expectRevert("Forbid to call self"); l1Messenger.relayMessageWithProof(address(this), address(l1Messenger), 0, 0, new bytes(0), proof); @@ -105,12 +119,12 @@ contract L1ScrollMessengerTest is DSTestPlus { // Provided message has not been enqueued hevm.expectRevert("Provided message has not been enqueued"); - l1Messenger.replayMessage(address(this), address(0), 101, 0, new bytes(0), 0, 1, refundAddress); + l1Messenger.replayMessage(address(this), address(0), 101, 0, new bytes(0), 1, refundAddress); gasOracle.setL2BaseFee(1); // Insufficient msg.value hevm.expectRevert("Insufficient msg.value for fee"); - l1Messenger.replayMessage(address(this), address(0), 100, 0, new bytes(0), 0, 1, refundAddress); + l1Messenger.replayMessage(address(this), address(0), 100, 0, new bytes(0), 1, refundAddress); uint256 _fee = gasOracle.l2BaseFee() * 100; @@ -123,7 +137,6 @@ contract L1ScrollMessengerTest is DSTestPlus { 100, 0, new bytes(0), - 0, 100, refundAddress ); diff --git a/contracts/src/test/L1StandardERC20Gateway.t.sol b/contracts/src/test/L1StandardERC20Gateway.t.sol index 226556cfa..d24e9a43f 100644 --- a/contracts/src/test/L1StandardERC20Gateway.t.sol +++ b/contracts/src/test/L1StandardERC20Gateway.t.sol @@ -233,8 +233,8 @@ contract L1StandardERC20GatewayTest is L1GatewayTestBase { address(factory) ); - // only call by conterpart - hevm.expectRevert("only call by conterpart"); + // only call by counterpart + hevm.expectRevert("only call by counterpart"); mockMessenger.callTarget( address(gateway), abi.encodeWithSelector( @@ -302,9 +302,9 @@ contract L1StandardERC20GatewayTest is L1GatewayTestBase { prepareL2MessageRoot(keccak256(xDomainCalldata)); IL1ScrollMessenger.L2MessageProof memory proof; - proof.batchHash = rollup.lastFinalizedBatchHash(); + proof.batchIndex = rollup.lastFinalizedBatchIndex(); - // conterpart is not L2WETHGateway + // counterpart is not L2WETHGateway // emit FailedRelayedMessage from L1ScrollMessenger hevm.expectEmit(true, false, false, true); emit FailedRelayedMessage(keccak256(xDomainCalldata)); @@ -359,7 +359,7 @@ contract L1StandardERC20GatewayTest is L1GatewayTestBase { prepareL2MessageRoot(keccak256(xDomainCalldata)); IL1ScrollMessenger.L2MessageProof memory proof; - proof.batchHash = rollup.lastFinalizedBatchHash(); + proof.batchIndex = rollup.lastFinalizedBatchIndex(); // emit FinalizeWithdrawERC20 from L1StandardERC20Gateway { diff --git a/contracts/src/test/L1WETHGateway.t.sol b/contracts/src/test/L1WETHGateway.t.sol index d1417d38f..92859631f 100644 --- a/contracts/src/test/L1WETHGateway.t.sol +++ b/contracts/src/test/L1WETHGateway.t.sol @@ -154,8 +154,8 @@ contract L1WETHGatewayTest is L1GatewayTestBase { gateway = new L1WETHGateway(address(l1weth), address(l2weth)); gateway.initialize(address(counterpartGateway), address(router), address(mockMessenger)); - // only call by conterpart - hevm.expectRevert("only call by conterpart"); + // only call by counterpart + hevm.expectRevert("only call by counterpart"); mockMessenger.callTarget( address(gateway), abi.encodeWithSelector( @@ -254,9 +254,9 @@ contract L1WETHGatewayTest is L1GatewayTestBase { prepareL2MessageRoot(keccak256(xDomainCalldata)); IL1ScrollMessenger.L2MessageProof memory proof; - proof.batchHash = rollup.lastFinalizedBatchHash(); + proof.batchIndex = rollup.lastFinalizedBatchIndex(); - // conterpart is not L2WETHGateway + // counterpart is not L2WETHGateway // emit FailedRelayedMessage from L1ScrollMessenger hevm.expectEmit(true, false, false, true); emit FailedRelayedMessage(keccak256(xDomainCalldata)); @@ -311,7 +311,7 @@ contract L1WETHGatewayTest is L1GatewayTestBase { prepareL2MessageRoot(keccak256(xDomainCalldata)); IL1ScrollMessenger.L2MessageProof memory proof; - proof.batchHash = rollup.lastFinalizedBatchHash(); + proof.batchIndex = rollup.lastFinalizedBatchIndex(); // emit FinalizeWithdrawERC20 from L1WETHGateway { diff --git a/contracts/src/test/L2CustomERC20Gateway.t.sol b/contracts/src/test/L2CustomERC20Gateway.t.sol index a60d31c74..df7a43de6 100644 --- a/contracts/src/test/L2CustomERC20Gateway.t.sol +++ b/contracts/src/test/L2CustomERC20Gateway.t.sol @@ -138,8 +138,8 @@ contract L2CustomERC20GatewayTest is L2GatewayTestBase { gateway = new L2CustomERC20Gateway(); gateway.initialize(address(counterpartGateway), address(router), address(mockMessenger)); - // only call by conterpart - hevm.expectRevert("only call by conterpart"); + // only call by counterpart + hevm.expectRevert("only call by counterpart"); mockMessenger.callTarget( address(gateway), abi.encodeWithSelector( @@ -218,7 +218,7 @@ contract L2CustomERC20GatewayTest is L2GatewayTestBase { message ); - // conterpart is not L1CustomERC20Gateway + // counterpart is not L1CustomERC20Gateway // emit FailedRelayedMessage from L2ScrollMessenger hevm.expectEmit(true, false, false, true); emit FailedRelayedMessage(keccak256(xDomainCalldata)); @@ -302,7 +302,7 @@ contract L2CustomERC20GatewayTest is L2GatewayTestBase { ) private { amount = bound(amount, 0, l2Token.balanceOf(address(this))); gasLimit = bound(gasLimit, 21000, 1000000); - feePerGas = bound(feePerGas, 0, 1000); + feePerGas = 0; setL1BaseFee(feePerGas); @@ -327,18 +327,18 @@ contract L2CustomERC20GatewayTest is L2GatewayTestBase { hevm.expectRevert("no corresponding l1 token"); if (useRouter) { - router.withdrawERC20{value: feeToPay + extraValue}(address(l2Token), amount, gasLimit); + router.withdrawERC20{value: feeToPay}(address(l2Token), amount, gasLimit); } else { - gateway.withdrawERC20{value: feeToPay + extraValue}(address(l2Token), amount, gasLimit); + gateway.withdrawERC20{value: feeToPay}(address(l2Token), amount, gasLimit); } gateway.updateTokenMapping(address(l2Token), address(l1Token)); if (amount == 0) { hevm.expectRevert("withdraw zero amount"); if (useRouter) { - router.withdrawERC20{value: feeToPay + extraValue}(address(l2Token), amount, gasLimit); + router.withdrawERC20{value: feeToPay}(address(l2Token), amount, gasLimit); } else { - gateway.withdrawERC20{value: feeToPay + extraValue}(address(l2Token), amount, gasLimit); + gateway.withdrawERC20{value: feeToPay}(address(l2Token), amount, gasLimit); } } else { // emit AppendMessage from L2MessageQueue @@ -361,9 +361,9 @@ contract L2CustomERC20GatewayTest is L2GatewayTestBase { uint256 feeVaultBalance = address(feeVault).balance; assertBoolEq(false, l2Messenger.isL2MessageSent(keccak256(xDomainCalldata))); if (useRouter) { - router.withdrawERC20{value: feeToPay + extraValue}(address(l2Token), amount, gasLimit); + router.withdrawERC20{value: feeToPay}(address(l2Token), amount, gasLimit); } else { - gateway.withdrawERC20{value: feeToPay + extraValue}(address(l2Token), amount, gasLimit); + gateway.withdrawERC20{value: feeToPay}(address(l2Token), amount, gasLimit); } assertEq(gatewayBalance, l1Token.balanceOf(address(gateway))); assertEq(feeToPay + feeVaultBalance, address(feeVault).balance); @@ -380,7 +380,7 @@ contract L2CustomERC20GatewayTest is L2GatewayTestBase { ) private { amount = bound(amount, 0, l2Token.balanceOf(address(this))); gasLimit = bound(gasLimit, 21000, 1000000); - feePerGas = bound(feePerGas, 0, 1000); + feePerGas = 0; setL1BaseFee(feePerGas); @@ -405,18 +405,18 @@ contract L2CustomERC20GatewayTest is L2GatewayTestBase { hevm.expectRevert("no corresponding l1 token"); if (useRouter) { - router.withdrawERC20{value: feeToPay + extraValue}(address(l2Token), amount, gasLimit); + router.withdrawERC20{value: feeToPay}(address(l2Token), amount, gasLimit); } else { - gateway.withdrawERC20{value: feeToPay + extraValue}(address(l2Token), amount, gasLimit); + gateway.withdrawERC20{value: feeToPay}(address(l2Token), amount, gasLimit); } gateway.updateTokenMapping(address(l2Token), address(l1Token)); if (amount == 0) { hevm.expectRevert("withdraw zero amount"); if (useRouter) { - router.withdrawERC20{value: feeToPay + extraValue}(address(l2Token), recipient, amount, gasLimit); + router.withdrawERC20{value: feeToPay}(address(l2Token), recipient, amount, gasLimit); } else { - gateway.withdrawERC20{value: feeToPay + extraValue}(address(l2Token), recipient, amount, gasLimit); + gateway.withdrawERC20{value: feeToPay}(address(l2Token), recipient, amount, gasLimit); } } else { // emit AppendMessage from L2MessageQueue @@ -439,9 +439,9 @@ contract L2CustomERC20GatewayTest is L2GatewayTestBase { uint256 feeVaultBalance = address(feeVault).balance; assertBoolEq(false, l2Messenger.isL2MessageSent(keccak256(xDomainCalldata))); if (useRouter) { - router.withdrawERC20{value: feeToPay + extraValue}(address(l2Token), recipient, amount, gasLimit); + router.withdrawERC20{value: feeToPay}(address(l2Token), recipient, amount, gasLimit); } else { - gateway.withdrawERC20{value: feeToPay + extraValue}(address(l2Token), recipient, amount, gasLimit); + gateway.withdrawERC20{value: feeToPay}(address(l2Token), recipient, amount, gasLimit); } assertEq(gatewayBalance, l2Token.balanceOf(address(gateway))); assertEq(feeToPay + feeVaultBalance, address(feeVault).balance); @@ -459,7 +459,7 @@ contract L2CustomERC20GatewayTest is L2GatewayTestBase { ) private { amount = bound(amount, 0, l2Token.balanceOf(address(this))); gasLimit = bound(gasLimit, 21000, 1000000); - feePerGas = bound(feePerGas, 0, 1000); + feePerGas = 0; setL1BaseFee(feePerGas); @@ -484,24 +484,18 @@ contract L2CustomERC20GatewayTest is L2GatewayTestBase { hevm.expectRevert("no corresponding l1 token"); if (useRouter) { - router.withdrawERC20{value: feeToPay + extraValue}(address(l2Token), amount, gasLimit); + router.withdrawERC20{value: feeToPay}(address(l2Token), amount, gasLimit); } else { - gateway.withdrawERC20{value: feeToPay + extraValue}(address(l2Token), amount, gasLimit); + gateway.withdrawERC20{value: feeToPay}(address(l2Token), amount, gasLimit); } gateway.updateTokenMapping(address(l2Token), address(l1Token)); if (amount == 0) { hevm.expectRevert("withdraw zero amount"); if (useRouter) { - router.withdrawERC20AndCall{value: feeToPay + extraValue}( - address(l2Token), - recipient, - amount, - dataToCall, - gasLimit - ); + router.withdrawERC20AndCall{value: feeToPay}(address(l2Token), recipient, amount, dataToCall, gasLimit); } else { - gateway.withdrawERC20AndCall{value: feeToPay + extraValue}( + gateway.withdrawERC20AndCall{value: feeToPay}( address(l2Token), recipient, amount, @@ -530,15 +524,9 @@ contract L2CustomERC20GatewayTest is L2GatewayTestBase { uint256 feeVaultBalance = address(feeVault).balance; assertBoolEq(false, l2Messenger.isL2MessageSent(keccak256(xDomainCalldata))); if (useRouter) { - router.withdrawERC20AndCall{value: feeToPay + extraValue}( - address(l2Token), - recipient, - amount, - dataToCall, - gasLimit - ); + router.withdrawERC20AndCall{value: feeToPay}(address(l2Token), recipient, amount, dataToCall, gasLimit); } else { - gateway.withdrawERC20AndCall{value: feeToPay + extraValue}( + gateway.withdrawERC20AndCall{value: feeToPay}( address(l2Token), recipient, amount, diff --git a/contracts/src/test/L2ERC1155Gateway.t.sol b/contracts/src/test/L2ERC1155Gateway.t.sol index d0becc4e8..b5737e4ca 100644 --- a/contracts/src/test/L2ERC1155Gateway.t.sol +++ b/contracts/src/test/L2ERC1155Gateway.t.sol @@ -203,7 +203,7 @@ contract L2ERC1155GatewayTest is DSTestPlus, ERC1155TokenReceiver { gateway.finalizeDepositERC1155(address(0), address(0), address(0), address(0), 0, 1); // should revert, called by messenger, xDomainMessageSender not set - hevm.expectRevert("only call by conterpart"); + hevm.expectRevert("only call by counterpart"); messenger.callTarget( address(gateway), abi.encodeWithSelector( @@ -219,7 +219,7 @@ contract L2ERC1155GatewayTest is DSTestPlus, ERC1155TokenReceiver { // should revert, called by messenger, xDomainMessageSender set wrong messenger.setXDomainMessageSender(address(2)); - hevm.expectRevert("only call by conterpart"); + hevm.expectRevert("only call by counterpart"); messenger.callTarget( address(gateway), abi.encodeWithSelector( @@ -279,7 +279,7 @@ contract L2ERC1155GatewayTest is DSTestPlus, ERC1155TokenReceiver { ); // should revert, called by messenger, xDomainMessageSender not set - hevm.expectRevert("only call by conterpart"); + hevm.expectRevert("only call by counterpart"); messenger.callTarget( address(gateway), abi.encodeWithSelector( @@ -295,7 +295,7 @@ contract L2ERC1155GatewayTest is DSTestPlus, ERC1155TokenReceiver { // should revert, called by messenger, xDomainMessageSender set wrong messenger.setXDomainMessageSender(address(2)); - hevm.expectRevert("only call by conterpart"); + hevm.expectRevert("only call by counterpart"); messenger.callTarget( address(gateway), abi.encodeWithSelector( diff --git a/contracts/src/test/L2ERC721Gateway.t.sol b/contracts/src/test/L2ERC721Gateway.t.sol index 166a20557..b90fbaf19 100644 --- a/contracts/src/test/L2ERC721Gateway.t.sol +++ b/contracts/src/test/L2ERC721Gateway.t.sol @@ -185,7 +185,7 @@ contract L2ERC721GatewayTest is DSTestPlus { gateway.finalizeDepositERC721(address(0), address(0), address(0), address(0), 0); // should revert, called by messenger, xDomainMessageSender not set - hevm.expectRevert("only call by conterpart"); + hevm.expectRevert("only call by counterpart"); messenger.callTarget( address(gateway), abi.encodeWithSelector( @@ -200,7 +200,7 @@ contract L2ERC721GatewayTest is DSTestPlus { // should revert, called by messenger, xDomainMessageSender set wrong messenger.setXDomainMessageSender(address(2)); - hevm.expectRevert("only call by conterpart"); + hevm.expectRevert("only call by counterpart"); messenger.callTarget( address(gateway), abi.encodeWithSelector( @@ -250,7 +250,7 @@ contract L2ERC721GatewayTest is DSTestPlus { gateway.finalizeBatchDepositERC721(address(0), address(0), address(0), address(0), new uint256[](0)); // should revert, called by messenger, xDomainMessageSender not set - hevm.expectRevert("only call by conterpart"); + hevm.expectRevert("only call by counterpart"); messenger.callTarget( address(gateway), abi.encodeWithSelector( @@ -265,7 +265,7 @@ contract L2ERC721GatewayTest is DSTestPlus { // should revert, called by messenger, xDomainMessageSender set wrong messenger.setXDomainMessageSender(address(2)); - hevm.expectRevert("only call by conterpart"); + hevm.expectRevert("only call by counterpart"); messenger.callTarget( address(gateway), abi.encodeWithSelector( diff --git a/contracts/src/test/L2ETHGateway.t.sol b/contracts/src/test/L2ETHGateway.t.sol index 10e8c0aaf..4bf6d6df5 100644 --- a/contracts/src/test/L2ETHGateway.t.sol +++ b/contracts/src/test/L2ETHGateway.t.sol @@ -116,8 +116,8 @@ contract L2ETHGatewayTest is L2GatewayTestBase { gateway = new L2ETHGateway(); gateway.initialize(address(counterpartGateway), address(router), address(mockMessenger)); - // only call by conterpart - hevm.expectRevert("only call by conterpart"); + // only call by counterpart + hevm.expectRevert("only call by counterpart"); mockMessenger.callTarget( address(gateway), abi.encodeWithSelector(gateway.finalizeDepositETH.selector, sender, recipient, amount, dataToCall) @@ -168,7 +168,7 @@ contract L2ETHGatewayTest is L2GatewayTestBase { message ); - // conterpart is not L1ETHGateway + // counterpart is not L1ETHGateway // emit FailedRelayedMessage from L2ScrollMessenger hevm.expectEmit(true, false, false, true); emit FailedRelayedMessage(keccak256(xDomainCalldata)); @@ -251,7 +251,7 @@ contract L2ETHGatewayTest is L2GatewayTestBase { ) private { amount = bound(amount, 0, address(this).balance / 2); gasLimit = bound(gasLimit, 21000, 1000000); - feePerGas = bound(feePerGas, 0, 1000); + feePerGas = 0; setL1BaseFee(feePerGas); @@ -300,9 +300,9 @@ contract L2ETHGatewayTest is L2GatewayTestBase { uint256 feeVaultBalance = address(feeVault).balance; assertBoolEq(false, l2Messenger.isL2MessageSent(keccak256(xDomainCalldata))); if (useRouter) { - router.withdrawETH{value: amount + feeToPay + extraValue}(amount, gasLimit); + router.withdrawETH{value: amount + feeToPay}(amount, gasLimit); } else { - gateway.withdrawETH{value: amount + feeToPay + extraValue}(amount, gasLimit); + gateway.withdrawETH{value: amount + feeToPay}(amount, gasLimit); } assertEq(amount + messengerBalance, address(l2Messenger).balance); assertEq(feeToPay + feeVaultBalance, address(feeVault).balance); @@ -319,7 +319,7 @@ contract L2ETHGatewayTest is L2GatewayTestBase { ) private { amount = bound(amount, 0, address(this).balance / 2); gasLimit = bound(gasLimit, 21000, 1000000); - feePerGas = bound(feePerGas, 0, 1000); + feePerGas = 0; setL1BaseFee(feePerGas); @@ -368,9 +368,9 @@ contract L2ETHGatewayTest is L2GatewayTestBase { uint256 feeVaultBalance = address(feeVault).balance; assertBoolEq(false, l2Messenger.isL2MessageSent(keccak256(xDomainCalldata))); if (useRouter) { - router.withdrawETH{value: amount + feeToPay + extraValue}(recipient, amount, gasLimit); + router.withdrawETH{value: amount + feeToPay}(recipient, amount, gasLimit); } else { - gateway.withdrawETH{value: amount + feeToPay + extraValue}(recipient, amount, gasLimit); + gateway.withdrawETH{value: amount + feeToPay}(recipient, amount, gasLimit); } assertEq(amount + messengerBalance, address(l2Messenger).balance); assertEq(feeToPay + feeVaultBalance, address(feeVault).balance); @@ -388,7 +388,7 @@ contract L2ETHGatewayTest is L2GatewayTestBase { ) private { amount = bound(amount, 0, address(this).balance / 2); gasLimit = bound(gasLimit, 21000, 1000000); - feePerGas = bound(feePerGas, 0, 1000); + feePerGas = 0; setL1BaseFee(feePerGas); @@ -437,19 +437,9 @@ contract L2ETHGatewayTest is L2GatewayTestBase { uint256 feeVaultBalance = address(feeVault).balance; assertBoolEq(false, l2Messenger.isL2MessageSent(keccak256(xDomainCalldata))); if (useRouter) { - router.withdrawETHAndCall{value: amount + feeToPay + extraValue}( - recipient, - amount, - dataToCall, - gasLimit - ); + router.withdrawETHAndCall{value: amount + feeToPay}(recipient, amount, dataToCall, gasLimit); } else { - gateway.withdrawETHAndCall{value: amount + feeToPay + extraValue}( - recipient, - amount, - dataToCall, - gasLimit - ); + gateway.withdrawETHAndCall{value: amount + feeToPay}(recipient, amount, dataToCall, gasLimit); } assertEq(amount + messengerBalance, address(l2Messenger).balance); assertEq(feeToPay + feeVaultBalance, address(feeVault).balance); diff --git a/contracts/src/test/L2ScrollMessenger.t.sol b/contracts/src/test/L2ScrollMessenger.t.sol index 867fbf979..91b74b617 100644 --- a/contracts/src/test/L2ScrollMessenger.t.sol +++ b/contracts/src/test/L2ScrollMessenger.t.sol @@ -57,20 +57,18 @@ contract L2ScrollMessengerTest is DSTestPlus { hevm.stopPrank(); } - function testSendMessage(uint256 exceedValue, address refundAddress) external { + function testSendMessage(address refundAddress) external { hevm.assume(refundAddress.code.length == 0); hevm.assume(uint256(uint160(refundAddress)) > 100); // ignore some precompile contracts hevm.assume(refundAddress != address(this)); - exceedValue = bound(exceedValue, 1, address(this).balance / 2); - // Insufficient msg.value - hevm.expectRevert("Insufficient msg.value"); + hevm.expectRevert("msg.value mismatch"); l2Messenger.sendMessage(address(0), 1, new bytes(0), 21000, refundAddress); - // refund exceed fee + // succeed normally uint256 balanceBefore = refundAddress.balance; - l2Messenger.sendMessage{value: 1 + exceedValue}(address(0), 1, new bytes(0), 21000, refundAddress); - assertEq(balanceBefore + exceedValue, refundAddress.balance); + l2Messenger.sendMessage{value: 1}(address(0), 1, new bytes(0), 21000, refundAddress); + assertEq(balanceBefore, refundAddress.balance); } } diff --git a/contracts/src/test/L2StandardERC20Gateway.t.sol b/contracts/src/test/L2StandardERC20Gateway.t.sol index a176bbd41..a96588536 100644 --- a/contracts/src/test/L2StandardERC20Gateway.t.sol +++ b/contracts/src/test/L2StandardERC20Gateway.t.sol @@ -174,8 +174,8 @@ contract L2StandardERC20GatewayTest is L2GatewayTestBase { gateway = new L2StandardERC20Gateway(); gateway.initialize(address(counterpartGateway), address(router), address(mockMessenger), address(factory)); - // only call by conterpart - hevm.expectRevert("only call by conterpart"); + // only call by counterpart + hevm.expectRevert("only call by counterpart"); mockMessenger.callTarget( address(gateway), abi.encodeWithSelector( @@ -252,7 +252,7 @@ contract L2StandardERC20GatewayTest is L2GatewayTestBase { message ); - // conterpart is not L2WETHGateway + // counterpart is not L2WETHGateway // emit FailedRelayedMessage from L1ScrollMessenger hevm.expectEmit(true, false, false, true); emit FailedRelayedMessage(keccak256(xDomainCalldata)); @@ -334,7 +334,7 @@ contract L2StandardERC20GatewayTest is L2GatewayTestBase { ) private { amount = bound(amount, 0, l2Token.balanceOf(address(this))); gasLimit = bound(gasLimit, 21000, 1000000); - feePerGas = bound(feePerGas, 0, 1000); + feePerGas = 0; setL1BaseFee(feePerGas); @@ -360,16 +360,16 @@ contract L2StandardERC20GatewayTest is L2GatewayTestBase { if (amount == 0) { hevm.expectRevert("withdraw zero amount"); if (useRouter) { - router.withdrawERC20{value: feeToPay + extraValue}(address(l2Token), amount, gasLimit); + router.withdrawERC20{value: feeToPay}(address(l2Token), amount, gasLimit); } else { - gateway.withdrawERC20{value: feeToPay + extraValue}(address(l2Token), amount, gasLimit); + gateway.withdrawERC20{value: feeToPay}(address(l2Token), amount, gasLimit); } } else { hevm.expectRevert("no corresponding l1 token"); if (useRouter) { - router.withdrawERC20{value: feeToPay + extraValue}(address(l1Token), amount, gasLimit); + router.withdrawERC20{value: feeToPay}(address(l1Token), amount, gasLimit); } else { - gateway.withdrawERC20{value: feeToPay + extraValue}(address(l1Token), amount, gasLimit); + gateway.withdrawERC20{value: feeToPay}(address(l1Token), amount, gasLimit); } // emit AppendMessage from L2MessageQueue @@ -392,9 +392,9 @@ contract L2StandardERC20GatewayTest is L2GatewayTestBase { uint256 feeVaultBalance = address(feeVault).balance; assertBoolEq(false, l2Messenger.isL2MessageSent(keccak256(xDomainCalldata))); if (useRouter) { - router.withdrawERC20{value: feeToPay + extraValue}(address(l2Token), amount, gasLimit); + router.withdrawERC20{value: feeToPay}(address(l2Token), amount, gasLimit); } else { - gateway.withdrawERC20{value: feeToPay + extraValue}(address(l2Token), amount, gasLimit); + gateway.withdrawERC20{value: feeToPay}(address(l2Token), amount, gasLimit); } assertEq(gatewayBalance, l2Token.balanceOf(address(gateway))); assertEq(feeToPay + feeVaultBalance, address(feeVault).balance); @@ -411,7 +411,7 @@ contract L2StandardERC20GatewayTest is L2GatewayTestBase { ) private { amount = bound(amount, 0, l2Token.balanceOf(address(this))); gasLimit = bound(gasLimit, 21000, 1000000); - feePerGas = bound(feePerGas, 0, 1000); + feePerGas = 0; setL1BaseFee(feePerGas); @@ -437,16 +437,16 @@ contract L2StandardERC20GatewayTest is L2GatewayTestBase { if (amount == 0) { hevm.expectRevert("withdraw zero amount"); if (useRouter) { - router.withdrawERC20{value: feeToPay + extraValue}(address(l2Token), recipient, amount, gasLimit); + router.withdrawERC20{value: feeToPay}(address(l2Token), recipient, amount, gasLimit); } else { - gateway.withdrawERC20{value: feeToPay + extraValue}(address(l2Token), recipient, amount, gasLimit); + gateway.withdrawERC20{value: feeToPay}(address(l2Token), recipient, amount, gasLimit); } } else { hevm.expectRevert("no corresponding l1 token"); if (useRouter) { - router.withdrawERC20{value: feeToPay + extraValue}(address(l1Token), recipient, amount, gasLimit); + router.withdrawERC20{value: feeToPay}(address(l1Token), recipient, amount, gasLimit); } else { - gateway.withdrawERC20{value: feeToPay + extraValue}(address(l1Token), recipient, amount, gasLimit); + gateway.withdrawERC20{value: feeToPay}(address(l1Token), recipient, amount, gasLimit); } // emit AppendMessage from L2MessageQueue @@ -469,9 +469,9 @@ contract L2StandardERC20GatewayTest is L2GatewayTestBase { uint256 feeVaultBalance = address(feeVault).balance; assertBoolEq(false, l2Messenger.isL2MessageSent(keccak256(xDomainCalldata))); if (useRouter) { - router.withdrawERC20{value: feeToPay + extraValue}(address(l2Token), recipient, amount, gasLimit); + router.withdrawERC20{value: feeToPay}(address(l2Token), recipient, amount, gasLimit); } else { - gateway.withdrawERC20{value: feeToPay + extraValue}(address(l2Token), recipient, amount, gasLimit); + gateway.withdrawERC20{value: feeToPay}(address(l2Token), recipient, amount, gasLimit); } assertEq(gatewayBalance, l2Token.balanceOf(address(gateway))); assertEq(feeToPay + feeVaultBalance, address(feeVault).balance); @@ -489,7 +489,7 @@ contract L2StandardERC20GatewayTest is L2GatewayTestBase { ) private { amount = bound(amount, 0, l2Token.balanceOf(address(this))); gasLimit = bound(gasLimit, 21000, 1000000); - feePerGas = bound(feePerGas, 0, 1000); + feePerGas = 0; setL1BaseFee(feePerGas); @@ -515,15 +515,9 @@ contract L2StandardERC20GatewayTest is L2GatewayTestBase { if (amount == 0) { hevm.expectRevert("withdraw zero amount"); if (useRouter) { - router.withdrawERC20AndCall{value: feeToPay + extraValue}( - address(l2Token), - recipient, - amount, - dataToCall, - gasLimit - ); + router.withdrawERC20AndCall{value: feeToPay}(address(l2Token), recipient, amount, dataToCall, gasLimit); } else { - gateway.withdrawERC20AndCall{value: feeToPay + extraValue}( + gateway.withdrawERC20AndCall{value: feeToPay}( address(l2Token), recipient, amount, @@ -534,15 +528,9 @@ contract L2StandardERC20GatewayTest is L2GatewayTestBase { } else { hevm.expectRevert("no corresponding l1 token"); if (useRouter) { - router.withdrawERC20AndCall{value: feeToPay + extraValue}( - address(l1Token), - recipient, - amount, - dataToCall, - gasLimit - ); + router.withdrawERC20AndCall{value: feeToPay}(address(l1Token), recipient, amount, dataToCall, gasLimit); } else { - gateway.withdrawERC20AndCall{value: feeToPay + extraValue}( + gateway.withdrawERC20AndCall{value: feeToPay}( address(l1Token), recipient, amount, @@ -571,15 +559,9 @@ contract L2StandardERC20GatewayTest is L2GatewayTestBase { uint256 feeVaultBalance = address(feeVault).balance; assertBoolEq(false, l2Messenger.isL2MessageSent(keccak256(xDomainCalldata))); if (useRouter) { - router.withdrawERC20AndCall{value: feeToPay + extraValue}( - address(l2Token), - recipient, - amount, - dataToCall, - gasLimit - ); + router.withdrawERC20AndCall{value: feeToPay}(address(l2Token), recipient, amount, dataToCall, gasLimit); } else { - gateway.withdrawERC20AndCall{value: feeToPay + extraValue}( + gateway.withdrawERC20AndCall{value: feeToPay}( address(l2Token), recipient, amount, diff --git a/contracts/src/test/L2WETHGateway.t.sol b/contracts/src/test/L2WETHGateway.t.sol index 8cf106041..e2fa91b54 100644 --- a/contracts/src/test/L2WETHGateway.t.sol +++ b/contracts/src/test/L2WETHGateway.t.sol @@ -155,8 +155,8 @@ contract L2WETHGatewayTest is L2GatewayTestBase { gateway = new L2WETHGateway(address(l2weth), address(l1weth)); gateway.initialize(address(counterpartGateway), address(router), address(mockMessenger)); - // only call by conterpart - hevm.expectRevert("only call by conterpart"); + // only call by counterpart + hevm.expectRevert("only call by counterpart"); mockMessenger.callTarget( address(gateway), abi.encodeWithSelector( @@ -252,7 +252,7 @@ contract L2WETHGatewayTest is L2GatewayTestBase { message ); - // conterpart is not L1WETHGateway + // counterpart is not L1WETHGateway // emit FailedRelayedMessage from L2ScrollMessenger hevm.expectEmit(true, false, false, true); emit FailedRelayedMessage(keccak256(xDomainCalldata)); @@ -337,7 +337,7 @@ contract L2WETHGatewayTest is L2GatewayTestBase { ) private { amount = bound(amount, 0, l2weth.balanceOf(address(this))); gasLimit = bound(gasLimit, 21000, 1000000); - feePerGas = bound(feePerGas, 0, 1000); + feePerGas = 0; setL1BaseFee(feePerGas); @@ -363,9 +363,9 @@ contract L2WETHGatewayTest is L2GatewayTestBase { if (amount == 0) { hevm.expectRevert("withdraw zero amount"); if (useRouter) { - router.withdrawERC20{value: feeToPay + extraValue}(address(l2weth), amount, gasLimit); + router.withdrawERC20{value: feeToPay}(address(l2weth), amount, gasLimit); } else { - gateway.withdrawERC20{value: feeToPay + extraValue}(address(l2weth), amount, gasLimit); + gateway.withdrawERC20{value: feeToPay}(address(l2weth), amount, gasLimit); } } else { // token is not l2WETH @@ -392,9 +392,9 @@ contract L2WETHGatewayTest is L2GatewayTestBase { uint256 feeVaultBalance = address(feeVault).balance; assertBoolEq(false, l2Messenger.isL2MessageSent(keccak256(xDomainCalldata))); if (useRouter) { - router.withdrawERC20{value: feeToPay + extraValue}(address(l2weth), amount, gasLimit); + router.withdrawERC20{value: feeToPay}(address(l2weth), amount, gasLimit); } else { - gateway.withdrawERC20{value: feeToPay + extraValue}(address(l2weth), amount, gasLimit); + gateway.withdrawERC20{value: feeToPay}(address(l2weth), amount, gasLimit); } assertEq(amount + messengerBalance, address(l2Messenger).balance); assertEq(feeToPay + feeVaultBalance, address(feeVault).balance); @@ -411,7 +411,7 @@ contract L2WETHGatewayTest is L2GatewayTestBase { ) private { amount = bound(amount, 0, l1weth.balanceOf(address(this))); gasLimit = bound(gasLimit, 21000, 1000000); - feePerGas = bound(feePerGas, 0, 1000); + feePerGas = 0; setL1BaseFee(feePerGas); @@ -437,9 +437,9 @@ contract L2WETHGatewayTest is L2GatewayTestBase { if (amount == 0) { hevm.expectRevert("withdraw zero amount"); if (useRouter) { - router.withdrawERC20{value: feeToPay + extraValue}(address(l2weth), recipient, amount, gasLimit); + router.withdrawERC20{value: feeToPay}(address(l2weth), recipient, amount, gasLimit); } else { - gateway.withdrawERC20{value: feeToPay + extraValue}(address(l2weth), recipient, amount, gasLimit); + gateway.withdrawERC20{value: feeToPay}(address(l2weth), recipient, amount, gasLimit); } } else { // token is not l1WETH @@ -466,9 +466,9 @@ contract L2WETHGatewayTest is L2GatewayTestBase { uint256 feeVaultBalance = address(feeVault).balance; assertBoolEq(false, l2Messenger.isL2MessageSent(keccak256(xDomainCalldata))); if (useRouter) { - router.withdrawERC20{value: feeToPay + extraValue}(address(l2weth), recipient, amount, gasLimit); + router.withdrawERC20{value: feeToPay}(address(l2weth), recipient, amount, gasLimit); } else { - gateway.withdrawERC20{value: feeToPay + extraValue}(address(l2weth), recipient, amount, gasLimit); + gateway.withdrawERC20{value: feeToPay}(address(l2weth), recipient, amount, gasLimit); } assertEq(amount + messengerBalance, address(l2Messenger).balance); assertEq(feeToPay + feeVaultBalance, address(feeVault).balance); @@ -486,7 +486,7 @@ contract L2WETHGatewayTest is L2GatewayTestBase { ) private { amount = bound(amount, 0, l1weth.balanceOf(address(this))); gasLimit = bound(gasLimit, 21000, 1000000); - feePerGas = bound(feePerGas, 0, 1000); + feePerGas = 0; setL1BaseFee(feePerGas); @@ -512,21 +512,9 @@ contract L2WETHGatewayTest is L2GatewayTestBase { if (amount == 0) { hevm.expectRevert("withdraw zero amount"); if (useRouter) { - router.withdrawERC20AndCall{value: feeToPay + extraValue}( - address(l2weth), - recipient, - amount, - dataToCall, - gasLimit - ); + router.withdrawERC20AndCall{value: feeToPay}(address(l2weth), recipient, amount, dataToCall, gasLimit); } else { - gateway.withdrawERC20AndCall{value: feeToPay + extraValue}( - address(l2weth), - recipient, - amount, - dataToCall, - gasLimit - ); + gateway.withdrawERC20AndCall{value: feeToPay}(address(l2weth), recipient, amount, dataToCall, gasLimit); } } else { // token is not l1WETH @@ -553,21 +541,9 @@ contract L2WETHGatewayTest is L2GatewayTestBase { uint256 feeVaultBalance = address(feeVault).balance; assertBoolEq(false, l2Messenger.isL2MessageSent(keccak256(xDomainCalldata))); if (useRouter) { - router.withdrawERC20AndCall{value: feeToPay + extraValue}( - address(l2weth), - recipient, - amount, - dataToCall, - gasLimit - ); + router.withdrawERC20AndCall{value: feeToPay}(address(l2weth), recipient, amount, dataToCall, gasLimit); } else { - gateway.withdrawERC20AndCall{value: feeToPay + extraValue}( - address(l2weth), - recipient, - amount, - dataToCall, - gasLimit - ); + gateway.withdrawERC20AndCall{value: feeToPay}(address(l2weth), recipient, amount, dataToCall, gasLimit); } assertEq(amount + messengerBalance, address(l2Messenger).balance); assertEq(feeToPay + feeVaultBalance, address(feeVault).balance); diff --git a/contracts/src/test/ScrollChain.t.sol b/contracts/src/test/ScrollChain.t.sol index 542e70d54..fcb0c6557 100644 --- a/contracts/src/test/ScrollChain.t.sol +++ b/contracts/src/test/ScrollChain.t.sol @@ -8,23 +8,28 @@ import {L1MessageQueue} from "../L1/rollup/L1MessageQueue.sol"; import {ScrollChain, IScrollChain} from "../L1/rollup/ScrollChain.sol"; import {MockScrollChain} from "./mocks/MockScrollChain.sol"; +import {MockRollupVerifier} from "./mocks/MockRollupVerifier.sol"; contract ScrollChainTest is DSTestPlus { // from ScrollChain event UpdateSequencer(address indexed account, bool status); + event UpdateVerifier(address oldVerifier, address newVerifier); + event CommitBatch(bytes32 indexed batchHash); - event RevertBatch(bytes32 indexed batchHash); - event FinalizeBatch(bytes32 indexed batchHash); + event FinalizeBatch(bytes32 indexed batchHash, bytes32 stateRoot, bytes32 withdrawRoot); ScrollChain private rollup; L1MessageQueue internal messageQueue; MockScrollChain internal chain; + MockRollupVerifier internal verifier; function setUp() public { messageQueue = new L1MessageQueue(); - rollup = new ScrollChain(233, 4, 0xb5baa665b2664c3bfed7eb46e00ebc110ecf2ebd257854a9bf2b9dbc9b2c08f6); + rollup = new ScrollChain(233); + verifier = new MockRollupVerifier(); - rollup.initialize(address(messageQueue)); + rollup.initialize(address(messageQueue), address(verifier), 100); + messageQueue.initialize(address(this), address(rollup), address(0), address(0), 1000000); chain = new MockScrollChain(); } @@ -34,9 +39,10 @@ contract ScrollChainTest is DSTestPlus { assertEq(rollup.layer2ChainId(), 233); hevm.expectRevert("Initializable: contract is already initialized"); - rollup.initialize(address(messageQueue)); + rollup.initialize(address(messageQueue), address(0), 100); } + /* function testPublicInputHash() public { IScrollChain.Batch memory batch; batch.prevStateRoot = bytes32(0x000000000000000000000000000000000000000000000000000000000000cafe); @@ -63,6 +69,386 @@ contract ScrollChainTest is DSTestPlus { (hash, , , ) = chain.computePublicInputHash(0, batch); assertEq(hash, bytes32(0x398cb22bbfa1665c1b342b813267538a4c933d7f92d8bd9184aba0dd1122987b)); } + */ + + function testCommitBatch() public { + bytes memory batchHeader0 = new bytes(89); + + // import genesis batch first + assembly { + mstore(add(batchHeader0, add(0x20, 25)), 1) + } + rollup.importGenesisBatch(batchHeader0, bytes32(uint256(1)), bytes32(uint256(0))); + + // caller not sequencer, revert + hevm.expectRevert("caller not sequencer"); + rollup.commitBatch(0, batchHeader0, new bytes[](0), new bytes(0)); + + rollup.updateSequencer(address(this), true); + + // invalid version, revert + hevm.expectRevert("invalid version"); + rollup.commitBatch(1, batchHeader0, new bytes[](0), new bytes(0)); + + // batch is empty, revert + hevm.expectRevert("batch is empty"); + rollup.commitBatch(0, batchHeader0, new bytes[](0), new bytes(0)); + + // batch header length too small, revert + hevm.expectRevert("batch header length too small"); + rollup.commitBatch(0, new bytes(88), new bytes[](1), new bytes(0)); + + // wrong bitmap length, revert + hevm.expectRevert("wrong bitmap length"); + rollup.commitBatch(0, new bytes(90), new bytes[](1), new bytes(0)); + + // incorrect parent batch hash, revert + assembly { + mstore(add(batchHeader0, add(0x20, 25)), 2) // change data hash for batch0 + } + hevm.expectRevert("incorrect parent batch hash"); + rollup.commitBatch(0, batchHeader0, new bytes[](1), new bytes(0)); + assembly { + mstore(add(batchHeader0, add(0x20, 25)), 1) // change back + } + + bytes[] memory chunks = new bytes[](1); + bytes memory chunk0; + + // no block in chunk, revert + chunk0 = new bytes(1); + chunks[0] = chunk0; + hevm.expectRevert("no block in chunk"); + rollup.commitBatch(0, batchHeader0, chunks, new bytes(0)); + + // invalid chunk length, revert + chunk0 = new bytes(1); + chunk0[0] = bytes1(uint8(1)); // one block in this chunk + chunks[0] = chunk0; + hevm.expectRevert("invalid chunk length"); + rollup.commitBatch(0, batchHeader0, chunks, new bytes(0)); + + // incomplete l2 transaction data, revert + chunk0 = new bytes(1 + 60 + 1); + chunk0[0] = bytes1(uint8(1)); // one block in this chunk + chunks[0] = chunk0; + hevm.expectRevert("incomplete l2 transaction data"); + rollup.commitBatch(0, batchHeader0, chunks, new bytes(0)); + + // commit batch with one chunk, no tx, correctly + chunk0 = new bytes(1 + 60); + chunk0[0] = bytes1(uint8(1)); // one block in this chunk + chunks[0] = chunk0; + rollup.commitBatch(0, batchHeader0, chunks, new bytes(0)); + assertGt(uint256(rollup.committedBatches(1)), 0); + + // batch is already committed, revert + hevm.expectRevert("batch already committed"); + rollup.commitBatch(0, batchHeader0, chunks, new bytes(0)); + } + + function testFinalizeBatchWithProof() public { + // caller not sequencer, revert + hevm.expectRevert("caller not sequencer"); + rollup.finalizeBatchWithProof(new bytes(0), bytes32(0), bytes32(0), bytes32(0), new bytes(0)); + + rollup.updateSequencer(address(this), true); + + bytes memory batchHeader0 = new bytes(89); + + // import genesis batch + assembly { + mstore(add(batchHeader0, add(0x20, 25)), 1) + } + rollup.importGenesisBatch(batchHeader0, bytes32(uint256(1)), bytes32(uint256(0))); + bytes32 batchHash0 = rollup.committedBatches(0); + + bytes[] memory chunks = new bytes[](1); + bytes memory chunk0; + + // commit one batch + chunk0 = new bytes(1 + 60); + chunk0[0] = bytes1(uint8(1)); // one block in this chunk + chunks[0] = chunk0; + rollup.commitBatch(0, batchHeader0, chunks, new bytes(0)); + assertGt(uint256(rollup.committedBatches(1)), 0); + + bytes memory batchHeader1 = new bytes(89); + assembly { + mstore(add(batchHeader1, 0x20), 0) // version + mstore(add(batchHeader1, add(0x20, 1)), shl(192, 1)) // batchIndex + mstore(add(batchHeader1, add(0x20, 9)), 0) // l1MessagePopped + mstore(add(batchHeader1, add(0x20, 17)), 0) // totalL1MessagePopped + mstore(add(batchHeader1, add(0x20, 25)), 0x246394445f4fe64ed5598554d55d1682d6fb3fe04bf58eb54ef81d1189fafb51) // dataHash + mstore(add(batchHeader1, add(0x20, 57)), batchHash0) // parentBatchHash + } + + // incorrect batch hash, revert + hevm.expectRevert("incorrect batch hash"); + batchHeader1[0] = bytes1(uint8(1)); // change version to 1 + rollup.finalizeBatchWithProof(batchHeader1, bytes32(uint256(1)), bytes32(uint256(2)), bytes32(0), new bytes(0)); + batchHeader1[0] = bytes1(uint8(0)); // change back + + // batch header length too small, revert + hevm.expectRevert("batch header length too small"); + rollup.finalizeBatchWithProof( + new bytes(88), + bytes32(uint256(1)), + bytes32(uint256(2)), + bytes32(0), + new bytes(0) + ); + + // wrong bitmap length, revert + hevm.expectRevert("wrong bitmap length"); + rollup.finalizeBatchWithProof( + new bytes(90), + bytes32(uint256(1)), + bytes32(uint256(2)), + bytes32(0), + new bytes(0) + ); + + // incorrect previous state root, revert + hevm.expectRevert("incorrect previous state root"); + rollup.finalizeBatchWithProof(batchHeader1, bytes32(uint256(2)), bytes32(uint256(2)), bytes32(0), new bytes(0)); + + // verify success + assertBoolEq(rollup.isBatchFinalized(1), false); + rollup.finalizeBatchWithProof( + batchHeader1, + bytes32(uint256(1)), + bytes32(uint256(2)), + bytes32(uint256(3)), + new bytes(0) + ); + assertBoolEq(rollup.isBatchFinalized(1), true); + assertEq(rollup.finalizedStateRoots(1), bytes32(uint256(2))); + assertEq(rollup.withdrawRoots(1), bytes32(uint256(3))); + assertEq(rollup.lastFinalizedBatchIndex(), 1); + + // batch already verified, revert + hevm.expectRevert("batch already verified"); + rollup.finalizeBatchWithProof( + batchHeader1, + bytes32(uint256(1)), + bytes32(uint256(2)), + bytes32(uint256(3)), + new bytes(0) + ); + } + + function testCommitAndFinalizeWithL1Messages() public { + rollup.updateSequencer(address(this), true); + + // import 300 L1 messages + for (uint256 i = 0; i < 300; i++) { + messageQueue.appendCrossDomainMessage(address(this), 1000000, new bytes(0)); + } + + // import genesis batch first + bytes memory batchHeader0 = new bytes(89); + assembly { + mstore(add(batchHeader0, add(0x20, 25)), 1) + } + rollup.importGenesisBatch(batchHeader0, bytes32(uint256(1)), bytes32(uint256(0))); + bytes32 batchHash0 = rollup.committedBatches(0); + + bytes memory bitmap; + bytes[] memory chunks; + bytes memory chunk0; + bytes memory chunk1; + + // commit batch1, one chunk with one block, 1 tx, 1 L1 message, no skip + bytes memory batchHeader1 = new bytes(89 + 32); + assembly { + mstore(add(batchHeader1, 0x20), 0) // version + mstore(add(batchHeader1, add(0x20, 1)), shl(192, 1)) // batchIndex = 1 + mstore(add(batchHeader1, add(0x20, 9)), shl(192, 1)) // l1MessagePopped = 1 + mstore(add(batchHeader1, add(0x20, 17)), shl(192, 1)) // totalL1MessagePopped = 1 + mstore(add(batchHeader1, add(0x20, 25)), 0xfe21c37fa013c76f86b8593ddf15d685a04b55061d06797597ee866f6ff2edf8) // dataHash + mstore(add(batchHeader1, add(0x20, 57)), batchHash0) // parentBatchHash + mstore(add(batchHeader1, add(0x20, 89)), 0) // bitmap0 + } + chunk0 = new bytes(1 + 60); + assembly { + mstore(add(chunk0, 0x20), shl(248, 1)) // numBlocks = 1 + mstore(add(chunk0, add(0x21, 56)), shl(240, 1)) // numTransactions = 1 + mstore(add(chunk0, add(0x21, 58)), shl(240, 1)) // numL1Messages = 1 + } + chunks = new bytes[](1); + chunks[0] = chunk0; + bitmap = new bytes(32); + rollup.commitBatch(0, batchHeader0, chunks, bitmap); + assertGt(uint256(rollup.committedBatches(1)), 0); + assertBoolEq(rollup.isBatchFinalized(1), false); + bytes32 batchHash1 = rollup.committedBatches(1); + + // finalize batch1 + rollup.finalizeBatchWithProof( + batchHeader1, + bytes32(uint256(1)), + bytes32(uint256(2)), + bytes32(uint256(3)), + new bytes(0) + ); + assertBoolEq(rollup.isBatchFinalized(1), true); + assertEq(rollup.finalizedStateRoots(1), bytes32(uint256(2))); + assertEq(rollup.withdrawRoots(1), bytes32(uint256(3))); + assertEq(rollup.lastFinalizedBatchIndex(), 1); + assertEq(messageQueue.getCrossDomainMessage(0), bytes32(0)); + assertEq(messageQueue.pendingQueueIndex(), 1); + + // commit batch2 with two chunks, correctly + // 1. chunk0 has one block, 3 tx, no L1 messages + // 2. chunk1 has three blocks + // 2.1 block0 has 5 tx, 3 L1 messages, no skips + // 2.2 block1 has 10 tx, 5 L1 messages, even is skipped. + // 2.2 block1 has 300 tx, 256 L1 messages, odd position is skipped. + bytes memory batchHeader2 = new bytes(89 + 32 + 32); + assembly { + mstore(add(batchHeader2, 0x20), 0) // version + mstore(add(batchHeader2, add(0x20, 1)), shl(192, 2)) // batchIndex = 2 + mstore(add(batchHeader2, add(0x20, 9)), shl(192, 264)) // l1MessagePopped = 264 + mstore(add(batchHeader2, add(0x20, 17)), shl(192, 265)) // totalL1MessagePopped = 265 + mstore(add(batchHeader2, add(0x20, 25)), 0xa447b3c80bc6c3ee1aebc1af746c7c6b35a05c4a7af89d2103e9e0788b54375c) // dataHash + mstore(add(batchHeader2, add(0x20, 57)), batchHash1) // parentBatchHash + mstore( + add(batchHeader2, add(0x20, 89)), + 77194726158210796949047323339125271902179989777093709359638389338608753093288 + ) // bitmap0 + mstore(add(batchHeader2, add(0x20, 121)), 170) // bitmap1 + } + chunk0 = new bytes(1 + 60 + 3 * 5); + assembly { + mstore(add(chunk0, 0x20), shl(248, 1)) // numBlocks = 1 + mstore(add(chunk0, add(0x21, 56)), shl(240, 3)) // numTransactions = 3 + mstore(add(chunk0, add(0x21, 58)), shl(240, 0)) // numL1Messages = 0 + } + for (uint256 i = 0; i < 3; i++) { + assembly { + mstore(add(chunk0, add(93, mul(i, 5))), shl(224, 1)) // tx = "0x00" + } + } + chunk1 = new bytes(1 + 60 * 3 + 51 * 5); + assembly { + mstore(add(chunk1, 0x20), shl(248, 3)) // numBlocks = 3 + mstore(add(chunk1, add(33, 56)), shl(240, 5)) // block0.numTransactions = 5 + mstore(add(chunk1, add(33, 58)), shl(240, 3)) // block0.numL1Messages = 3 + mstore(add(chunk1, add(93, 56)), shl(240, 10)) // block1.numTransactions = 10 + mstore(add(chunk1, add(93, 58)), shl(240, 5)) // block1.numL1Messages = 5 + mstore(add(chunk1, add(153, 56)), shl(240, 300)) // block1.numTransactions = 300 + mstore(add(chunk1, add(153, 58)), shl(240, 256)) // block1.numL1Messages = 256 + } + for (uint256 i = 0; i < 51; i++) { + assembly { + mstore(add(chunk1, add(213, mul(i, 5))), shl(224, 1)) // tx = "0x00" + } + } + chunks = new bytes[](2); + chunks[0] = chunk0; + chunks[1] = chunk1; + bitmap = new bytes(64); + assembly { + mstore( + add(bitmap, add(0x20, 0)), + 77194726158210796949047323339125271902179989777093709359638389338608753093288 + ) // bitmap0 + mstore(add(bitmap, add(0x20, 32)), 170) // bitmap1 + } + + rollup.commitBatch(0, batchHeader1, chunks, bitmap); + assertGt(uint256(rollup.committedBatches(2)), 0); + assertBoolEq(rollup.isBatchFinalized(2), false); + bytes32 batchHash2 = rollup.committedBatches(2); + + // verify committed batch correctly + rollup.finalizeBatchWithProof( + batchHeader2, + bytes32(uint256(2)), + bytes32(uint256(4)), + bytes32(uint256(5)), + new bytes(0) + ); + assertBoolEq(rollup.isBatchFinalized(2), true); + assertEq(rollup.finalizedStateRoots(2), bytes32(uint256(4))); + assertEq(rollup.withdrawRoots(2), bytes32(uint256(5))); + assertEq(rollup.lastFinalizedBatchIndex(), 2); + assertEq(messageQueue.pendingQueueIndex(), 265); + // 1 ~ 4, zero + for (uint256 i = 1; i < 4; i++) { + assertEq(messageQueue.getCrossDomainMessage(i), bytes32(0)); + } + // 4 ~ 9, even is nonzero, odd is zero + for (uint256 i = 4; i < 9; i++) { + if (i % 2 == 1) { + assertEq(messageQueue.getCrossDomainMessage(i), bytes32(0)); + } else { + assertGt(uint256(messageQueue.getCrossDomainMessage(i)), 0); + } + } + // 9 ~ 265, even is nonzero, odd is zero + for (uint256 i = 9; i < 265; i++) { + if (i % 2 == 1) { + assertEq(messageQueue.getCrossDomainMessage(i), bytes32(0)); + } else { + assertGt(uint256(messageQueue.getCrossDomainMessage(i)), 0); + } + } + } + + function testRevertBatch() public { + // caller not owner, revert + hevm.startPrank(address(1)); + hevm.expectRevert("Ownable: caller is not the owner"); + rollup.revertBatch(new bytes(89)); + hevm.stopPrank(); + + rollup.updateSequencer(address(this), true); + + bytes memory batchHeader0 = new bytes(89); + + // import genesis batch + assembly { + mstore(add(batchHeader0, add(0x20, 25)), 1) + } + rollup.importGenesisBatch(batchHeader0, bytes32(uint256(1)), bytes32(uint256(0))); + bytes32 batchHash0 = rollup.committedBatches(0); + + bytes[] memory chunks = new bytes[](1); + bytes memory chunk0; + + // commit one batch + chunk0 = new bytes(1 + 60); + chunk0[0] = bytes1(uint8(1)); // one block in this chunk + chunks[0] = chunk0; + rollup.commitBatch(0, batchHeader0, chunks, new bytes(0)); + assertGt(uint256(rollup.committedBatches(1)), 0); + + bytes memory batchHeader1 = new bytes(89); + assembly { + mstore(add(batchHeader1, 0x20), 0) // version + mstore(add(batchHeader1, add(0x20, 1)), shl(192, 1)) // batchIndex + mstore(add(batchHeader1, add(0x20, 9)), 0) // l1MessagePopped + mstore(add(batchHeader1, add(0x20, 17)), 0) // totalL1MessagePopped + mstore(add(batchHeader1, add(0x20, 25)), 0x246394445f4fe64ed5598554d55d1682d6fb3fe04bf58eb54ef81d1189fafb51) // dataHash + mstore(add(batchHeader1, add(0x20, 57)), batchHash0) // parentBatchHash + } + + // incorrect batch hash, revert + hevm.expectRevert("incorrect batch hash"); + batchHeader1[0] = bytes1(uint8(1)); // change version to 1 + rollup.revertBatch(batchHeader1); + batchHeader1[0] = bytes1(uint8(0)); // change back + + // can only revert unfinalized batch, revert + hevm.expectRevert("can only revert unfinalized batch"); + rollup.revertBatch(batchHeader0); + + // succeed + rollup.revertBatch(batchHeader1); + assertEq(uint256(rollup.committedBatches(1)), 0); + } function testUpdateSequencer(address _sequencer) public { // set by non-owner, should revert @@ -85,86 +471,88 @@ contract ScrollChainTest is DSTestPlus { assertBoolEq(rollup.isSequencer(_sequencer), false); } - function testImportGenesisBlock(IScrollChain.BlockContext memory _genesisBlock) public { - hevm.assume(_genesisBlock.blockHash != bytes32(0)); - _genesisBlock.blockNumber = 0; - _genesisBlock.parentHash = bytes32(0); - _genesisBlock.numTransactions = 0; - _genesisBlock.numL1Messages = 0; + function testUpdateVerifier(address _newVerifier) public { + // set by non-owner, should revert + hevm.startPrank(address(1)); + hevm.expectRevert("Ownable: caller is not the owner"); + rollup.updateVerifier(_newVerifier); + hevm.stopPrank(); - IScrollChain.Batch memory _genesisBatch; - _genesisBatch.blocks = new IScrollChain.BlockContext[](1); - _genesisBatch.blocks[0] = _genesisBlock; - _genesisBatch.newStateRoot = bytes32(uint256(2)); + // change to random operator + hevm.expectEmit(false, false, false, true); + emit UpdateVerifier(address(verifier), _newVerifier); - // Not exact one block in genesis, should revert - _genesisBatch.blocks = new IScrollChain.BlockContext[](2); - hevm.expectRevert("Not exact one block in genesis"); - rollup.importGenesisBatch(_genesisBatch); - _genesisBatch.blocks = new IScrollChain.BlockContext[](0); - hevm.expectRevert("Not exact one block in genesis"); - rollup.importGenesisBatch(_genesisBatch); + assertEq(rollup.verifier(), address(verifier)); + rollup.updateVerifier(_newVerifier); + assertEq(rollup.verifier(), _newVerifier); + } - _genesisBatch.blocks = new IScrollChain.BlockContext[](1); - _genesisBatch.blocks[0] = _genesisBlock; + function testImportGenesisBlock() public { + bytes memory batchHeader; - // Nonzero prevStateRoot, should revert - _genesisBatch.prevStateRoot = bytes32(uint256(1)); - hevm.expectRevert("Nonzero prevStateRoot"); - rollup.importGenesisBatch(_genesisBatch); - _genesisBatch.prevStateRoot = bytes32(0); + // zero state root, revert + batchHeader = new bytes(89); + hevm.expectRevert("zero state root"); + rollup.importGenesisBatch(batchHeader, bytes32(0), bytes32(0)); - // Block hash is zero, should revert - bytes32 _originalHash = _genesisBatch.blocks[0].blockHash; - _genesisBatch.blocks[0].blockHash = bytes32(0); - hevm.expectRevert("Block hash is zero"); - rollup.importGenesisBatch(_genesisBatch); - _genesisBatch.blocks[0].blockHash = _originalHash; + // batch header length too small, revert + batchHeader = new bytes(88); + hevm.expectRevert("batch header length too small"); + rollup.importGenesisBatch(batchHeader, bytes32(uint256(1)), bytes32(0)); - // Block is not genesis, should revert - _genesisBatch.blocks[0].blockNumber = 1; - hevm.expectRevert("Block is not genesis"); - rollup.importGenesisBatch(_genesisBatch); - _genesisBatch.blocks[0].blockNumber = 0; + // wrong bitmap length, revert + batchHeader = new bytes(90); + hevm.expectRevert("wrong bitmap length"); + rollup.importGenesisBatch(batchHeader, bytes32(uint256(1)), bytes32(0)); - // Parent hash not empty, should revert - _genesisBatch.blocks[0].parentHash = bytes32(uint256(2)); - hevm.expectRevert("Parent hash not empty"); - rollup.importGenesisBatch(_genesisBatch); - _genesisBatch.blocks[0].parentHash = bytes32(0); + // not all fields are zero, revert + batchHeader = new bytes(89); + batchHeader[0] = bytes1(uint8(1)); // version not zero + hevm.expectRevert("not all fields are zero"); + rollup.importGenesisBatch(batchHeader, bytes32(uint256(1)), bytes32(0)); + + batchHeader = new bytes(89); + batchHeader[1] = bytes1(uint8(1)); // batchIndex not zero + hevm.expectRevert("not all fields are zero"); + rollup.importGenesisBatch(batchHeader, bytes32(uint256(1)), bytes32(0)); + + batchHeader = new bytes(89 + 32); + assembly { + mstore(add(batchHeader, add(0x20, 9)), shl(192, 1)) // l1MessagePopped not zero + } + hevm.expectRevert("not all fields are zero"); + rollup.importGenesisBatch(batchHeader, bytes32(uint256(1)), bytes32(0)); + + batchHeader = new bytes(89); + batchHeader[17] = bytes1(uint8(1)); // totalL1MessagePopped not zero + hevm.expectRevert("not all fields are zero"); + rollup.importGenesisBatch(batchHeader, bytes32(uint256(1)), bytes32(0)); + + // zero data hash, revert + batchHeader = new bytes(89); + hevm.expectRevert("zero data hash"); + rollup.importGenesisBatch(batchHeader, bytes32(uint256(1)), bytes32(0)); + + // nonzero parent batch hash, revert + batchHeader = new bytes(89); + batchHeader[25] = bytes1(uint8(1)); // dataHash not zero + batchHeader[57] = bytes1(uint8(1)); // parentBatchHash not zero + hevm.expectRevert("nonzero parent batch hash"); + rollup.importGenesisBatch(batchHeader, bytes32(uint256(1)), bytes32(0)); // import correctly - assertEq(rollup.finalizedBatches(0), bytes32(0)); - (bytes32 _batchHash, , , ) = chain.computePublicInputHash(0, _genesisBatch); + batchHeader = new bytes(89); + batchHeader[25] = bytes1(uint8(1)); // dataHash not zero + assertEq(rollup.finalizedStateRoots(0), bytes32(0)); + assertEq(rollup.withdrawRoots(0), bytes32(0)); + assertEq(rollup.committedBatches(0), bytes32(0)); + rollup.importGenesisBatch(batchHeader, bytes32(uint256(1)), bytes32(uint256(2))); + assertEq(rollup.finalizedStateRoots(0), bytes32(uint256(1))); + assertEq(rollup.withdrawRoots(0), bytes32(uint256(2))); + assertGt(uint256(rollup.committedBatches(0)), 0); - hevm.expectEmit(true, false, false, true); - emit CommitBatch(_batchHash); - hevm.expectEmit(true, false, false, true); - emit FinalizeBatch(_batchHash); - rollup.importGenesisBatch(_genesisBatch); - { - assertEq(rollup.finalizedBatches(0), _batchHash); - assertEq(rollup.lastFinalizedBatchHash(), _batchHash); - ( - bytes32 currStateRoot, - bytes32 withdrawTrieRoot, - bytes32 parentBatchHash, - uint64 batchIndex, - uint64 timestamp, - , - , - bool finalized - ) = rollup.batches(_batchHash); - assertEq(currStateRoot, bytes32(uint256(2))); - assertEq(withdrawTrieRoot, bytes32(0)); - assertEq(batchIndex, 0); - assertEq(parentBatchHash, 0); - assertEq(timestamp, _genesisBlock.timestamp); - assertBoolEq(finalized, true); - } - - // genesis block imported + // Genesis batch imported, revert hevm.expectRevert("Genesis batch imported"); - rollup.importGenesisBatch(_genesisBatch); + rollup.importGenesisBatch(batchHeader, bytes32(uint256(1)), bytes32(uint256(2))); } } diff --git a/contracts/src/test/WithdrawTrieVerifier.t.sol b/contracts/src/test/WithdrawTrieVerifier.t.sol index bad7c5b01..dc2605bc4 100644 --- a/contracts/src/test/WithdrawTrieVerifier.t.sol +++ b/contracts/src/test/WithdrawTrieVerifier.t.sol @@ -7,26 +7,25 @@ import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol"; import {WithdrawTrieVerifier} from "../libraries/verifier/WithdrawTrieVerifier.sol"; contract WithdrawTrieVerifierTest is DSTestPlus { - function testInvalidProof() public { hevm.expectRevert("Invalid proof"); - WithdrawTrieVerifier.verifyMerkleProof(bytes32(uint256(1)), bytes32(uint256(1)), 1, hex"00"); + WithdrawTrieVerifier.verifyMerkleProof(bytes32(uint256(1)), bytes32(uint256(1)), 1, hex"00"); } function testMerkleProof() public { bytes32[] memory roots = new bytes32[](4); bytes32[] memory hashes = new bytes32[](4); - uint[] memory nonces = new uint[](4); + uint256[] memory nonces = new uint256[](4); bytes[] memory proofs = new bytes[](4); // generated from bridge folder - // adding leaves one at a time (0x0..1, 0x0..2, 0x0..3, 0x0..4) + // adding leaves one at a time (0x0..1, 0x0..2, 0x0..3, 0x0..4) - roots[0] = hex"0000000000000000000000000000000000000000000000000000000000000001"; + roots[0] = hex"0000000000000000000000000000000000000000000000000000000000000001"; hashes[0] = hex"0000000000000000000000000000000000000000000000000000000000000001"; nonces[0] = 0; proofs[0] = hex""; - + roots[1] = hex"e90b7bceb6e7df5418fb78d8ee546e97c83a08bbccc01a0644d599ccd2a7c2e0"; hashes[1] = hex"0000000000000000000000000000000000000000000000000000000000000002"; nonces[1] = 1; @@ -35,15 +34,22 @@ contract WithdrawTrieVerifierTest is DSTestPlus { roots[2] = hex"222ff5e0b5877792c2bc1670e2ccd0c2c97cd7bb1672a57d598db05092d3d72c"; hashes[2] = hex"0000000000000000000000000000000000000000000000000000000000000003"; nonces[2] = 2; - proofs[2] = hex"0000000000000000000000000000000000000000000000000000000000000000e90b7bceb6e7df5418fb78d8ee546e97c83a08bbccc01a0644d599ccd2a7c2e0"; + proofs[ + 2 + ] = hex"0000000000000000000000000000000000000000000000000000000000000000e90b7bceb6e7df5418fb78d8ee546e97c83a08bbccc01a0644d599ccd2a7c2e0"; roots[3] = hex"a9bb8c3f1f12e9aa903a50c47f314b57610a3ab32f2d463293f58836def38d36"; hashes[3] = hex"0000000000000000000000000000000000000000000000000000000000000004"; nonces[3] = 3; - proofs[3] = hex"0000000000000000000000000000000000000000000000000000000000000003e90b7bceb6e7df5418fb78d8ee546e97c83a08bbccc01a0644d599ccd2a7c2e0"; + proofs[ + 3 + ] = hex"0000000000000000000000000000000000000000000000000000000000000003e90b7bceb6e7df5418fb78d8ee546e97c83a08bbccc01a0644d599ccd2a7c2e0"; - for(uint i = 0; i < 4; i++) { - require(WithdrawTrieVerifier.verifyMerkleProof(roots[i], hashes[i], nonces[i] , proofs[i]) == true, "WithdrawTrieVerifier: verifyMerkleProof failed"); + for (uint256 i = 0; i < 4; i++) { + require( + WithdrawTrieVerifier.verifyMerkleProof(roots[i], hashes[i], nonces[i], proofs[i]) == true, + "WithdrawTrieVerifier: verifyMerkleProof failed" + ); } } } diff --git a/contracts/src/test/mocks/MockRollupVerifier.sol b/contracts/src/test/mocks/MockRollupVerifier.sol new file mode 100644 index 000000000..3b03c3847 --- /dev/null +++ b/contracts/src/test/mocks/MockRollupVerifier.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import {IRollupVerifier} from "../../libraries/verifier/IRollupVerifier.sol"; + +contract MockRollupVerifier is IRollupVerifier { + /// @inheritdoc IRollupVerifier + function verifyAggregateProof(bytes calldata, bytes32) external view {} +} diff --git a/contracts/src/test/mocks/MockScrollChain.sol b/contracts/src/test/mocks/MockScrollChain.sol index 92466e3a6..18eea016b 100644 --- a/contracts/src/test/mocks/MockScrollChain.sol +++ b/contracts/src/test/mocks/MockScrollChain.sol @@ -5,8 +5,9 @@ pragma solidity ^0.8.0; import {ScrollChain} from "../../L1/rollup/ScrollChain.sol"; contract MockScrollChain is ScrollChain { - constructor() ScrollChain(0, 4, 0xb5baa665b2664c3bfed7eb46e00ebc110ecf2ebd257854a9bf2b9dbc9b2c08f6) {} + constructor() ScrollChain(0) {} + /* function computePublicInputHash(uint64 accTotalL1Messages, Batch memory batch) external view @@ -19,4 +20,5 @@ contract MockScrollChain is ScrollChain { { return _computePublicInputHash(accTotalL1Messages, batch); } + */ }