diff --git a/contracts/deploy/01-outbox/01-arb-to-gnosis-outbox.ts b/contracts/deploy/01-outbox/01-arb-to-gnosis-outbox.ts index 3cad1dd3..5d756481 100644 --- a/contracts/deploy/01-outbox/01-arb-to-gnosis-outbox.ts +++ b/contracts/deploy/01-outbox/01-arb-to-gnosis-outbox.ts @@ -36,17 +36,17 @@ const paramsByChainId = { sequencerLimit: 86400, // 24 hours }, HARDHAT: { - deposit: parseEther("5"), // 120 xDAI budget for timeout + deposit: parseEther("10"), // Average happy path wait time is 22.5 mins, assume no censorship epochPeriod: 600, // 10 min - challengePeriod: 600, // 10 min (assume no sequencer backdating) - numEpochTimeout: 24, // 6 hours + minChallengePeriod: 600, // 10 min (assume no sequencer backdating) + numEpochTimeout: 21600, // 6 hours claimDelay: 2, amb: ethers.constants.AddressZero, routerAddress: ethers.constants.AddressZero, maxMissingBlocks: 10000000000000, routerChainId: 31337, - sequencerLimit: 86400, // 24 hours + sequencerLimit: 864, }, }; @@ -56,8 +56,15 @@ const deployOutbox: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { providers } = ethers; // fallback to hardhat node signers on local network - const deployer = (await getNamedAccounts()).deployer ?? (await hre.ethers.getSigners())[0].address; - const chainId = Number(await getChainId()); + const [namedAccounts, signers, rawChainId] = await Promise.all([ + getNamedAccounts(), + hre.ethers.getSigners(), + getChainId(), + ]); + + const deployer = namedAccounts.deployer ?? signers[0].address; + const chainId = Number(rawChainId); + console.log("deploying to chainId %s with deployer %s", chainId, deployer); const routerNetworks = { @@ -84,27 +91,50 @@ const deployOutbox: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { // ---------------------------------------------------------------------------------------------- const hardhatDeployer = async () => { let nonce = await ethers.provider.getTransactionCount(deployer); - nonce += 4; // SenderGatewayToEthereum deploy tx will be the 5th after this, same network for both sender/receiver. - const routerAddress = getContractAddress(deployer, nonce); - console.log("calculated future router for nonce %d: %s", nonce, routerAddress); + const routerAddress = getContractAddress(deployer, nonce + 10); + console.log("calculated future router for nonce %d: %s", nonce + 10, routerAddress); + + const senderGatewayAddress = getContractAddress(deployer, nonce + 6); // with the current order of transaction ,nonce for sender gateway would be 14. + console.log("calculated future SenderGatewayToGnosis address for nonce %d: %s", nonce + 6, senderGatewayAddress); - await deploy("VeaOutboxGnosisMock", { + const ambMock = await deploy("MockAMB", { from: deployer, + args: [], + log: true, + }); + + const wethMock = await deploy("MockWETH", { + from: deployer, + args: [], + log: true, + }); + + const veaOutbox = await deploy("VeaOutboxArbToGnosis", { + from: deployer, + contract: "VeaOutboxArbToGnosis", args: [ deposit, epochPeriod, minChallengePeriod, numEpochTimeout, - amb, - ethers.constants.AddressZero, + ambMock.address, + routerAddress, sequencerLimit, maxMissingBlocks, routerChainId, - WETH, + wethMock.address, ], log: true, }); + + await deploy("ArbToGnosisReceiverGateway", { + from: deployer, + contract: "ReceiverGatewayMock", + args: [veaOutbox.address, senderGatewayAddress], + gasLimit: 4000000, + log: true, + }); }; // ---------------------------------------------------------------------------------------------- diff --git a/contracts/deploy/02-inbox/02-arb-to-gnosis-inbox.ts b/contracts/deploy/02-inbox/02-arb-to-gnosis-inbox.ts index 7bf86668..55a44bfe 100644 --- a/contracts/deploy/02-inbox/02-arb-to-gnosis-inbox.ts +++ b/contracts/deploy/02-inbox/02-arb-to-gnosis-inbox.ts @@ -1,12 +1,14 @@ import { HardhatRuntimeEnvironment } from "hardhat/types"; import { DeployFunction } from "hardhat-deploy/types"; import getContractAddress from "../../deploy-helpers/getContractAddress"; +import { ethers } from "hardhat"; enum SenderChains { ARBITRUM = 42161, ARBITRUM_SEPOLIA = 421614, HARDHAT = 31337, } + const paramsByChainId = { ARBITRUM: { epochPeriod: 3600, // 1 hours @@ -19,38 +21,69 @@ const paramsByChainId = { }, }; -// TODO: use deterministic deployments const deployInbox: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { ethers, deployments, getNamedAccounts, getChainId, config } = hre; const { deploy } = deployments; - const chainId = Number(await getChainId()); - const { providers } = ethers; - const deployer = (await getNamedAccounts()).deployer; - console.log("deployer: %s", deployer); + // fallback to hardhat node signers on local network + const [namedAccounts, signers, rawChainId] = await Promise.all([ + getNamedAccounts(), + hre.ethers.getSigners(), + getChainId(), + ]); + + const deployer = namedAccounts.deployer ?? signers[0].address; + const chainId = Number(rawChainId); + + console.log("deploying to chainId %s with deployer %s", chainId, deployer); const { epochPeriod } = paramsByChainId[SenderChains[chainId]]; - const routerNetworks = { - ARBITRUM: config.networks.mainnet, - ARBITRUM_SEPOLIA: config.networks.sepolia, - HARDHAT: config.networks.localhost, - }; + // Hack to predict the deployment address on the sender chain. + // TODO: use deterministic deployments // ---------------------------------------------------------------------------------------------- + const hardhatDeployer = async () => { + let nonce = await ethers.provider.getTransactionCount(deployer); + + const arbitrumBridgeAddress = getContractAddress(deployer, nonce + 5); - const routerChainProvider = new providers.JsonRpcProvider(routerNetworks[SenderChains[chainId]].url); - let nonceRouter = await routerChainProvider.getTransactionCount(deployer); + const arbSysMock = await deploy("ArbSysMock", { + from: deployer, + contract: "ArbSysMockWithBridge", + args: [arbitrumBridgeAddress], + log: true, + }); - const routerAddress = getContractAddress(deployer, nonceRouter); - console.log("calculated future router for nonce %d: %s", nonceRouter, routerAddress); + const routerAddress = getContractAddress(deployer, nonce + 6); + console.log("calculated future router for nonce %d: %s", nonce + 6, routerAddress); - await deploy("VeaInboxArbToGnosis" + (chainId === 42161 ? "" : "Testnet"), { - contract: "VeaInboxArbToGnosis", - from: deployer, - args: [epochPeriod, routerAddress], - log: true, - }); + const receiverGateway = await deployments.get("ArbToGnosisReceiverGateway"); + const veaInbox = await deploy("VeaInboxArbToGnosis", { + from: deployer, + contract: "VeaInboxArbToGnosisMock", + args: [epochPeriod, routerAddress, arbSysMock.address], + log: true, + }); + + await deploy("ArbToGnosisSenderGateway", { + from: deployer, + contract: "SenderGatewayMock", + args: [veaInbox.address, receiverGateway.address], + gasLimit: 4000000, + log: true, + }); + }; + + // ---------------------------------------------------------------------------------------------- + const liveDeployer = async () => {}; + + // ---------------------------------------------------------------------------------------------- + if (chainId === 31337) { + await hardhatDeployer(); + } else { + await liveDeployer(); + } }; deployInbox.tags = ["ArbToGnosisInbox"]; diff --git a/contracts/deploy/03-routers/03-arb-to-gnosis-router.ts b/contracts/deploy/03-routers/03-arb-to-gnosis-router.ts index edb573ff..b36554cc 100644 --- a/contracts/deploy/03-routers/03-arb-to-gnosis-router.ts +++ b/contracts/deploy/03-routers/03-arb-to-gnosis-router.ts @@ -27,23 +27,50 @@ const paramsByChainId = { const deployRouter: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { deployments, getNamedAccounts, getChainId } = hre; const { deploy } = deployments; - const chainId = Number(await getChainId()); // fallback to hardhat node signers on local network - const deployer = (await getNamedAccounts()).deployer ?? (await hre.ethers.getSigners())[0].address; - console.log("deployer: %s", deployer); + const [namedAccounts, signers, rawChainId] = await Promise.all([ + getNamedAccounts(), + hre.ethers.getSigners(), + getChainId(), + ]); + + const deployer = namedAccounts.deployer ?? signers[0].address; + const chainId = Number(rawChainId); + + console.log("deploying to chainId %s with deployer %s", chainId, deployer); const { arbitrumBridge, amb } = paramsByChainId[RouterChains[chainId]]; // ---------------------------------------------------------------------------------------------- const hardhatDeployer = async () => { - const veaOutbox = await deployments.get("VeaOutboxArbToGnosis"); - const veaInbox = await deployments.get("VeaInboxArbToGnosis"); + const [veaOutbox, veaInbox, amb] = await Promise.all([ + deployments.get("VeaOutboxArbToGnosis"), + deployments.get("VeaInboxArbToGnosis"), + deployments.get("MockAMB"), + ]); + + const sequencerInbox = await deploy("SequencerInboxMock", { + from: deployer, + contract: "SequencerInboxMock", + args: ["10"], + }); + const outbox = await deploy("OutboxMock", { + from: deployer, + args: [veaInbox.address], + log: true, + }); + + const arbitrumBridge = await deploy("BridgeMock", { + from: deployer, + contract: "BridgeMock", + args: [outbox.address, sequencerInbox.address], + }); const router = await deploy("RouterArbToGnosis", { from: deployer, contract: "RouterArbToGnosis", - args: [arbitrumBridge, amb, veaInbox.address, veaOutbox.address], + args: [arbitrumBridge.address, amb.address, veaInbox.address, veaOutbox.address], }); }; @@ -73,7 +100,6 @@ const deployRouter: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { deployRouter.tags = ["ArbToGnosisRouter"]; deployRouter.skip = async ({ getChainId }) => { const chainId = Number(await getChainId()); - console.log(chainId); return !RouterChains[chainId]; }; deployRouter.runAtTheEnd = true; diff --git a/contracts/src/test/ArbToGnosis/VeaInboxArbToGnosisMock.sol b/contracts/src/test/ArbToGnosis/VeaInboxArbToGnosisMock.sol new file mode 100644 index 00000000..8852bd93 --- /dev/null +++ b/contracts/src/test/ArbToGnosis/VeaInboxArbToGnosisMock.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT + +/// @custom:authors: [@madhurMongia] +/// @custom:reviewers: [] +/// @custom:auditors: [] +/// @custom:bounties: [] +/// @custom:deployments: [] + +pragma solidity 0.8.24; + +import "../../arbitrumToGnosis/VeaInboxArbToGnosis.sol"; +import "../../canonical/arbitrum/IArbSys.sol"; +import "../../interfaces/routers/IRouterToGnosis.sol"; + +contract VeaInboxArbToGnosisMock is VeaInboxArbToGnosis { + IArbSys public immutable mockArbSys; + + constructor( + uint256 _epochPeriod, + address _routerArbToGnosis, + IArbSys _mockArbSys + ) VeaInboxArbToGnosis(_epochPeriod, _routerArbToGnosis) { + mockArbSys = _mockArbSys; + } + + // Override sendSnapshot to use the mock ArbSys + function sendSnapshot(uint256 _epoch, uint256 _gasLimit, Claim memory _claim) external override { + unchecked { + require(_epoch < block.timestamp / epochPeriod, "Can only send past epoch snapshot."); + } + + bytes memory data = abi.encodeCall(IRouterToGnosis.route, (_epoch, snapshots[_epoch], _gasLimit, _claim)); + + // Use the mock ArbSys instead of the constant ARB_SYS + bytes32 ticketID = bytes32(mockArbSys.sendTxToL1(routerArbToGnosis, data)); + + emit SnapshotSent(_epoch, ticketID); + } +} diff --git a/contracts/src/test/bridge-mocks/arbitrum/ArbSysMockWithBridge.sol b/contracts/src/test/bridge-mocks/arbitrum/ArbSysMockWithBridge.sol new file mode 100644 index 00000000..b30e2e4d --- /dev/null +++ b/contracts/src/test/bridge-mocks/arbitrum/ArbSysMockWithBridge.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT + +/// @custom:authors: [@madhurMongia] +/// @custom:reviewers: [] +/// @custom:auditors: [] +/// @custom:bounties: [] +/// @custom:deployments: [] + +pragma solidity 0.8.24; + +import "../../../canonical/arbitrum/IArbSys.sol"; +import "./BridgeMock.sol"; + +contract ArbSysMockWithBridge is IArbSys { + BridgeMock public immutable bridge; + + constructor(BridgeMock _bridge) { + bridge = _bridge; + } + + function sendTxToL1( + address destination, + bytes calldata calldataForL1 + ) external payable returns (uint256 _withdrawal_ID) { + return bridge.executeL1Message(destination, calldataForL1); + } +} diff --git a/contracts/src/test/bridge-mocks/arbitrum/BridgeMock.sol b/contracts/src/test/bridge-mocks/arbitrum/BridgeMock.sol index f4dd232f..20dd14a2 100644 --- a/contracts/src/test/bridge-mocks/arbitrum/BridgeMock.sol +++ b/contracts/src/test/bridge-mocks/arbitrum/BridgeMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -/// @custom:authors: [@hrishibhat] +/// @custom:authors: [@hrishibhat,@madhurMongia] /// @custom:reviewers: [] /// @custom:auditors: [] /// @custom:bounties: [] @@ -27,4 +27,17 @@ contract BridgeMock is IBridge { if (index == 0) return sequencerInbox; return address(0); } + + function executeL1Message(address destination, bytes calldata data) external returns (uint256) { + // Simulate the bridge calling the destination contract + (bool success, bytes memory returnData) = destination.call(data); + + if (!success) { + assembly { + revert(add(returnData, 32), mload(returnData)) + } + } + + return 0; + } } diff --git a/contracts/src/test/bridge-mocks/gnosis/MockAMB.sol b/contracts/src/test/bridge-mocks/gnosis/MockAMB.sol index c80a5030..aaa41e1a 100644 --- a/contracts/src/test/bridge-mocks/gnosis/MockAMB.sol +++ b/contracts/src/test/bridge-mocks/gnosis/MockAMB.sol @@ -5,7 +5,7 @@ pragma solidity 0.8.24; import "../../../canonical/gnosis-chain/IAMB.sol"; contract MockAMB is IAMB { - event MockedEvent(bytes32 indexed messageId, bytes encodedData); + event MockedEvent(bytes32 indexed messageId, bytes encodedData, bytes _data); address public messageSender; uint256 public maxGasPerTx; @@ -34,8 +34,14 @@ contract MockAMB is IAMB { messageSender = _sender; messageId = _messageId; transactionHash = _messageId; - messageSourceChainId = bytes32(uint256(1337)); - (bool status, ) = _contract.call{gas: _gas}(_data); + messageSourceChainId = bytes32(uint256(31337)); + (bool status, bytes memory returnData) = _contract.call{gas: _gas}(_data); + + if (!status) { + assembly { + revert(add(returnData, 32), mload(returnData)) + } + } messageSender = address(0); messageId = bytes32(0); transactionHash = bytes32(0); @@ -64,7 +70,7 @@ contract MockAMB is IAMB { uint256 _dataType ) internal returns (bytes32) { require(messageId == bytes32(0)); - bytes32 bridgeId = keccak256(abi.encodePacked(uint16(1337), address(this))) & + bytes32 bridgeId = keccak256(abi.encodePacked(uint16(31337), address(this))) & 0x00000000ffffffffffffffffffffffffffffffffffffffff0000000000000000; bytes32 _messageId = bytes32(uint256(0x11223344 << 224)) | bridgeId | bytes32(uint256(nonce)); @@ -77,12 +83,12 @@ contract MockAMB is IAMB { uint8(2), uint8(2), uint8(_dataType), - uint16(1337), - uint16(1338), + uint16(31337), + uint16(31337), _data ); - emit MockedEvent(_messageId, eventData); + emit MockedEvent(_messageId, eventData, _data); return _messageId; } diff --git a/contracts/src/test/tokens/gnosis/MockWETH.sol b/contracts/src/test/tokens/gnosis/MockWETH.sol new file mode 100644 index 00000000..7f27b293 --- /dev/null +++ b/contracts/src/test/tokens/gnosis/MockWETH.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT + +/// @custom:authors: [@madhurMongia] +/// @custom:reviewers: [] +/// @custom:auditors: [] +/// @custom:bounties: [] +/// @custom:deployments: [] + +pragma solidity ^0.8.0; + +contract MockWETH { + string public name = "Wrapped Ether"; + string public symbol = "WETH"; + uint8 public decimals = 18; + + event Approval(address indexed src, address indexed guy, uint wad); + event Transfer(address indexed src, address indexed dst, uint wad); + event Deposit(address indexed dst, uint wad); + event Withdrawal(address indexed src, uint wad); + + mapping(address => uint) public balanceOf; + mapping(address => mapping(address => uint)) public allowance; + + receive() external payable { + deposit(); + } + + function deposit() public payable { + balanceOf[msg.sender] += msg.value; + emit Deposit(msg.sender, msg.value); + } + + function withdraw(uint wad) public { + require(balanceOf[msg.sender] >= wad, "WETH: insufficient balance"); + balanceOf[msg.sender] -= wad; + payable(msg.sender).transfer(wad); + emit Withdrawal(msg.sender, wad); + } + + function totalSupply() public view returns (uint) { + return address(this).balance; + } + + function approve(address guy, uint wad) public returns (bool) { + allowance[msg.sender][guy] = wad; + emit Approval(msg.sender, guy, wad); + return true; + } + + function transfer(address dst, uint wad) public returns (bool) { + return transferFrom(msg.sender, dst, wad); + } + + function transferFrom(address src, address dst, uint wad) public returns (bool) { + require(balanceOf[src] >= wad, "WETH: insufficient balance"); + + if (src != msg.sender && allowance[src][msg.sender] != type(uint).max) { + require(allowance[src][msg.sender] >= wad, "WETH: insufficient allowance"); + allowance[src][msg.sender] -= wad; + } + + balanceOf[src] -= wad; + balanceOf[dst] += wad; + + emit Transfer(src, dst, wad); + + return true; + } + + // This function is added for convenience in testing + function mintMock(address to, uint256 amount) public { + balanceOf[to] += amount; + emit Transfer(address(0), to, amount); + } + + function burn(uint wad) public { + require(balanceOf[msg.sender] >= wad, "WETH: insufficient balance"); + balanceOf[msg.sender] -= wad; + emit Transfer(msg.sender, address(0), wad); + } +} diff --git a/contracts/test/integration/ArbToGnosis.ts b/contracts/test/integration/ArbToGnosis.ts new file mode 100644 index 00000000..3eef5488 --- /dev/null +++ b/contracts/test/integration/ArbToGnosis.ts @@ -0,0 +1,944 @@ +import { expect } from "chai"; +import { deployments, ethers, network } from "hardhat"; +import { BigNumber, Contract } from "ethers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { MerkleTree } from "../merkle/MerkleTree"; +const { mine } = require("@nomicfoundation/hardhat-network-helpers"); + +import { + VeaOutboxArbToGnosis, + ReceiverGatewayMock, + VeaInboxArbToGnosis, + SenderGatewayMock, + RouterArbToGnosis, + MockWETH, + MockAMB, + ArbSysMock, +} from "../../typechain-types"; + +// Constants +const TEN_ETH = BigNumber.from(10).pow(19); +const EPOCH_PERIOD = 600; +const CHALLENGE_PERIOD = 600; +const SEQUENCER_DELAY = 300; + +describe("Arbitrum to Gnosis Bridge Tests", async () => { + // Test participants + let bridger: SignerWithAddress; + let sender: SignerWithAddress; + let receiver: SignerWithAddress; + let challenger: SignerWithAddress; + + // Contracts + let veaOutbox: VeaOutboxArbToGnosis; + let receiverGateway: ReceiverGatewayMock; + let veaInbox: VeaInboxArbToGnosis; + let senderGateway: SenderGatewayMock; + let router: RouterArbToGnosis; + let amb: MockAMB; + let weth: MockWETH; + let arbsysMock: ArbSysMock; + + // Helper function to create a claim object + const createClaim = (stateRoot: string, claimer: string, timestamp: number) => ({ + stateRoot, + claimer, + timestampClaimed: timestamp, + timestampVerification: 0, + blocknumberVerification: 0, + honest: 0, + challenger: ethers.constants.AddressZero, + }); + + // Helper function to simulate dispute resolution + async function simulateDisputeResolution(epoch: number, claim: any) { + await veaInbox.connect(bridger).sendSnapshot(epoch, 100000, claim, { gasLimit: 100000 }); + + await network.provider.send("evm_increaseTime", [CHALLENGE_PERIOD + SEQUENCER_DELAY]); + await network.provider.send("evm_mine"); + + const events = await amb.queryFilter(amb.filters.MockedEvent()); + const lastEvent = events[events.length - 1]; + + await amb.executeMessageCall( + veaOutbox.address, + router.address, + lastEvent.args._data, + lastEvent.args.messageId, + 1000000 + ); + } + + // Helper function to setup a claim and challenge + async function setupClaimAndChallenge(epoch: any, merkleRoot: string, honest: number) { + const claimTx = await veaOutbox.connect(bridger).claim(epoch, merkleRoot); + const claimBlock = await ethers.provider.getBlock(claimTx.blockNumber!); + + const challengeTx = await veaOutbox.connect(challenger).challenge(epoch, { + stateRoot: merkleRoot, + claimer: bridger.address, + timestampClaimed: claimBlock.timestamp, + timestampVerification: 0, + blocknumberVerification: 0, + honest, + challenger: ethers.constants.AddressZero, + }); + + return { claimBlock, merkleRoot, challengeTx }; + } + + before("Initialize wallets", async () => { + [challenger, bridger, sender, receiver] = await ethers.getSigners(); + }); + + beforeEach("Setup contracts and tokens", async () => { + // Deploy contracts + await deployments.fixture(["ArbToGnosisOutbox", "ArbToGnosisInbox", "ArbToGnosisRouter"], { + fallbackToGlobal: true, + keepExistingDeployments: false, + }); + + // Get contract instances + veaOutbox = (await ethers.getContract("VeaOutboxArbToGnosis")) as VeaOutboxArbToGnosis; + receiverGateway = (await ethers.getContract("ArbToGnosisReceiverGateway")) as ReceiverGatewayMock; + veaInbox = (await ethers.getContract("VeaInboxArbToGnosis")) as VeaInboxArbToGnosis; + senderGateway = (await ethers.getContract("ArbToGnosisSenderGateway")) as SenderGatewayMock; + router = (await ethers.getContract("RouterArbToGnosis")) as RouterArbToGnosis; + amb = (await ethers.getContract("MockAMB")) as MockAMB; + weth = (await ethers.getContract("MockWETH")) as MockWETH; + arbsysMock = (await ethers.getContract("ArbSysMock")) as ArbSysMock; + + // Setup initial token balances + await weth.deposit({ value: TEN_ETH.mul(100) }); + await weth.transfer(bridger.address, TEN_ETH.mul(10)); + }); + + describe("Honest Claim - No Challenge - Bridger Paid", async () => { + it("should send a message and save snapshot", async () => { + const data = 1121; + await senderGateway.connect(sender).sendMessage(data); + await veaInbox.connect(bridger).saveSnapshot(); + + const BatchOutgoing = veaInbox.filters.SnapshotSaved(); + const batchOutGoingEvent = await veaInbox.queryFilter(BatchOutgoing); + const epoch = Math.floor((await batchOutGoingEvent[0].getBlock()).timestamp / EPOCH_PERIOD); + const batchMerkleRoot = await veaInbox.snapshots(epoch); + + expect(await veaInbox.snapshots(epoch)).to.equal(batchMerkleRoot, "Snapshot not saved correctly"); + }); + + it("should allow bridger to claim", async () => { + // Setup + const data = 1121; + await senderGateway.connect(sender).sendMessage(data); + await veaInbox.connect(bridger).saveSnapshot(); + + const BatchOutgoing = veaInbox.filters.SnapshotSaved(); + const batchOutGoingEvent = await veaInbox.queryFilter(BatchOutgoing); + const epoch = Math.floor((await batchOutGoingEvent[0].getBlock()).timestamp / EPOCH_PERIOD); + const batchMerkleRoot = await veaInbox.snapshots(epoch); + + // Advance time to next epoch + await network.provider.send("evm_increaseTime", [EPOCH_PERIOD]); + await network.provider.send("evm_mine"); + + // Approve WETH spending and claim + await weth.connect(bridger).approve(veaOutbox.address, TEN_ETH.mul(2)); + const claimTx = await veaOutbox.connect(bridger).claim(epoch, batchMerkleRoot); + + // Check claim event + await expect(claimTx).to.emit(veaOutbox, "Claimed").withArgs(bridger.address, epoch, batchMerkleRoot); + + // Ensure double claim is not possible + await expect(veaOutbox.connect(bridger).claim(epoch, batchMerkleRoot)).to.be.revertedWith("Claim already made."); + }); + + it("should start verification after maxL2StateSyncDelay", async () => { + // Setup + const data = 1121; + await senderGateway.connect(sender).sendMessage(data); + await veaInbox.connect(bridger).saveSnapshot(); + + const BatchOutgoing = veaInbox.filters.SnapshotSaved(); + const batchOutGoingEvent = await veaInbox.queryFilter(BatchOutgoing); + const epoch = Math.floor((await batchOutGoingEvent[0].getBlock()).timestamp / EPOCH_PERIOD); + const batchMerkleRoot = await veaInbox.snapshots(epoch); + + // Advance time and make claim + await network.provider.send("evm_increaseTime", [EPOCH_PERIOD]); + await network.provider.send("evm_mine"); + + await weth.connect(bridger).approve(veaOutbox.address, TEN_ETH); + const claimTx = await veaOutbox.connect(bridger).claim(epoch, batchMerkleRoot); + const block = await ethers.provider.getBlock(claimTx.blockNumber!); + + // Calculate and advance time for maxL2StateSyncDelay + const sequencerDelayLimit = await veaOutbox.sequencerDelayLimit(); + const maxL2StateSyncDelay = sequencerDelayLimit.add(EPOCH_PERIOD); + await network.provider.send("evm_increaseTime", [maxL2StateSyncDelay.toNumber()]); + await network.provider.send("evm_mine"); + + // Start verification + const startVerificationTx = await veaOutbox.startVerification( + epoch, + createClaim(batchMerkleRoot, bridger.address, block.timestamp) + ); + + await expect(startVerificationTx).to.emit(veaOutbox, "VerificationStarted").withArgs(epoch); + }); + + it("should verify snapshot after challenge period", async () => { + // Setup + const data = 1121; + await senderGateway.connect(sender).sendMessage(data); + await veaInbox.connect(bridger).saveSnapshot(); + + const BatchOutgoing = veaInbox.filters.SnapshotSaved(); + const batchOutGoingEvent = await veaInbox.queryFilter(BatchOutgoing); + const epoch = Math.floor((await batchOutGoingEvent[0].getBlock()).timestamp / EPOCH_PERIOD); + const batchMerkleRoot = await veaInbox.snapshots(epoch); + + // Advance time, make claim, and start verification + await network.provider.send("evm_increaseTime", [EPOCH_PERIOD]); + await network.provider.send("evm_mine"); + + await weth.connect(bridger).approve(veaOutbox.address, TEN_ETH); + const claimTx = await veaOutbox.connect(bridger).claim(epoch, batchMerkleRoot); + const block = await ethers.provider.getBlock(claimTx.blockNumber!); + + const sequencerDelayLimit = await veaOutbox.sequencerDelayLimit(); + const maxL2StateSyncDelay = sequencerDelayLimit.add(EPOCH_PERIOD); + + await network.provider.send("evm_increaseTime", [maxL2StateSyncDelay.toNumber()]); + await network.provider.send("evm_mine"); + + const startVerificationTx = await veaOutbox.startVerification( + epoch, + createClaim(batchMerkleRoot, bridger.address, block.timestamp) + ); + const verificationBlock = await ethers.provider.getBlock(startVerificationTx.blockNumber!); + + // Advance time for challenge period + const safeAdvanceTime = CHALLENGE_PERIOD + EPOCH_PERIOD; + await network.provider.send("evm_increaseTime", [safeAdvanceTime]); + await network.provider.send("evm_mine"); + + // Verify snapshot + await veaOutbox.connect(bridger).verifySnapshot(epoch, { + ...createClaim(batchMerkleRoot, bridger.address, block.timestamp), + timestampVerification: verificationBlock.timestamp, + blocknumberVerification: startVerificationTx.blockNumber!, + }); + + expect(await veaOutbox.stateRoot()).to.equal(batchMerkleRoot, "State root not updated correctly"); + expect(await veaOutbox.latestVerifiedEpoch()).to.equal(epoch, "Latest verified epoch not updated"); + }); + + it("should relay message after verification", async () => { + // Setup + const data = 1121; + const sendMessageTx = await senderGateway.connect(sender).sendMessage(data); + await veaInbox.connect(bridger).saveSnapshot(); + + const MessageSent = veaInbox.filters.MessageSent(); + const MessageSentEvent = await veaInbox.queryFilter(MessageSent); + const msg = MessageSentEvent[0].args._nodeData; + const nonce = "0x" + msg.slice(2, 18); + const to = "0x" + msg.slice(18, 58); + const msgData = "0x" + msg.slice(58); + + let nodes: string[] = []; + nodes.push(MerkleTree.makeLeafNode(nonce, to, msgData)); + const mt = new MerkleTree(nodes); + const proof = mt.getHexProof(nodes[0]); + + const BatchOutgoing = veaInbox.filters.SnapshotSaved(); + const batchOutGoingEvent = await veaInbox.queryFilter(BatchOutgoing); + const epoch = Math.floor((await batchOutGoingEvent[0].getBlock()).timestamp / EPOCH_PERIOD); + const batchMerkleRoot = await veaInbox.snapshots(epoch); + + // Advance time, make claim, and start verification + await network.provider.send("evm_increaseTime", [EPOCH_PERIOD]); + await network.provider.send("evm_mine"); + + await weth.connect(bridger).approve(veaOutbox.address, TEN_ETH); + const claimTx = await veaOutbox.connect(bridger).claim(epoch, batchMerkleRoot); + const block = await ethers.provider.getBlock(claimTx.blockNumber!); + + const sequencerDelayLimit = await veaOutbox.sequencerDelayLimit(); + const maxL2StateSyncDelay = sequencerDelayLimit.add(EPOCH_PERIOD); + + await network.provider.send("evm_increaseTime", [maxL2StateSyncDelay.toNumber()]); + await network.provider.send("evm_mine"); + + const startVerificationTx = await veaOutbox.startVerification( + epoch, + createClaim(batchMerkleRoot, bridger.address, block.timestamp) + ); + const verificationBlock = await ethers.provider.getBlock(startVerificationTx.blockNumber!); + + await network.provider.send("evm_increaseTime", [CHALLENGE_PERIOD]); + await mine(Math.ceil(CHALLENGE_PERIOD / 12)); + + await veaOutbox.connect(bridger).verifySnapshot(epoch, { + ...createClaim(batchMerkleRoot, bridger.address, block.timestamp), + timestampVerification: verificationBlock.timestamp, + blocknumberVerification: startVerificationTx.blockNumber!, + }); + + // Relay message + const relayTx = await veaOutbox.connect(receiver).sendMessage(proof, nonce, to, msgData); + await expect(relayTx).to.emit(veaOutbox, "MessageRelayed").withArgs(0); + + // Ensure message can't be relayed twice + await expect(veaOutbox.connect(receiver).sendMessage(proof, nonce, to, msgData)).to.be.revertedWith( + "Message already relayed" + ); + }); + + it("should allow bridger to withdraw deposit after successful claim", async () => { + // Setup + const data = 1121; + await senderGateway.connect(sender).sendMessage(data); + await veaInbox.connect(bridger).saveSnapshot(); + + const BatchOutgoing = veaInbox.filters.SnapshotSaved(); + const batchOutGoingEvent = await veaInbox.queryFilter(BatchOutgoing); + const epoch = Math.floor((await batchOutGoingEvent[0].getBlock()).timestamp / EPOCH_PERIOD); + const batchMerkleRoot = await veaInbox.snapshots(epoch); + + // Advance time, make claim, and start verification + await network.provider.send("evm_increaseTime", [EPOCH_PERIOD]); + await network.provider.send("evm_mine"); + + await weth.connect(bridger).approve(veaOutbox.address, TEN_ETH); + const claimTx = await veaOutbox.connect(bridger).claim(epoch, batchMerkleRoot); + const block = await ethers.provider.getBlock(claimTx.blockNumber!); + + const sequencerDelayLimit = await veaOutbox.sequencerDelayLimit(); + const maxL2StateSyncDelay = sequencerDelayLimit.add(EPOCH_PERIOD); + + await network.provider.send("evm_increaseTime", [maxL2StateSyncDelay.toNumber()]); + await network.provider.send("evm_mine"); + + const startVerificationTx = await veaOutbox.startVerification( + epoch, + createClaim(batchMerkleRoot, bridger.address, block.timestamp) + ); + const verificationBlock = await ethers.provider.getBlock(startVerificationTx.blockNumber!); + + await network.provider.send("evm_increaseTime", [CHALLENGE_PERIOD]); + await mine(Math.ceil(CHALLENGE_PERIOD / 12)); + + await veaOutbox.connect(bridger).verifySnapshot(epoch, { + ...createClaim(batchMerkleRoot, bridger.address, block.timestamp), + timestampVerification: verificationBlock.timestamp, + blocknumberVerification: startVerificationTx.blockNumber!, + }); + + // Withdraw deposit + const initialBalance = await weth.balanceOf(bridger.address); + await veaOutbox.connect(bridger).withdrawClaimDeposit(epoch, { + ...createClaim(batchMerkleRoot, bridger.address, block.timestamp), + timestampVerification: verificationBlock.timestamp, + blocknumberVerification: startVerificationTx.blockNumber!, + honest: 1, + }); + const finalBalance = await weth.balanceOf(bridger.address); + + expect(finalBalance.sub(initialBalance)).to.equal(TEN_ETH, "Incorrect withdrawal amount"); + }); + }); + + describe("Honest Claim - Dishonest Challenge - Bridger paid, challenger deposit forfeited", async () => { + let epoch: number; + let batchMerkleRoot: string; + + beforeEach(async () => { + // Setup: Send message and save snapshot on Arbitrum + await senderGateway.connect(sender).sendMessage(1121); + await veaInbox.connect(bridger).saveSnapshot(); + + const BatchOutgoing = veaInbox.filters.SnapshotSaved(); + const batchOutGoingEvent = await veaInbox.queryFilter(BatchOutgoing); + epoch = Math.floor((await batchOutGoingEvent[0].getBlock()).timestamp / EPOCH_PERIOD); + batchMerkleRoot = await veaInbox.snapshots(epoch); + + // Advance time to next epoch + await network.provider.send("evm_increaseTime", [EPOCH_PERIOD]); + await network.provider.send("evm_mine"); + + // Ensure bridger and challenger have enough WETH + await weth.transfer(bridger.address, TEN_ETH.mul(10)); + await weth.transfer(challenger.address, TEN_ETH.mul(10)); + + // Approve WETH spending for both + await weth.connect(bridger).approve(veaOutbox.address, TEN_ETH.mul(10)); + await weth.connect(challenger).approve(veaOutbox.address, TEN_ETH.mul(10)); + await amb.setMaxGasPerTx(100000); + }); + + it("should allow challenger to submit a challenge", async () => { + const { claimBlock, challengeTx } = await setupClaimAndChallenge(epoch, batchMerkleRoot, 0); + + await expect(challengeTx).to.emit(veaOutbox, "Challenged").withArgs(epoch, challenger.address); + }); + + it("should handle the entire cross-chain dispute resolution process", async () => { + const { claimBlock } = await setupClaimAndChallenge(epoch, batchMerkleRoot, 0); + + const sendSnapshotTx = await veaInbox.connect(bridger).sendSnapshot( + epoch, + 100000, + { + stateRoot: batchMerkleRoot, + claimer: bridger.address, + timestampClaimed: claimBlock.timestamp, + timestampVerification: 0, + blocknumberVerification: 0, + honest: 0, + challenger: challenger.address, + }, + { gasLimit: 100000 } + ); + + await expect(sendSnapshotTx) + .to.emit(veaInbox, "SnapshotSent") + .withArgs(epoch, ethers.utils.formatBytes32String("")); + + await network.provider.send("evm_increaseTime", [EPOCH_PERIOD]); + await network.provider.send("evm_mine"); + + const routerEvents = await router.queryFilter(router.filters.Routed(), sendSnapshotTx.blockNumber); + expect(routerEvents.length).to.equal(1, "Expected one Routed event"); + const routedEvent = routerEvents[0]; + expect(routedEvent.args._epoch).to.equal(epoch, "Routed event epoch mismatch"); + expect(routedEvent.args._ticketID).to.not.equal( + ethers.constants.HashZero, + "Routed event ticketID should not be zero" + ); + + // Simulate time passing for claim and challenge period + await network.provider.send("evm_increaseTime", [CHALLENGE_PERIOD + SEQUENCER_DELAY]); + await network.provider.send("evm_mine"); + + const events = await amb.queryFilter(amb.filters.MockedEvent()); + expect(events.length).to.be.above(0, "No MockedEvent emitted"); + + // Simulate the passage of time + await network.provider.send("evm_increaseTime", [EPOCH_PERIOD]); + await network.provider.send("evm_mine"); + + const lastEvent = events[events.length - 1]; + + await amb.executeMessageCall( + veaOutbox.address, + router.address, + lastEvent.args._data, + lastEvent.args.messageId, + 1000000 + ); + + expect(await amb.messageCallStatus(lastEvent.args.messageId)).to.be.true; + + // Check for Verified event + const verifiedEvents = await veaOutbox.queryFilter(veaOutbox.filters.Verified()); + expect(verifiedEvents.length).to.equal(1, "Expected one Verified event"); + const verifiedEvent = verifiedEvents[0]; + expect(verifiedEvent.args._epoch).to.equal(epoch, "Verified event epoch mismatch"); + + const expectedClaim = { + stateRoot: batchMerkleRoot, + claimer: bridger.address, + timestampClaimed: claimBlock.timestamp, + timestampVerification: 0, + blocknumberVerification: 0, + honest: 1, + challenger: challenger.address, + }; + + const expectedClaimHash = await veaOutbox.hashClaim(expectedClaim); + const storedClaimHash = await veaOutbox.claimHashes(epoch); + + expect(storedClaimHash).to.equal(expectedClaimHash, "Stored claim hash does not match expected"); + expect(await veaOutbox.stateRoot()).to.equal(batchMerkleRoot, "VeaOutbox stateRoot should be updated"); + expect(await veaOutbox.latestVerifiedEpoch()).to.equal(epoch, "VeaOutbox latestVerifiedEpoch should be updated"); + }); + + it("should not update latestEpoch and stateRoot when resolving older dispute", async () => { + const { claimBlock } = await setupClaimAndChallenge(epoch, batchMerkleRoot, 0); + + // Create and verify newer epochs + const newEpoch1 = epoch + 1; + const newMerkleRoot1 = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("newer1")); + + // Advance time to the next epoch + await network.provider.send("evm_increaseTime", [EPOCH_PERIOD]); + await network.provider.send("evm_mine"); + + const newClaimTxOne = await veaOutbox.connect(bridger).claim(newEpoch1, newMerkleRoot1); + const newClaimTxOneBlock = await ethers.provider.getBlock(newClaimTxOne.blockNumber!); + + const sequencerDelayLimit = await veaOutbox.sequencerDelayLimit(); + const maxL2StateSyncDelay = sequencerDelayLimit.add(EPOCH_PERIOD); + await network.provider.send("evm_increaseTime", [maxL2StateSyncDelay.toNumber()]); + await network.provider.send("evm_mine"); + + const newVerifyTxOne = await veaOutbox.startVerification( + newEpoch1, + createClaim(newMerkleRoot1, bridger.address, newClaimTxOneBlock.timestamp) + ); + const newVerifyTxOneBlock = await ethers.provider.getBlock(newVerifyTxOne.blockNumber!); + + await network.provider.send("evm_increaseTime", [CHALLENGE_PERIOD]); + await network.provider.send("evm_mine"); + + await veaOutbox.connect(bridger).verifySnapshot(newEpoch1, { + ...createClaim(newMerkleRoot1, bridger.address, newClaimTxOneBlock.timestamp), + blocknumberVerification: newVerifyTxOne.blockNumber!, + timestampVerification: newVerifyTxOneBlock.timestamp, + }); + + // Advance time to the next epoch + await network.provider.send("evm_increaseTime", [EPOCH_PERIOD]); + await network.provider.send("evm_mine"); + + const newEpoch2 = (await veaOutbox.epochNow()).toNumber() - 1; + const newMerkleRoot2 = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("newer2")); + const newClaimTxTwo = await veaOutbox.connect(bridger).claim(newEpoch2, newMerkleRoot2); + + const newClaimTxTwoBlock = await ethers.provider.getBlock(newClaimTxTwo.blockNumber!); + + await network.provider.send("evm_increaseTime", [maxL2StateSyncDelay.toNumber()]); + await network.provider.send("evm_mine"); + + const newVerifyTxTwo = await veaOutbox.startVerification( + newEpoch2, + createClaim(newMerkleRoot2, bridger.address, newClaimTxTwoBlock.timestamp) + ); + const newVerifyTxTwoBlock = await ethers.provider.getBlock(newVerifyTxTwo.blockNumber!); + + await network.provider.send("evm_increaseTime", [CHALLENGE_PERIOD]); + await network.provider.send("evm_mine"); + + await veaOutbox.connect(bridger).verifySnapshot(newEpoch2, { + ...createClaim(newMerkleRoot2, bridger.address, newClaimTxTwoBlock.timestamp), + timestampVerification: newVerifyTxTwoBlock.timestamp!, + blocknumberVerification: newVerifyTxTwo.blockNumber!, + }); + + // Resolve the dispute for the old epoch + await simulateDisputeResolution(epoch, { + stateRoot: batchMerkleRoot, + claimer: bridger.address, + timestampClaimed: claimBlock.timestamp, + timestampVerification: 0, + blocknumberVerification: 0, + honest: 0, + challenger: challenger.address, + }); + + // Check that latestEpoch and stateRoot weren't updated to the old epoch's data + expect(await veaOutbox.latestVerifiedEpoch()).to.equal(newEpoch2, "Latest verified epoch should not change"); + expect(await veaOutbox.stateRoot()).to.equal(newMerkleRoot2, "State root should not change"); + }); + + it("should allow bridger to withdraw deposit plus reward", async () => { + const { claimBlock } = await setupClaimAndChallenge(epoch, batchMerkleRoot, 0); + + await simulateDisputeResolution(epoch, { + stateRoot: batchMerkleRoot, + claimer: bridger.address, + timestampClaimed: claimBlock.timestamp, + timestampVerification: 0, + blocknumberVerification: 0, + honest: 0, + challenger: challenger.address, + }); + + const bridgerInitialBalance = await weth.balanceOf(bridger.address); + await veaOutbox.connect(bridger).withdrawClaimDeposit(epoch, { + stateRoot: batchMerkleRoot, + claimer: bridger.address, + timestampClaimed: claimBlock.timestamp, + timestampVerification: 0, + blocknumberVerification: 0, + honest: 1, + challenger: challenger.address, + }); + const bridgerFinalBalance = await weth.balanceOf(bridger.address); + expect(bridgerFinalBalance.sub(bridgerInitialBalance)).to.equal( + TEN_ETH.add(TEN_ETH.div(2)), + "Incorrect withdrawal amount" + ); + }); + + it("should not allow challenger to withdraw deposit", async () => { + const { claimBlock } = await setupClaimAndChallenge(epoch, batchMerkleRoot, 0); + + await simulateDisputeResolution(epoch, { + stateRoot: batchMerkleRoot, + claimer: bridger.address, + timestampClaimed: claimBlock.timestamp, + timestampVerification: 0, + blocknumberVerification: 0, + honest: 0, + challenger: challenger.address, + }); + + await expect( + veaOutbox.connect(challenger).withdrawChallengeDeposit(epoch, { + stateRoot: batchMerkleRoot, + claimer: bridger.address, + timestampClaimed: claimBlock.timestamp, + timestampVerification: 0, + blocknumberVerification: 0, + honest: 1, + challenger: challenger.address, + }) + ).to.be.revertedWith("Challenge failed."); + }); + + it("should allow message relay after dispute resolution", async () => { + const { claimBlock } = await setupClaimAndChallenge(epoch, batchMerkleRoot, 0); + + await simulateDisputeResolution(epoch, { + stateRoot: batchMerkleRoot, + claimer: bridger.address, + timestampClaimed: claimBlock.timestamp, + timestampVerification: 0, + blocknumberVerification: 0, + honest: 0, + challenger: challenger.address, + }); + + const MessageSent = veaInbox.filters.MessageSent(); + const MessageSentEvent = await veaInbox.queryFilter(MessageSent); + const msg = MessageSentEvent[0].args._nodeData; + const nonce = "0x" + msg.slice(2, 18); + const to = "0x" + msg.slice(18, 58); + const msgData = "0x" + msg.slice(58); + + let nodes: string[] = []; + nodes.push(MerkleTree.makeLeafNode(nonce, to, msgData)); + const mt = new MerkleTree(nodes); + const proof = mt.getHexProof(nodes[0]); + + const relayTx = await veaOutbox.connect(receiver).sendMessage(proof, 0, receiverGateway.address, msgData); + await expect(relayTx).to.emit(veaOutbox, "MessageRelayed").withArgs(0); + }); + }); + + describe("Dishonest Claim - Honest Challenge - Bridger deposit forfeited, Challenger paid", async () => { + let epoch: number; + let dishonestMerkleRoot: string; + let honestMerkleRoot: string; + + beforeEach(async () => { + // Setup: Send message and save snapshot on Arbitrum + await senderGateway.connect(sender).sendMessage(1121); + await veaInbox.connect(bridger).saveSnapshot(); + + const BatchOutgoing = veaInbox.filters.SnapshotSaved(); + const batchOutGoingEvent = await veaInbox.queryFilter(BatchOutgoing); + epoch = Math.floor((await batchOutGoingEvent[0].getBlock()).timestamp / EPOCH_PERIOD); + honestMerkleRoot = await veaInbox.snapshots(epoch); + dishonestMerkleRoot = ethers.utils.keccak256("0x123456"); // Simulating a dishonest state root + + // Advance time to next epoch + await network.provider.send("evm_increaseTime", [EPOCH_PERIOD]); + await network.provider.send("evm_mine"); + + // Ensure bridger and challenger have enough WETH + await weth.transfer(bridger.address, TEN_ETH.mul(10)); + await weth.transfer(challenger.address, TEN_ETH.mul(10)); + + // Approve WETH spending for both + await weth.connect(bridger).approve(veaOutbox.address, TEN_ETH.mul(10)); + await weth.connect(challenger).approve(veaOutbox.address, TEN_ETH.mul(10)); + }); + + it("should allow challenger to submit a challenge to a dishonest claim", async () => { + const { claimBlock, challengeTx } = await setupClaimAndChallenge(epoch, dishonestMerkleRoot, 0); + + await expect(challengeTx).to.emit(veaOutbox, "Challenged").withArgs(epoch, challenger.address); + }); + + it("should initiate cross-chain dispute resolution for dishonest claim", async () => { + const { claimBlock } = await setupClaimAndChallenge(epoch, dishonestMerkleRoot, 0); + + const sendSnapshotTx = await veaInbox.connect(bridger).sendSnapshot( + epoch, + 100000, + { + stateRoot: dishonestMerkleRoot, + claimer: bridger.address, + timestampClaimed: claimBlock.timestamp, + timestampVerification: 0, + blocknumberVerification: 0, + honest: 0, + challenger: challenger.address, + }, + { gasLimit: 100000 } + ); + + await expect(sendSnapshotTx) + .to.emit(veaInbox, "SnapshotSent") + .withArgs(epoch, ethers.utils.formatBytes32String("")); + + const routerEvents = await router.queryFilter(router.filters.Routed(), sendSnapshotTx.blockNumber); + expect(routerEvents.length).to.equal(1, "Expected one Routed event"); + const routedEvent = routerEvents[0]; + expect(routedEvent.args._epoch).to.equal(epoch, "Routed event epoch mismatch"); + }); + + it("should resolve dispute in favor of the challenger", async () => { + const { claimBlock } = await setupClaimAndChallenge(epoch, dishonestMerkleRoot, 0); + + await simulateDisputeResolution(epoch, { + stateRoot: dishonestMerkleRoot, + claimer: bridger.address, + timestampClaimed: claimBlock.timestamp, + timestampVerification: 0, + blocknumberVerification: 0, + honest: 0, + challenger: challenger.address, + }); + + const expectedClaim = { + stateRoot: dishonestMerkleRoot, + claimer: bridger.address, + timestampClaimed: claimBlock.timestamp, + timestampVerification: 0, + blocknumberVerification: 0, + honest: 2, + challenger: challenger.address, + }; + + const expectedClaimHash = await veaOutbox.hashClaim(expectedClaim); + const storedClaimHash = await veaOutbox.claimHashes(epoch); + + expect(storedClaimHash).to.equal(expectedClaimHash, "Stored claim hash does not match expected"); + + expect(await veaOutbox.stateRoot()).to.equal(honestMerkleRoot, "State root should be updated to honest root"); + }); + + it("should not allow dishonest bridger to withdraw deposit", async () => { + const { claimBlock } = await setupClaimAndChallenge(epoch, dishonestMerkleRoot, 0); + + await simulateDisputeResolution(epoch, { + stateRoot: dishonestMerkleRoot, + claimer: bridger.address, + timestampClaimed: claimBlock.timestamp, + timestampVerification: 0, + blocknumberVerification: 0, + honest: 0, + challenger: challenger.address, + }); + + await expect( + veaOutbox.connect(bridger).withdrawClaimDeposit(epoch, { + stateRoot: dishonestMerkleRoot, + claimer: bridger.address, + timestampClaimed: claimBlock.timestamp, + timestampVerification: 0, + blocknumberVerification: 0, + honest: 2, + challenger: challenger.address, + }) + ).to.be.revertedWith("Claim failed."); + }); + + it("should allow challenger to withdraw deposit plus reward", async () => { + const { claimBlock } = await setupClaimAndChallenge(epoch, dishonestMerkleRoot, 0); + + await simulateDisputeResolution(epoch, { + stateRoot: dishonestMerkleRoot, + claimer: bridger.address, + timestampClaimed: claimBlock.timestamp, + timestampVerification: 0, + blocknumberVerification: 0, + honest: 0, + challenger: challenger.address, + }); + + const challengerInitialBalance = await weth.balanceOf(challenger.address); + await veaOutbox.connect(challenger).withdrawChallengeDeposit(epoch, { + stateRoot: dishonestMerkleRoot, + claimer: bridger.address, + timestampClaimed: claimBlock.timestamp, + timestampVerification: 0, + blocknumberVerification: 0, + honest: 2, + challenger: challenger.address, + }); + const challengerFinalBalance = await weth.balanceOf(challenger.address); + expect(challengerFinalBalance.sub(challengerInitialBalance)).to.equal( + TEN_ETH.add(TEN_ETH.div(2)), + "Incorrect withdrawal amount" + ); + }); + + it("should allow message relay with correct state root after dispute resolution", async () => { + const { claimBlock } = await setupClaimAndChallenge(epoch, dishonestMerkleRoot, 0); + + await simulateDisputeResolution(epoch, { + stateRoot: dishonestMerkleRoot, + claimer: bridger.address, + timestampClaimed: claimBlock.timestamp, + timestampVerification: 0, + blocknumberVerification: 0, + honest: 0, + challenger: challenger.address, + }); + + const MessageSent = veaInbox.filters.MessageSent(); + const MessageSentEvent = await veaInbox.queryFilter(MessageSent); + const msg = MessageSentEvent[0].args._nodeData; + const nonce = "0x" + msg.slice(2, 18); + const to = "0x" + msg.slice(18, 58); + const msgData = "0x" + msg.slice(58); + + let nodes: string[] = []; + nodes.push(MerkleTree.makeLeafNode(nonce, to, msgData)); + const mt = new MerkleTree(nodes); + const proof = mt.getHexProof(nodes[0]); + + const relayTx = await veaOutbox.connect(receiver).sendMessage(proof, 0, receiverGateway.address, msgData); + await expect(relayTx).to.emit(veaOutbox, "MessageRelayed").withArgs(0); + }); + + it("should update latest verified epoch and state root correctly after dispute resolution", async () => { + const { claimBlock } = await setupClaimAndChallenge(epoch, dishonestMerkleRoot, 0); + + await simulateDisputeResolution(epoch, { + stateRoot: dishonestMerkleRoot, + claimer: bridger.address, + timestampClaimed: claimBlock.timestamp, + timestampVerification: 0, + blocknumberVerification: 0, + honest: 0, + challenger: challenger.address, + }); + + expect(await veaOutbox.latestVerifiedEpoch()).to.equal(epoch, "Latest verified epoch should be updated"); + expect(await veaOutbox.stateRoot()).to.equal(honestMerkleRoot, "State root should be updated to honest root"); + }); + + it("should not update latestEpoch and stateRoot when resolving older dispute", async () => { + const { claimBlock } = await setupClaimAndChallenge(epoch, dishonestMerkleRoot, 0); + + // Create and verify newer epochs + const newEpoch1 = epoch + 1; + const newMerkleRoot1 = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("newer1")); + + // Advance time to the next epoch + await network.provider.send("evm_increaseTime", [EPOCH_PERIOD]); + await network.provider.send("evm_mine"); + + const newClaimTxOne = await veaOutbox.connect(bridger).claim(newEpoch1, newMerkleRoot1); + const newClaimTxOneBlock = await ethers.provider.getBlock(newClaimTxOne.blockNumber!); + + const sequencerDelayLimit = await veaOutbox.sequencerDelayLimit(); + const maxL2StateSyncDelay = sequencerDelayLimit.add(EPOCH_PERIOD); + await network.provider.send("evm_increaseTime", [maxL2StateSyncDelay.toNumber()]); + await network.provider.send("evm_mine"); + + const newVerifyTxOne = await veaOutbox.startVerification( + newEpoch1, + createClaim(newMerkleRoot1, bridger.address, newClaimTxOneBlock.timestamp) + ); + const newVerifyTxOneBlock = await ethers.provider.getBlock(newVerifyTxOne.blockNumber!); + + await network.provider.send("evm_increaseTime", [CHALLENGE_PERIOD]); + await network.provider.send("evm_mine"); + + await veaOutbox.connect(bridger).verifySnapshot(newEpoch1, { + ...createClaim(newMerkleRoot1, bridger.address, newClaimTxOneBlock.timestamp), + blocknumberVerification: newVerifyTxOne.blockNumber!, + timestampVerification: newVerifyTxOneBlock.timestamp, + }); + + // Advance time to the next epoch + await network.provider.send("evm_increaseTime", [EPOCH_PERIOD]); + await network.provider.send("evm_mine"); + + const newEpoch2 = (await veaOutbox.epochNow()).toNumber() - 1; + const newMerkleRoot2 = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("newer2")); + const newClaimTxTwo = await veaOutbox.connect(bridger).claim(newEpoch2, newMerkleRoot2); + + const newClaimTxTwoBlock = await ethers.provider.getBlock(newClaimTxTwo.blockNumber!); + + await network.provider.send("evm_increaseTime", [maxL2StateSyncDelay.toNumber()]); + await network.provider.send("evm_mine"); + + const newVerifyTxTwo = await veaOutbox.startVerification( + newEpoch2, + createClaim(newMerkleRoot2, bridger.address, newClaimTxTwoBlock.timestamp) + ); + const newVerifyTxTwoBlock = await ethers.provider.getBlock(newVerifyTxTwo.blockNumber!); + + await network.provider.send("evm_increaseTime", [CHALLENGE_PERIOD]); + await network.provider.send("evm_mine"); + + await veaOutbox.connect(bridger).verifySnapshot(newEpoch2, { + ...createClaim(newMerkleRoot2, bridger.address, newClaimTxTwoBlock.timestamp), + timestampVerification: newVerifyTxTwoBlock.timestamp!, + blocknumberVerification: newVerifyTxTwo.blockNumber!, + }); + + // Resolve the dispute for the old epoch + await simulateDisputeResolution(epoch, { + stateRoot: dishonestMerkleRoot, + claimer: bridger.address, + timestampClaimed: claimBlock.timestamp, + timestampVerification: 0, + blocknumberVerification: 0, + honest: 0, + challenger: challenger.address, + }); + + // Check that latestEpoch and stateRoot weren't updated to the old epoch's data + expect(await veaOutbox.latestVerifiedEpoch()).to.equal(newEpoch2, "Latest verified epoch should not change"); + expect(await veaOutbox.stateRoot()).to.equal(newMerkleRoot2, "State root should not change"); + }); + + it("should not allow multiple withdrawals for the same challenge", async () => { + const { claimBlock } = await setupClaimAndChallenge(epoch, dishonestMerkleRoot, 0); + + await simulateDisputeResolution(epoch, { + stateRoot: dishonestMerkleRoot, + claimer: bridger.address, + timestampClaimed: claimBlock.timestamp, + timestampVerification: 0, + blocknumberVerification: 0, + honest: 0, + challenger: challenger.address, + }); + + // First withdrawal should succeed + await veaOutbox.connect(challenger).withdrawChallengeDeposit(epoch, { + stateRoot: dishonestMerkleRoot, + claimer: bridger.address, + timestampClaimed: claimBlock.timestamp, + timestampVerification: 0, + blocknumberVerification: 0, + honest: 2, + challenger: challenger.address, + }); + + // Second withdrawal should fail + await expect( + veaOutbox.connect(challenger).withdrawChallengeDeposit(epoch, { + stateRoot: dishonestMerkleRoot, + claimer: bridger.address, + timestampClaimed: claimBlock.timestamp, + timestampVerification: 0, + blocknumberVerification: 0, + honest: 2, + challenger: challenger.address, + }) + ).to.be.revertedWith("Invalid claim."); + }); + }); +});