diff --git a/contracts/deploy/00-home-chain-arbitration.ts b/contracts/deploy/00-home-chain-arbitration.ts index bcb27b7be..be3d8d51a 100644 --- a/contracts/deploy/00-home-chain-arbitration.ts +++ b/contracts/deploy/00-home-chain-arbitration.ts @@ -50,6 +50,12 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) ).address ); } + const centralizedArbitrator = await deploy("CentralizedArbitrator", { + from: deployer, + args: [0, 0, 0], + log: true, + }); + const pnk = pnkByChain.get(Number(await getChainId())) ?? AddressZero; const minStake = BigNumber.from(10).pow(20).mul(2); const alpha = 10000; diff --git a/contracts/deploy/01-foreign-chain.ts b/contracts/deploy/01-foreign-chain.ts index 370b851ac..a92bd1266 100644 --- a/contracts/deploy/01-foreign-chain.ts +++ b/contracts/deploy/01-foreign-chain.ts @@ -12,20 +12,23 @@ enum ForeignChains { } const paramsByChainId = { 1: { - claimDeposit: parseEther("0.1"), - challengeDuration: 86400, // 1 day + deposit: parseEther("0.1"), + epochPeriod: 86400, // 1 day homeChainId: 42161, + arbitrumInbox: "0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f", }, 4: { - claimDeposit: parseEther("0.1"), - challengeDuration: 120, // 2 min + deposit: parseEther("0.1"), + epochPeriod: 86400, // 1 day homeChainId: 421611, + arbitrumInbox: "0x578BAde599406A8fE3d24Fd7f7211c0911F5B29e", }, 31337: { - claimDeposit: parseEther("0.1"), - challengeDuration: 120, // 2 min + deposit: parseEther("0.1"), + epochPeriod: 86400, // 1 day homeChainId: 31337, - }, + arbitrumInbox: "0x00", + } }; const deployForeignGateway: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { @@ -50,36 +53,44 @@ const deployForeignGateway: DeployFunction = async (hre: HardhatRuntimeEnvironme let nonce; if (chainId === ForeignChains.HARDHAT) { nonce = await ethers.provider.getTransactionCount(deployer); - nonce += 4; // HomeGatewayToEthereum deploy tx will be the 6th after this, same network for both home/foreign. + nonce += 5; // HomeGatewayToEthereum deploy tx will be the 6th after this, same network for both home/foreign. } else { const homeChainProvider = new providers.JsonRpcProvider(homeNetworks[chainId].url); nonce = await homeChainProvider.getTransactionCount(deployer); nonce += 1; // HomeGatewayToEthereum deploy tx will the third tx after this on its home network, so we add two to the current nonce. } - const { claimDeposit, challengeDuration, homeChainId } = paramsByChainId[chainId]; - const challengeDeposit = claimDeposit; - const bridgeAlpha = 5000; + const { deposit, epochPeriod, homeChainId, arbitrumInbox } = paramsByChainId[chainId]; const homeChainIdAsBytes32 = hexZeroPad(homeChainId, 32); const homeGatewayAddress = getContractAddress(deployer, nonce); console.log("calculated future HomeGatewayToEthereum address for nonce %d: %s", nonce, homeGatewayAddress); + nonce -= 1; + + const fastBridgeSenderAddress = getContractAddress(deployer, nonce); + console.log("calculated future FastSender for nonce %d: %s", nonce, fastBridgeSenderAddress); + + nonce += 5; + + const inboxAddress = chainId === ForeignChains.HARDHAT ? getContractAddress(deployer, nonce) : arbitrumInbox; + console.log("calculated future inboxAddress for nonce %d: %s", nonce, inboxAddress); + + const genesis = 1652709415 // sample genesis time const fastBridgeReceiver = await deploy("FastBridgeReceiverOnEthereum", { from: deployer, args: [ - deployer, - ethers.constants.AddressZero, // should be safeBridgeSender - ethers.constants.AddressZero, // should be Arbitrum Inbox - claimDeposit, - challengeDeposit, - challengeDuration, - bridgeAlpha, + deposit, + epochPeriod, + genesis, + inboxAddress, + fastBridgeSenderAddress, ], log: true, }); const foreignGateway = await deploy("ForeignGatewayOnEthereum", { from: deployer, + contract: "ForeignGateway", args: [ deployer, fastBridgeReceiver.address, @@ -87,12 +98,14 @@ const deployForeignGateway: DeployFunction = async (hre: HardhatRuntimeEnvironme homeGatewayAddress, homeChainIdAsBytes32, ], + gasLimit: 4000000, log: true, }); const metaEvidenceUri = "https://raw.githubusercontent.com/kleros/kleros-v2/master/contracts/deployments/rinkeby/MetaEvidence_ArbitrableExample.json"; - const arbitrable = await deploy("ArbitrableExample", { + + await deploy("ArbitrableExample", { from: deployer, args: [foreignGateway.address, metaEvidenceUri], log: true, diff --git a/contracts/deploy/02-home-chain.ts b/contracts/deploy/02-home-chain.ts index 87cef5270..8526b26b8 100644 --- a/contracts/deploy/02-home-chain.ts +++ b/contracts/deploy/02-home-chain.ts @@ -1,10 +1,11 @@ import { HardhatRuntimeEnvironment } from "hardhat/types"; import { DeployFunction } from "hardhat-deploy/types"; -import { Address } from "ethereumjs-util"; import { ethers } from "hardhat"; const HOME_CHAIN_IDS = [42161, 421611, 31337]; // ArbOne, ArbRinkeby, Hardhat +// TODO: use deterministic deployments + const deployHomeGateway: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { deployments, getNamedAccounts, getChainId } = hre; const { deploy, execute } = deployments; @@ -14,35 +15,106 @@ const deployHomeGateway: DeployFunction = async (hre: HardhatRuntimeEnvironment) const deployer = (await getNamedAccounts()).deployer ?? (await hre.ethers.getSigners())[0].address; console.log("deployer: %s", deployer); - // The object below is not available when launching the hardhat node. - // TODO: use deterministic deployments - const fastBridgeReceiver = - chainId === 31337 - ? await deployments.get("FastBridgeReceiverOnEthereum") - : await hre.companionNetworks.foreign.deployments.get("FastBridgeReceiverOnEthereum"); - const fastBridgeSender = await deploy("FastBridgeSenderToEthereum", { - from: deployer, - args: [deployer, fastBridgeReceiver.address, ethers.constants.AddressZero], - log: true, - }); // nonce+0 - - const klerosCore = await deployments.get("KlerosCore"); - const foreignGateway = - chainId === 31337 - ? await deployments.get("ForeignGatewayOnEthereum") - : await hre.companionNetworks.foreign.deployments.get("ForeignGatewayOnEthereum"); - const foreignChainId = chainId === 31337 ? 31337 : Number(await hre.companionNetworks.foreign.getChainId()); - const homeGateway = await deploy("HomeGatewayToEthereum", { - from: deployer, - args: [klerosCore.address, fastBridgeSender.address, foreignGateway.address, foreignChainId], - log: true, - }); // nonce+1 - - const fastSender = await hre.ethers - .getContractAt("FastBridgeSenderToEthereum", fastBridgeSender.address) - .then((contract) => contract.fastBridgeSender()); - if (fastSender === ethers.constants.AddressZero) { - await execute("FastBridgeSenderToEthereum", { from: deployer, log: true }, "changeFastSender", homeGateway.address); + // ---------------------------------------------------------------------------------------------- + const hardhatDeployer = async () => { + const fastBridgeReceiver = await deployments.get("FastBridgeReceiverOnEthereum"); + const arbSysMock = await deploy("ArbSysMock", { from: deployer, log: true }); + const epochPeriod = 86400; // 1 day + const genesis = 1652709415 // sample genesis time + + const fastBridgeSender = await deploy("FastBridgeSenderToEthereumMock", { + from: deployer, + contract: "FastBridgeSenderMock", + args: [epochPeriod, genesis, fastBridgeReceiver.address, arbSysMock.address], + log: true, + }); // nonce+0 + + const klerosCore = await deployments.get("KlerosCore"); + const foreignGateway = await deployments.get("ForeignGatewayOnEthereum"); + let foreignChainId = 1; + if (chainId === 31337){ + foreignChainId = 31337; + } else if (chainId === 421611){ + foreignChainId = 4; + } + const homeGatewayToEthereum = await deploy("HomeGatewayToEthereum", { + from: deployer, + contract: "HomeGateway", + args: [deployer, klerosCore.address, fastBridgeSender.address, foreignGateway.address, foreignChainId], + gasLimit: 4000000, + log: true, + }); // nonce+1 + foreignChainId = 100; + if (chainId === 31337){ + foreignChainId = 31337; + } + const homeGatewayToGnosis = await deploy("HomeGatewayToGnosis", { + from: deployer, + contract: "HomeGateway", + args: [deployer, klerosCore.address, fastBridgeSender.address, foreignGateway.address, foreignChainId], + gasLimit: 4000000, + log: true, + }); // nonce+1 + + + const outbox = await deploy("OutboxMock", { + from: deployer, + args: [fastBridgeSender.address], + log: true, + }); + + const bridge = await deploy("BridgeMock", { + from: deployer, + args: [outbox.address], + log: true, + }); + + await deploy("InboxMock", { + from: deployer, + args: [bridge.address], + log: true, + }); + }; + + // ---------------------------------------------------------------------------------------------- + const liveDeployer = async () => { + const fastBridgeReceiver = await hre.companionNetworks.foreign.deployments.get("FastBridgeReceiverOnEthereum"); + + const fastBridgeSender = await deploy("FastBridgeSenderToEthereum", { + from: deployer, + args: [deployer, fastBridgeReceiver.address, ethers.constants.AddressZero], + log: true, + }); // nonce+0 + + const klerosCore = await deployments.get("KlerosCore"); + const foreignGateway = await hre.companionNetworks.foreign.deployments.get("ForeignGatewayOnEthereum"); + const foreignChainId = Number(await hre.companionNetworks.foreign.getChainId()); + const homeGateway = await deploy("HomeGatewayToEthereum", { + from: deployer, + contract: "HomeGateway", + args: [deployer, klerosCore.address, fastBridgeSender.address, foreignGateway.address, foreignChainId], + log: true, + }); // nonce+1 + + const fastSender = await hre.ethers + .getContractAt("FastBridgeSenderToEthereum", fastBridgeSender.address) + .then((contract) => contract.fastBridgeSender()); + + if (fastSender === ethers.constants.AddressZero) { + await execute( + "FastBridgeSenderToEthereum", + { from: deployer, log: true }, + "changeFastSender", + homeGateway.address + ); + } + }; + + // ---------------------------------------------------------------------------------------------- + if (chainId === 31337) { + await hardhatDeployer(); + } else { + await liveDeployer(); } }; diff --git a/contracts/deploy/04-foreign-chain-test.ts b/contracts/deploy/04-foreign-chain-test.ts new file mode 100644 index 000000000..fd7739085 --- /dev/null +++ b/contracts/deploy/04-foreign-chain-test.ts @@ -0,0 +1,108 @@ +import { parseEther } from "ethers/lib/utils"; +import { HardhatRuntimeEnvironment } from "hardhat/types"; +import { DeployFunction } from "hardhat-deploy/types"; +import { ethers } from "hardhat"; +import getContractAddress from "../deploy-helpers/getContractAddress"; + +enum ForeignChains { + ETHEREUM_RINKEBY = 4, + HARDHAT = 31337, +} +const deployForeignGateway: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const { deployments, getNamedAccounts, getChainId, config } = hre; + const { deploy } = deployments; + const { hexZeroPad } = hre.ethers.utils; + + const deployer = (await getNamedAccounts()).deployer; + const chainId = Number(await getChainId()); + console.log("deploying to chainId %s with deployer %s", chainId, deployer); + + const deposit = parseEther("0.1") + const epochPeriod = 120 // 2 min + const homeChainId = 421611 // arbitrum testnet + const arbInbox = "0x578BAde599406A8fE3d24Fd7f7211c0911F5B29e" // https://developer.offchainlabs.com/docs/useful_addresses + const genesis = 1652709415 // sample genesis time + + // TODO: use deterministic deployments + let nonce = await ethers.provider.getTransactionCount(deployer)+7; + + const fastBridgeSenderToEthereumAddress = getContractAddress(deployer, nonce ); + console.log("calculated future fastBridgeSenderToEthereum address for nonce %d: %s", nonce, fastBridgeSenderToEthereumAddress); + const fastBridgeSenderToGnosisAddress = getContractAddress(deployer, nonce + 1); + console.log("calculated future fastBridgeSenderToGnosis address for nonce %d: %s", nonce + 1, fastBridgeSenderToGnosisAddress); + const homeGatewayOnEthereumAddress = getContractAddress(deployer, nonce + 2); + console.log("calculated future HomeGatewayOnEthereum address for nonce %d: %s", nonce + 2, homeGatewayOnEthereumAddress); + const homeGatewayOnGnosisAddress = getContractAddress(deployer, nonce + 3); + console.log("calculated future HomeGatewayOnGnosis address for nonce %d: %s", nonce + 3, homeGatewayOnGnosisAddress); + const fastBridgeReceiverOnEthereum = await deploy("FastBridgeReceiverOnEthereum", { + from: deployer, + args: [ + deposit, + epochPeriod, + genesis, + arbInbox, // should be Arbitrum Inbox + fastBridgeSenderToEthereumAddress, + ], + log: true, + }); + + const ForeignGatewayOnEthereum = await deploy("ForeignGatewayMockOnEthereum", { + from: deployer, + contract: "ForeignGatewayMock", + args: [ + fastBridgeReceiverOnEthereum.address, + homeGatewayOnEthereumAddress, + homeChainId + ], + log: true, + }); + + const mockAMB = await deploy("MockAMB", { + from: deployer, + log: true, + }); // nonce+0 + + nonce = await ethers.provider.getTransactionCount(deployer) + 1; + const fastBridgeReceiverOnGnosisChainAddress = getContractAddress(deployer, nonce); + console.log("calculated future HomeGatewayOnEthereum address for nonce %d: %s", nonce, homeGatewayOnEthereumAddress); + + const safeBridgeRouter = await deploy("SafeBridgeRouter", { + from: deployer, + args: [ + arbInbox, + mockAMB.address, + fastBridgeSenderToGnosisAddress, + fastBridgeReceiverOnGnosisChainAddress + ], + log: true, + }); // nonce+0 + + const fastBridgeReceiverOnGnosisChain = await deploy("FastBridgeReceiverOnGnosis", { + from: deployer, + args: [ + deposit, + epochPeriod, + genesis, + mockAMB.address, // should be Arbitrum Inbox + safeBridgeRouter.address, + ], + log: true, + }); + + const ForeignGatewayOnGnosis = await deploy("ForeignGatewayMockOnGnosis", { + from: deployer, + contract: "ForeignGatewayMock", + args: [ + fastBridgeReceiverOnGnosisChain.address, + homeGatewayOnGnosisAddress, + homeChainId + ], + log: true, + }); +}; +deployForeignGateway.tags = ["BridgeTest"]; +deployForeignGateway.skip = async ({ getChainId }) => { + const chainId = Number(await getChainId()); + return !ForeignChains[chainId]; +}; +export default deployForeignGateway; diff --git a/contracts/deploy/05-home-chain-test.ts b/contracts/deploy/05-home-chain-test.ts new file mode 100644 index 000000000..92aff3167 --- /dev/null +++ b/contracts/deploy/05-home-chain-test.ts @@ -0,0 +1,65 @@ +import { HardhatRuntimeEnvironment } from "hardhat/types"; +import { DeployFunction } from "hardhat-deploy/types"; +import { Address } from "ethereumjs-util"; +import { ethers } from "hardhat"; + +const HOME_CHAIN_ID = 421611; // ArbRinkeby + +const deployHomeGateway: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const { deployments, getNamedAccounts, getChainId } = hre; + const { deploy, execute } = 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); + // TODO: use deterministic deployments + const fastBridgeReceiverOnEthereum = await hre.companionNetworks.foreign.deployments.get("FastBridgeReceiverOnEthereum"); + const fastBridgeReceiverOnGnosis = await hre.companionNetworks.foreign.deployments.get("FastBridgeReceiverOnGnosis"); + + const genesis = 1652709415; // sample genesis time + const epochPeriod = 120; + + const fastBridgeSenderToEthereum = await deploy("FastBridgeSenderToEthereum", { + from: deployer, + contract: "FastBridgeSender", + args: [epochPeriod, genesis, fastBridgeReceiverOnEthereum.address], + log: true, + }); + + const fastBridgeSenderToGnosis = await deploy("FastBridgeSenderToGnosis", { + from: deployer, + contract: "FastBridgeSender", + args: [epochPeriod, genesis, fastBridgeReceiverOnGnosis.address], + log: true, + }); + + const foreignGatewayOnEthereum = await hre.companionNetworks.foreign.deployments.get("ForeignGatewayMockOnEthereum"); + const foreignGatewayOnGnosis = await hre.companionNetworks.foreign.deployments.get("ForeignGatewayMockOnGnosis"); + const foreignChainId = 4; + const homeGatewayToEthereum = await deploy("homeGatewayMockToEthereum", { + from: deployer, + contract: "HomeGatewayMock", + args: [ + fastBridgeSenderToEthereum.address, + foreignGatewayOnEthereum.address, + foreignChainId + ], + log: true, + }); // nonce+1 + const homeGatewayToGnosis = await deploy("homeGatewayMockToGnosis", { + from: deployer, + contract: "HomeGatewayMock", + args: [ + fastBridgeSenderToGnosis.address, + foreignGatewayOnGnosis.address, + foreignChainId + ], + log: true, + }); // nonce+1 +}; + +deployHomeGateway.tags = ["BridgeTest"]; +deployHomeGateway.skip = async ({ getChainId }) => (HOME_CHAIN_ID != Number(await getChainId())); + +export default deployHomeGateway; diff --git a/contracts/hardhat.config.ts b/contracts/hardhat.config.ts index 6288be0ec..fa0b571af 100644 --- a/contracts/hardhat.config.ts +++ b/contracts/hardhat.config.ts @@ -11,7 +11,6 @@ import "hardhat-deploy-ethers"; import "hardhat-watcher"; import "hardhat-docgen"; import "hardhat-contract-sizer"; - dotenv.config(); const config: HardhatUserConfig = { @@ -102,6 +101,17 @@ const config: HardhatUserConfig = { }, }, // Foreign chain --------------------------------------------------------------------------------- + gnosischain: { + chainId: 100, + url: `http://rpc.gnosischain.com/`, + accounts: process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [], + live: true, + saveDeployments: true, + tags: ["staging", "foreign", "layer2"], + companionNetworks: { + home: "arbitrum", + }, + }, rinkeby: { chainId: 4, url: `https://rinkeby.infura.io/v3/${process.env.INFURA_API_KEY}`, @@ -163,6 +173,13 @@ const config: HardhatUserConfig = { ], files: ["./test/**/*", "./src/**/*"], }, + testBridge: { + tasks: [ + { command: "compile", params: { quiet: true } }, + { command: "test", params: { noCompile: true, testFiles: ["./test/bridge/index.ts"] } }, + ], + files: ["./test/**/*", "./src/**/*"], + }, }, docgen: { path: './docs', diff --git a/contracts/package.json b/contracts/package.json index 71f8fc435..6f6ba50ad 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -14,7 +14,8 @@ "build": "hardhat compile", "clean": "hardhat clean", "deploy": "hardhat deploy", - "deploy:staging": "run-s \"deploy --network rinkeby {@}\" \"deploy --network arbitrumRinkeby {@}\" --", + "deploy:staging": "run-s \"deploy --network rinkeby --tags ForeignChain {@}\" \"deploy --network arbitrumRinkeby --tags HomeChain {@}\" --", + "deploy:staging-bridge-only": "run-s \"deploy --network rinkeby --tags BridgeTest{@}\" \"deploy --network arbitrumRinkeby --tags BridgeTest {@}\" --", "test": "hardhat test", "watch": "hardhat watch", "docgen": "hardhat docgen" diff --git a/contracts/scripts/generateDeploymentsMarkdownLite.sh b/contracts/scripts/generateDeploymentsMarkdownLite.sh new file mode 100755 index 000000000..595597b4f --- /dev/null +++ b/contracts/scripts/generateDeploymentsMarkdownLite.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +function generate() { #deploymentDir #explorerUrl + deploymentDir=$1 + explorerUrl=$2 + for f in $(ls -1 $deploymentDir/*.json); do + contractName=$(basename $f .json) + address=$(cat $f | jq -r .address) + echo "- [$contractName]($explorerUrl$address)" + done +} + +echo "### Rinkeby" +echo +echo "- [PNK](https://rinkeby.etherscan.io/token/0x14aba1fa8a31a8649e8098ad067b739cc5708f30)" +generate "$SCRIPT_DIR/../deployments/rinkeby" "" +echo +echo "### Arbitrum Rinkeby" +echo +echo "- [PNK](https://testnet.arbiscan.io/token/0x364530164a2338cdba211f72c1438eb811b5c639)" +generate "$SCRIPT_DIR/../deployments/arbitrumRinkeby" "" \ No newline at end of file diff --git a/contracts/src/bridge/FastBridgeReceiverBase.sol b/contracts/src/bridge/FastBridgeReceiverBase.sol new file mode 100644 index 000000000..5062de5da --- /dev/null +++ b/contracts/src/bridge/FastBridgeReceiverBase.sol @@ -0,0 +1,251 @@ +// SPDX-License-Identifier: MIT + +/** + * @authors: [@jaybuidl, @shalzz, @hrishibhat, @shotaronowhere] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ + +pragma solidity ^0.8.0; + +import "./interfaces/IFastBridgeReceiver.sol"; +import "./interfaces/ISafeBridgeReceiver.sol"; +import "./merkle/MerkleProof.sol"; + +/** + * Fast Receiver Base + * Counterpart of `FastSenderBase` + */ +abstract contract FastBridgeReceiverBase is MerkleProof, IFastBridgeReceiver, ISafeBridgeReceiver { + // ************************************* // + // * Enums / Structs * // + // ************************************* // + + struct Claim { + bytes32 batchMerkleRoot; + address bridger; + bool honest; + } + + struct Challenge { + address challenger; + bool honest; + } + + // ************************************* // + // * Storage * // + // ************************************* // + + uint256 public immutable deposit; // The deposit required to submit a claim or challenge + uint256 public immutable genesis; // Marks the beginning of the first epoch. + uint256 public immutable override epochPeriod; // Epochs mark the period between potential batches of messages. + + address public immutable safeRouter; // The address of the Safe Router on the connecting chain. + + mapping(uint256 => bytes32) public fastInbox; // epoch => validated batch merkle root(optimistically, or challenged and verified with the safe bridge) + mapping(uint256 => Claim) public claims; // epoch => claim + mapping(uint256 => Challenge) public challenges; // epoch => challenge + mapping(uint256 => mapping(uint256 => bytes32)) public relayed; // epoch => packed replay bitmap + + /** + * @dev Constructor. + * @param _deposit The deposit amount to submit a claim in wei. + * @param _epochPeriod The duration of the period allowing to challenge a claim. + * @param _genesis The genesis time to synchronize epochs. + * @param _safeRouter The address of the Safe Router on Ethereum. + */ + constructor( + uint256 _deposit, + uint256 _epochPeriod, + uint256 _genesis, + address _safeRouter + ) { + deposit = _deposit; + epochPeriod = _epochPeriod; + genesis = _genesis; + safeRouter = _safeRouter; + } + + // ************************************* // + // * State Modifiers * // + // ************************************* // + + /** + * @dev Submit a claim about the `_batchMerkleRoot` for the last completed epoch from the Fast Bridge and submit a deposit. The `_batchMerkleRoot` should match the one on the sending side otherwise the sender will lose his deposit. + * @param _batchMerkleRoot The batch merkle root claimed for the last completed epoch. + */ + function claim(bytes32 _batchMerkleRoot) external payable override { + require(msg.value >= deposit, "Insufficient claim deposit."); + require(_batchMerkleRoot != bytes32(0), "Invalid claim."); + + uint256 epochCount = (block.timestamp - genesis) / epochPeriod; + uint256 epochClaim = epochCount - 1; // Can only claim last completed epoch. + uint256 claimDeadline = genesis + epochCount * epochPeriod + epochPeriod / 2; + + require(block.timestamp < claimDeadline, "Claim period expired."); + require(claims[epochClaim].bridger == address(0), "Claim already made for most recent finalized epoch."); + + claims[epochClaim] = Claim({batchMerkleRoot: _batchMerkleRoot, bridger: msg.sender, honest: false}); + + emit ClaimReceived(epochClaim, _batchMerkleRoot); + } + + /** + * @dev Submit a challenge for the claim of the current epoch's Fast Bridge batch merkleroot state and submit a deposit. The `batchMerkleRoot` in the claim already made for the last finalized epoch should be different from the one on the sending side, otherwise the sender will lose his deposit. + */ + function challenge() external payable override { + require(msg.value >= deposit, "Not enough claim deposit"); + + // can only challenge the only active claim, about the previous epoch + uint256 epochChallenge = (block.timestamp - genesis) / epochPeriod - 1; + require(claims[epochChallenge].bridger != address(0), "No claim to challenge."); + + challenges[epochChallenge] = Challenge({challenger: msg.sender, honest: false}); + + emit ClaimChallenged(epochChallenge); + } + + /** + * @dev Resolves the optimistic claim for '_epoch'. + * @param _epoch The epoch of the optimistic claim. + */ + function verify(uint256 _epoch) public { + uint256 epochCount = (block.timestamp - genesis) / epochPeriod; + + require(epochCount > _epoch + 1, "Challenge period for epoch has not elapsed."); + require(fastInbox[_epoch] == bytes32(0), "Epoch already verified."); + + Claim storage claim = claims[_epoch]; + require(claim.bridger != address(0), "Invalid epoch, no claim to verify."); + + if (challenges[_epoch].challenger == address(0)) { + // optimistic happy path + claim.honest = true; + fastInbox[_epoch] = claim.batchMerkleRoot; + } + } + + /** + * @dev Verifies merkle proof for the given message and associated nonce for the epoch and relays the message. + * @param _epoch The epoch in which the message was batched by the bridge. + * @param _proof The merkle proof to prove the membership of the message and nonce in the merkle tree for the epoch. + * @param _message The data on the cross-domain chain for the message. + * @param _nonce The nonce (index in the merkle tree) to avoid replay. + */ + function verifyAndRelayMessage( + uint256 _epoch, + bytes32[] calldata _proof, + bytes calldata _message, + uint256 _nonce + ) external override { + bytes32 batchMerkleRoot = fastInbox[_epoch]; + require(batchMerkleRoot != bytes32(0), "Invalid epoch."); + + uint256 index = _nonce / 256; + uint256 offset = _nonce - index * 256; + + bytes32 replay = relayed[_epoch][index]; + require(((replay >> offset) & bytes32(uint256(1))) == 0, "Message already relayed"); + relayed[_epoch][index] = replay | bytes32(1 << offset); + + // Claim assessment if any + bytes32 messageHash = sha256(abi.encodePacked(_message, _nonce)); + + require(validateProof(_proof, messageHash, batchMerkleRoot) == true, "Invalid proof."); + require(_relay(_message), "Failed to call contract"); // Checks-Effects-Interaction + } + + /** + * Note: Access restricted to the Safe Bridge. + * @dev Resolves any challenge of the optimistic claim for '_epoch'. + * @param _epoch The epoch to verify. + * @param _batchMerkleRoot The true batch merkle root for the epoch. + */ + function verifySafe(uint256 _epoch, bytes32 _batchMerkleRoot) external override onlyFromSafeBridge { + require(isSentBySafeBridge(), "Access not allowed: SafeBridgeSender only."); + + fastInbox[_epoch] = _batchMerkleRoot; + + if (_batchMerkleRoot == claims[_epoch].batchMerkleRoot) { + claims[_epoch].honest = true; + } else { + challenges[_epoch].honest = true; + } + } + + /** + * @dev Sends the deposit back to the Bridger if their claim is not successfully challenged. Includes a portion of the Challenger's deposit if unsuccessfully challenged. + * @param _epoch The epoch associated with the claim deposit to withraw. + */ + function withdrawClaimDeposit(uint256 _epoch) external override { + Claim memory claim = claims[_epoch]; + Challenge memory challenge = challenges[_epoch]; + require(claim.bridger != address(0), "Claim does not exist"); + require(claim.honest == true, "Claim not verified."); + + uint256 amount = deposit; + if (challenge.challenger != address(0)) amount = (deposit * 3) / 2; // half burnt + + delete claims[_epoch]; + delete challenges[_epoch]; + payable(claim.bridger).send(amount); // Use of send to prevent reverting fallback. User is responsibility for accepting ETH. + // Checks-Effects-Interaction + } + + /** + * @dev Sends the deposit back to the Challenger if their challenge is successful. Includes a portion of the Bridger's deposit. + * @param _epoch The epoch associated with the challenge deposit to withraw. + */ + function withdrawChallengeDeposit(uint256 _epoch) external override { + Challenge memory challenge = challenges[_epoch]; + require(challenge.challenger != address(0), "Claim does not exist"); + require(challenge.honest == true, "Claim not verified: deposit forfeited"); + + uint256 amount = (deposit * 3) / 2; + + delete claims[_epoch]; + delete challenges[_epoch]; + payable(challenge.challenger).send(amount); // Use of send to prevent reverting fallback. User is responsibility for accepting ETH. + // Checks-Effects-Interaction + } + + // ************************************* // + // * Public Views * // + // ************************************* // + + /** + * Returns the `start` and `end` time of challenge period for this `_epoch`. + * start The start time of the challenge period. + * end The end time of the challenge period. + */ + function challengePeriod() external view override returns (uint256 start, uint256 end) { + // start begins latest after the claim deadline expiry + // however can begin as soon as a claim is made + // can only challenge the only active claim, about the previous epoch + uint256 epochChallenge = (block.timestamp - genesis) / epochPeriod - 1; + start = genesis + epochChallenge * epochPeriod + epochPeriod / 2; + end = start + epochPeriod / 2; + return (start, end); + } + + /** + * Returns the `start` and `end` time of challenge period for this `_epoch`. + * start The start time of the challenge period. + * end The end time of the challenge period. + */ + function epochCount() external view returns (uint256 _epochCount) { + _epochCount = (block.timestamp - genesis) / epochPeriod; + } + + // ************************ // + // * Internal * // + // ************************ // + + function _relay(bytes calldata _messageData) internal returns (bool success) { + // Decode the receiver address from the data encoded by the IFastBridgeSender + (address receiver, bytes memory data) = abi.decode(_messageData, (address, bytes)); + (success, ) = address(receiver).call(data); + } +} diff --git a/contracts/src/bridge/FastBridgeReceiverOnEthereum.sol b/contracts/src/bridge/FastBridgeReceiverOnEthereum.sol index e70cd9f07..8aa844d28 100644 --- a/contracts/src/bridge/FastBridgeReceiverOnEthereum.sol +++ b/contracts/src/bridge/FastBridgeReceiverOnEthereum.sol @@ -10,252 +10,45 @@ pragma solidity ^0.8.0; -import "./SafeBridgeReceiverOnEthereum.sol"; -import "./interfaces/IFastBridgeReceiver.sol"; +import "./FastBridgeReceiverBase.sol"; +import "./interfaces/arbitrum/IInbox.sol"; +import "./interfaces/arbitrum/IOutbox.sol"; /** * Fast Bridge Receiver on Ethereum from Arbitrum * Counterpart of `FastBridgeSenderToEthereum` */ -contract FastBridgeReceiverOnEthereum is SafeBridgeReceiverOnEthereum, IFastBridgeReceiver { - // ************************************* // - // * Enums / Structs * // - // ************************************* // - - struct Claim { - bytes32 messageHash; - address bridger; - uint256 claimedAt; - uint256 claimDeposit; - bool verified; - } - - struct Challenge { - address challenger; - uint256 challengedAt; - uint256 challengeDeposit; - } - - struct Ticket { - Claim claim; - Challenge challenge; - bool relayed; - } - +contract FastBridgeReceiverOnEthereum is FastBridgeReceiverBase { // ************************************* // // * Storage * // // ************************************* // - uint256 public constant ONE_BASIS_POINT = 1e4; // One basis point, for scaling. - uint256 public override claimDeposit; // The deposit required to submit a claim. - uint256 public override challengeDeposit; // The deposit required to submit a challenge. - uint256 public override challengeDuration; // The duration of the period allowing to challenge a claim. - uint256 public override alpha; // Basis point of claim or challenge deposit that are lost when dishonest. - mapping(uint256 => Ticket) public tickets; // The tickets by ticketID. + IInbox public immutable inbox; // The address of the Arbitrum Inbox contract. /** * @dev Constructor. - * @param _governor The governor's address. - * @param _safeBridgeSender The address of the Safe Bridge sender on Arbitrum. - * @param _inbox The address of the Arbitrum Inbox contract. - * @param _claimDeposit The deposit amount to submit a claim in wei. - * @param _challengeDeposit The deposit amount to submit a challenge in wei. - * @param _challengeDuration The duration of the period allowing to challenge a claim. - * @param _alpha Basis point of claim or challenge deposit that are lost when dishonest. + * @param _deposit The deposit amount to submit a claim in wei. + * @param _epochPeriod The duration of the period allowing to challenge a claim. + * @param _genesis The genesis time to synchronize epochs. + * @param _inbox The address of the inbox contract on Ethereum. + * @param _safeRouter The address of the Safe Router on Ethereum. */ constructor( - address _governor, - address _safeBridgeSender, + uint256 _deposit, + uint256 _epochPeriod, + uint256 _genesis, address _inbox, - uint256 _claimDeposit, - uint256 _challengeDeposit, - uint256 _challengeDuration, - uint256 _alpha - ) SafeBridgeReceiverOnEthereum(_governor, _safeBridgeSender, _inbox) { - claimDeposit = _claimDeposit; - challengeDeposit = _challengeDeposit; - challengeDuration = _challengeDuration; - alpha = _alpha; + address _safeRouter + ) FastBridgeReceiverBase(_deposit, _epochPeriod, _genesis, _safeRouter) { + inbox = IInbox(_inbox); } // ************************************* // - // * State Modifiers * // + // * Views * // // ************************************* // - /** - * @dev Submit a claim about the `messageHash` for a particular Fast Bridge `ticketID` and submit a deposit. The `messageHash` should match the one on the sending side otherwise the sender will lose his deposit. - * @param _ticketID The ticket identifier referring to a message going through the bridge. - * @param _messageHash The hash claimed for the ticket. - */ - function claim(uint256 _ticketID, bytes32 _messageHash) external payable override { - Ticket storage ticket = tickets[_ticketID]; - require(ticket.claim.bridger == address(0), "Claim already made"); - require(ticket.relayed == false, "Claim already relayed"); // already relayed via verifyAndRelaySafe() without claim. - require(msg.value >= claimDeposit, "Not enough claim deposit"); - - ticket.claim = Claim({ - messageHash: _messageHash, - bridger: msg.sender, - claimedAt: block.timestamp, - claimDeposit: msg.value, - verified: false - }); - - emit ClaimReceived(_ticketID, _messageHash, block.timestamp); - } - - /** - * @dev Submit a challenge for a particular Fast Bridge `ticketID` and submit a deposit. The `messageHash` in the claim already made for this `ticketID` should be different from the one on the sending side, otherwise the sender will lose his deposit. - * @param _ticketID The ticket identifier referring to a message going through the bridge. - */ - function challenge(uint256 _ticketID) external payable override { - Ticket storage ticket = tickets[_ticketID]; - require(ticket.claim.bridger != address(0), "Claim does not exist"); - require(block.timestamp - ticket.claim.claimedAt < challengeDuration, "Challenge period over"); - require(ticket.challenge.challenger == address(0), "Claim already challenged"); - require(msg.value >= challengeDeposit, "Not enough challenge deposit"); - - ticket.challenge = Challenge({ - challenger: msg.sender, - challengedAt: block.timestamp, - challengeDeposit: msg.value - }); - - emit ClaimChallenged(_ticketID, block.timestamp); - } - - /** - * @dev Relay the message for this `ticketID` if the challenge period has passed and the claim is unchallenged. The hash computed over `messageData` and the other parameters must match the hash provided by the claim. - * @param _ticketID The ticket identifier referring to a message going through the bridge. - * @param _blockNumber The block number on the cross-domain chain when the message with this ticketID has been sent. - * @param _messageData The data on the cross-domain chain for the message sent with this ticketID. - */ - function verifyAndRelay( - uint256 _ticketID, - uint256 _blockNumber, - bytes calldata _messageData - ) external override { - Ticket storage ticket = tickets[_ticketID]; - require(ticket.claim.bridger != address(0), "Claim does not exist"); - require( - ticket.claim.messageHash == keccak256(abi.encode(_ticketID, _blockNumber, _messageData)), - "Invalid hash" - ); - require(ticket.claim.claimedAt + challengeDuration < block.timestamp, "Challenge period not over"); - require(ticket.challenge.challenger == address(0), "Claim is challenged"); - require(ticket.relayed == false, "Message already relayed"); - - ticket.claim.verified = true; - ticket.relayed = true; - require(_relay(_messageData), "Failed to call contract"); // Checks-Effects-Interaction - } - - /** - * Note: Access restricted to the Safe Bridge. - * @dev Relay the message for this `ticketID` as provided by the Safe Bridge. Resolve a challenged claim for this `ticketID` if any. - * @param _ticketID The ticket identifier referring to a message going through the bridge. - * @param _blockNumber The block number on the cross-domain chain when the message with this ticketID has been sent. - * @param _messageData The data on the cross-domain chain for the message sent with this ticketID. - */ - function verifyAndRelaySafe( - uint256 _ticketID, - uint256 _blockNumber, - bytes calldata _messageData - ) external override { - require(isSentBySafeBridge(), "Access not allowed: SafeBridgeSender only."); - - Ticket storage ticket = tickets[_ticketID]; - require(ticket.relayed == false, "Message already relayed"); - - // Claim assessment if any - bytes32 messageHash = keccak256(abi.encode(_ticketID, _blockNumber, _messageData)); - if (ticket.claim.bridger != address(0) && ticket.claim.messageHash == messageHash) { - ticket.claim.verified = true; - } - - ticket.relayed = true; - require(_relay(_messageData), "Failed to call contract"); // Checks-Effects-Interaction - } - - /** - * @dev Sends the deposit back to the Bridger if his claim is not successfully challenged. Includes a portion of the Challenger's deposit if unsuccessfully challenged. - * @param _ticketID The ticket identifier referring to a message going through the bridge. - */ - function withdrawClaimDeposit(uint256 _ticketID) external override { - Ticket storage ticket = tickets[_ticketID]; - require(ticket.relayed == true, "Message not relayed yet"); - require(ticket.claim.bridger != address(0), "Claim does not exist"); - require(ticket.claim.verified == true, "Claim not verified: deposit forfeited"); - - uint256 amount = ticket.claim.claimDeposit + (ticket.challenge.challengeDeposit * alpha) / ONE_BASIS_POINT; - ticket.claim.claimDeposit = 0; - ticket.challenge.challengeDeposit = 0; - payable(ticket.claim.bridger).send(amount); // Use of send to prevent reverting fallback. User is responsibility for accepting ETH. - // Checks-Effects-Interaction - } - - /** - * @dev Sends the deposit back to the Challenger if his challenge is successful. Includes a portion of the Bridger's deposit. - * @param _ticketID The ticket identifier referring to a message going through the bridge. - */ - function withdrawChallengeDeposit(uint256 _ticketID) external override { - Ticket storage ticket = tickets[_ticketID]; - require(ticket.relayed == true, "Message not relayed"); - require(ticket.challenge.challenger != address(0), "Challenge does not exist"); - require(ticket.claim.verified == false, "Claim verified: deposit forfeited"); - - uint256 amount = ticket.challenge.challengeDeposit + (ticket.claim.claimDeposit * alpha) / ONE_BASIS_POINT; - ticket.claim.claimDeposit = 0; - ticket.challenge.challengeDeposit = 0; - payable(ticket.challenge.challenger).send(amount); // Use of send to prevent reverting fallback. User is responsibility for accepting ETH. - // Checks-Effects-Interaction - } - - // ************************************* // - // * Public Views * // - // ************************************* // - - /** - * @dev Returns the `start` and `end` time of challenge period for this `ticketID`. - * @return start The start time of the challenge period. - * @return end The end time of the challenge period. - */ - function challengePeriod(uint256 _ticketID) external view override returns (uint256 start, uint256 end) { - Ticket storage ticket = tickets[_ticketID]; - require(ticket.claim.bridger != address(0), "Claim does not exist"); - - start = ticket.claim.claimedAt; - end = start + challengeDuration; - return (start, end); - } - - // ************************ // - // * Governance * // - // ************************ // - - function changeClaimDeposit(uint256 _claimDeposit) external onlyByGovernor { - claimDeposit = _claimDeposit; - } - - function changeChallengeDeposit(uint256 _challengeDeposit) external onlyByGovernor { - challengeDeposit = _challengeDeposit; - } - - function changeChallengePeriodDuration(uint256 _challengeDuration) external onlyByGovernor { - challengeDuration = _challengeDuration; - } - - function changeAlpha(uint256 _alpha) external onlyByGovernor { - alpha = _alpha; - } - - // ************************ // - // * Internal * // - // ************************ // - - function _relay(bytes calldata _messageData) internal returns (bool success) { - // Decode the receiver address from the data encoded by the IFastBridgeSender - (address receiver, bytes memory data) = abi.decode(_messageData, (address, bytes)); - (success, ) = address(receiver).call(data); + function isSentBySafeBridge() internal view override returns (bool) { + IOutbox outbox = IOutbox(inbox.bridge().activeOutbox()); + return outbox.l2ToL1Sender() == safeRouter; } } diff --git a/contracts/src/bridge/FastBridgeReceiverOnGnosis.sol b/contracts/src/bridge/FastBridgeReceiverOnGnosis.sol index ef8df9478..0f2a6524f 100644 --- a/contracts/src/bridge/FastBridgeReceiverOnGnosis.sol +++ b/contracts/src/bridge/FastBridgeReceiverOnGnosis.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT /** - * @authors: [@jaybuidl] + * @authors: [@jaybuidl, @shalzz, @hrishibhat, @shotaronowhere] * @reviewers: [] * @auditors: [] * @bounties: [] @@ -10,12 +10,43 @@ pragma solidity ^0.8.0; -import "./interfaces/IFastBridgeReceiver.sol"; +import "./FastBridgeReceiverBase.sol"; +import "./interfaces/gnosis-chain/IAMB.sol"; /** - * Fast Bridge Receiver on Gnosis from Arbitrum + * Fast Bridge Receiver on Ethereum from Arbitrum * Counterpart of `FastBridgeSenderToGnosis` */ -abstract contract FastBridgeReceiverOnGnosis is IFastBridgeReceiver { - // TODO in prealpha-3 +contract FastBridgeReceiverOnGnosis is FastBridgeReceiverBase { + // ************************************* // + // * Storage * // + // ************************************* // + + IAMB public immutable amb; // The address of the AMB contract on GC. + + /** + * @dev Constructor. + * @param _deposit The deposit amount to submit a claim in wei. + * @param _epochPeriod The duration of the period allowing to challenge a claim. + * @param _genesis The genesis time to synchronize epochs. + * @param _amb The the AMB contract on Gnosis Chain. + * @param _safeRouter The address of the Safe Bridge Router on Ethereum. + */ + constructor( + uint256 _deposit, + uint256 _epochPeriod, + uint256 _genesis, + address _amb, + address _safeRouter + ) FastBridgeReceiverBase(_deposit, _epochPeriod, _genesis, _safeRouter) { + amb = IAMB(_amb); + } + + // ************************************* // + // * Views * // + // ************************************* // + + function isSentBySafeBridge() internal view override returns (bool) { + return (msg.sender == address(amb)) && (amb.messageSender() == safeRouter); + } } diff --git a/contracts/src/bridge/FastBridgeSender.sol b/contracts/src/bridge/FastBridgeSender.sol new file mode 100644 index 000000000..1b62f5bce --- /dev/null +++ b/contracts/src/bridge/FastBridgeSender.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT + +/** + * @authors: [@jaybuidl, @shalzz, @hrishibhat, @shotaronowhere] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ + +pragma solidity ^0.8.0; + +import "./FastBridgeSenderBase.sol"; +import "./interfaces/arbitrum/IArbSys.sol"; + +/** + * Fast Bridge Sender to Ethereum from Arbitrum + * Counterpart of `FastBridgeReceiverOnEthereum` + */ +contract FastBridgeSender is FastBridgeSenderBase { + // ************************************* // + // * Events * // + // ************************************* // + + event L2ToL1TxCreated(uint256 indexed txID); + + // ************************************* // + // * Storage * // + // ************************************* // + + IArbSys public constant ARB_SYS = IArbSys(address(100)); + + /** + * @dev Constructor. + * @param _epochPeriod The immutable period between epochs. + * @param _genesis The immutable genesis state variable from the FastBridgeSeneder. + * @param _safeRouter The address of the Safe Router on Ethereum. + */ + constructor( + uint256 _epochPeriod, + uint256 _genesis, + address _safeRouter + ) FastBridgeSenderBase(_epochPeriod, _genesis, _safeRouter) {} + + /** + * @dev Sends the merkle root state for _epoch to Ethereum using the Safe Bridge, which relies on Arbitrum's canonical bridge. It is unnecessary during normal operations but essential only in case of challenge. + * @param _epoch The blocknumber of the batch + */ + function sendSafeFallback(uint256 _epoch) external payable override { + bytes32 batchMerkleRoot = fastOutbox[_epoch]; + + // Safe Bridge message envelope + bytes4 methodSelector = ISafeBridgeReceiver.verifySafe.selector; + bytes memory safeMessageData = abi.encodeWithSelector(methodSelector, _epoch, batchMerkleRoot); + + _sendSafe(safeRouter, safeMessageData); + } + + function _sendSafe(address _receiver, bytes memory _calldata) internal override returns (bytes32) { + uint256 txID = ARB_SYS.sendTxToL1(_receiver, _calldata); + + emit L2ToL1TxCreated(txID); + return bytes32(txID); + } +} diff --git a/contracts/src/bridge/FastBridgeSenderBase.sol b/contracts/src/bridge/FastBridgeSenderBase.sol new file mode 100644 index 000000000..df8b3d22b --- /dev/null +++ b/contracts/src/bridge/FastBridgeSenderBase.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MIT + +/** + * @authors: [@jaybuidl, @shotaronowhere] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ + +pragma solidity ^0.8.0; + +import "./merkle/MerkleTree.sol"; +import "./interfaces/IFastBridgeSender.sol"; +import "./interfaces/ISafeBridgeSender.sol"; +import "./interfaces/ISafeBridgeReceiver.sol"; + +/** + * Fast Bridge Sender Base + * Counterpart of `FastReceiverBase` + */ +abstract contract FastBridgeSenderBase is MerkleTree, IFastBridgeSender, ISafeBridgeSender { + // ************************************* // + // * Storage * // + // ************************************* // + + uint256 public immutable genesis; // Marks the beginning of epoch 0. + uint256 public immutable epochPeriod; // Epochs mark the period between potential batches of messages. + mapping(uint256 => bytes32) public fastOutbox; // epoch count => merkle root of batched messages + address public immutable safeRouter; + + // ************************************* // + // * Events * // + // ************************************* // + + /** + * The bridgers need to watch for these events and relay the + * batchMerkleRoot on the FastBridgeReceiver. + */ + event SendEpoch(uint256 indexed epoch, bytes32 indexed epochMerkleRoot); + + /** + * @dev Constructor. + * @param _epochPeriod The duration between epochs. + * @param _genesis The genesis time to synchronize epochs with the FastBridgeReceiver. + * @param _safeRouter The the Safe Bridge Router on Ethereum to the foreign chain. + */ + constructor( + uint256 _epochPeriod, + uint256 _genesis, + address _safeRouter + ) { + epochPeriod = _epochPeriod; + genesis = _genesis; + safeRouter = _safeRouter; + } + + // ************************************* // + // * State Modifiers * // + // ************************************* // + + /** + * @dev Sends an arbitrary message to Ethereum using the Fast Bridge. + * @param _receiver The address of the contract on Ethereum which receives the calldata. + * @param _functionSelector The function to call. + * @param _calldata The receiving domain encoded message data / function arguments. + */ + function sendFast( + address _receiver, + bytes4 _functionSelector, + bytes memory _calldata + ) external override { + bytes memory _fastMessage = abi.encodeWithSelector(_functionSelector, msg.sender, _calldata); + bytes32 fastMessageHash = sha256(abi.encode(_fastMessage, batchSize)); + appendMessage(fastMessageHash); // add message to merkle tree + emit MessageReceived(_receiver, _fastMessage, batchSize); + } + + /** + * @dev Sends an arbitrary message to Ethereum using the Fast Bridge. + * @param _receiver The address of the contract on Ethereum which receives the calldata. + * @param _functionSelector The function to call. + */ + function sendFast(address _receiver, bytes4 _functionSelector) external override { + bytes memory _fastMessage = abi.encodeWithSelector(_functionSelector, msg.sender); + bytes32 fastMessageHash = sha256(abi.encode(_fastMessage, batchSize)); + appendMessage(fastMessageHash); // add message to merkle tree + emit MessageReceived(_receiver, _fastMessage, batchSize); + } + + /** + * Sends a batch of arbitrary message from one domain to another + * via the fast bridge mechanism. + */ + function sendEpoch() external { + uint256 epochFinalized = (block.timestamp - genesis) / epochPeriod; + require(fastOutbox[epochFinalized] == 0, "Batch already sent for most recent finalized epoch."); + require(batchSize > 0, "No messages to send."); + + // set merkle root in outbox and reset merkle tree + bytes32 epochMerkleRoot = getMerkleRootAndReset(); + fastOutbox[epochFinalized] = epochMerkleRoot; + + emit SendEpoch(epochFinalized, epochMerkleRoot); + } +} diff --git a/contracts/src/bridge/FastBridgeSenderToEthereum.sol b/contracts/src/bridge/FastBridgeSenderToEthereum.sol deleted file mode 100644 index 1f879268e..000000000 --- a/contracts/src/bridge/FastBridgeSenderToEthereum.sol +++ /dev/null @@ -1,145 +0,0 @@ -// SPDX-License-Identifier: MIT - -/** - * @authors: [@jaybuidl, @shalzz, @hrishibhat, @shotaronowhere] - * @reviewers: [] - * @auditors: [] - * @bounties: [] - * @deployments: [] - */ - -pragma solidity ^0.8.0; - -import "./SafeBridgeSenderToEthereum.sol"; -import "./interfaces/IFastBridgeSender.sol"; -import "./interfaces/IFastBridgeReceiver.sol"; - -/** - * Fast Bridge Sender to Ethereum from Arbitrum - * Counterpart of `FastBridgeReceiverOnEthereum` - */ -contract FastBridgeSenderToEthereum is SafeBridgeSenderToEthereum, IFastBridgeSender { - // ************************************* // - // * Enums / Structs * // - // ************************************* // - - struct Ticket { - bytes32 messageHash; - uint256 blockNumber; - bool sentSafe; - } - - // ************************************* // - // * Storage * // - // ************************************* // - - address public governor; // The governor of the contract. - IFastBridgeReceiver public fastBridgeReceiver; // The address of the Fast Bridge on Ethereum. - address public fastBridgeSender; // The address of the Fast Bridge sender on Arbitrum, generally the Home Gateway. - uint256 public currentTicketID = 1; // Zero means not set, start at 1. - mapping(uint256 => Ticket) public tickets; // The tickets by ticketID. - - // ************************************* // - // * Function Modifiers * // - // ************************************* // - - modifier onlyByGovernor() { - require(governor == msg.sender, "Access not allowed: Governor only."); - _; - } - - /** - * @dev Constructor. - * @param _governor The governor's address. - * @param _fastBridgeReceiver The address of the Fast Bridge on Ethereum. - * @param _fastBridgeSender The address of the Fast Bridge sender on Arbitrum, generally the Home Gateway. - */ - constructor( - address _governor, - IFastBridgeReceiver _fastBridgeReceiver, - address _fastBridgeSender - ) SafeBridgeSenderToEthereum() { - governor = _governor; - fastBridgeReceiver = _fastBridgeReceiver; - fastBridgeSender = _fastBridgeSender; - } - - // ************************************* // - // * State Modifiers * // - // ************************************* // - - /** - * Note: Access restricted to the `fastSender`, generally the Gateway. - * @dev Sends an arbitrary message to Ethereum using the Fast Bridge. - * @param _receiver The address of the contract on Ethereum which receives the calldata. - * @param _calldata The receiving domain encoded message data. - * @return ticketID The identifier to provide to sendSafeFallback(). - */ - function sendFast(address _receiver, bytes memory _calldata) external override returns (uint256 ticketID) { - require(msg.sender == fastBridgeSender, "Access not allowed: Fast Sender only."); - - ticketID = currentTicketID++; - - (bytes32 messageHash, bytes memory messageData) = _encode(ticketID, block.number, _receiver, _calldata); - emit OutgoingMessage(ticketID, block.number, _receiver, messageHash, messageData); - - tickets[ticketID] = Ticket({messageHash: messageHash, blockNumber: block.number, sentSafe: false}); - } - - /** - * @dev Sends an arbitrary message to Ethereum using the Safe Bridge, which relies on Arbitrum's canonical bridge. It is unnecessary during normal operations but essential only in case of challenge. - * @param _ticketID The ticketID as returned by `sendFast()`. - * @param _receiver The address of the contract on Ethereum which receives the calldata. - * @param _calldata The receiving domain encoded message data. - */ - function sendSafeFallback( - uint256 _ticketID, - address _receiver, - bytes memory _calldata - ) external payable override { - // TODO: check if keeping _calldata in storage in sendFast() is cheaper than passing it again as a parameter here - Ticket storage ticket = tickets[_ticketID]; - require(ticket.messageHash != 0, "Ticket does not exist."); - require(ticket.sentSafe == false, "Ticket already sent safely."); - - (bytes32 messageHash, bytes memory messageData) = _encode(_ticketID, ticket.blockNumber, _receiver, _calldata); - require(ticket.messageHash == messageHash, "Invalid message for ticketID."); - - // Safe Bridge message envelope - bytes4 methodSelector = IFastBridgeReceiver.verifyAndRelaySafe.selector; - bytes memory safeMessageData = abi.encodeWithSelector( - methodSelector, - _ticketID, - ticket.blockNumber, - messageData - ); - - // TODO: how much ETH should be provided for bridging? add an ISafeBridgeSender.bridgingCost() if needed - _sendSafe(address(fastBridgeReceiver), safeMessageData); - } - - // ************************ // - // * Governance * // - // ************************ // - - function changeFastSender(address _fastBridgeSender) external onlyByGovernor { - fastBridgeSender = _fastBridgeSender; - } - - // ************************ // - // * Internal * // - // ************************ // - - function _encode( - uint256 _ticketID, - uint256 _blockNumber, - address _receiver, - bytes memory _calldata - ) internal pure returns (bytes32 messageHash, bytes memory messageData) { - // Encode the receiver address with the function signature + arguments i.e calldata - messageData = abi.encode(_receiver, _calldata); - - // Compute the hash over the message header (ticketID, blockNumber) and body (data). - messageHash = keccak256(abi.encode(_ticketID, _blockNumber, messageData)); - } -} diff --git a/contracts/src/bridge/FastBridgeSenderToGnosis.sol b/contracts/src/bridge/FastBridgeSenderToGnosis.sol deleted file mode 100644 index c03379b0a..000000000 --- a/contracts/src/bridge/FastBridgeSenderToGnosis.sol +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: MIT - -/** - * @authors: [@jaybuidl] - * @reviewers: [] - * @auditors: [] - * @bounties: [] - * @deployments: [] - */ - -pragma solidity ^0.8.0; - -import "./interfaces/IFastBridgeSender.sol"; - -/** - * Fast Bridge Sender to Gnosis from Arbitrum - * Counterpart of `FastBridgeReceiverOnGnosis` - */ -abstract contract FastBridgeSenderToGnosis is IFastBridgeSender { - // TODO in prealpha-3 -} diff --git a/contracts/src/bridge/SafeBridgeReceiverOnEthereum.sol b/contracts/src/bridge/SafeBridgeReceiverOnEthereum.sol deleted file mode 100644 index 07a43f833..000000000 --- a/contracts/src/bridge/SafeBridgeReceiverOnEthereum.sol +++ /dev/null @@ -1,75 +0,0 @@ -// SPDX-License-Identifier: MIT - -/** - * @authors: [@jaybuidl, @shalzz] - * @reviewers: [] - * @auditors: [] - * @bounties: [] - * @deployments: [] - */ - -pragma solidity ^0.8.0; - -import "./interfaces/ISafeBridgeReceiver.sol"; -import "./interfaces/arbitrum/IInbox.sol"; -import "./interfaces/arbitrum/IOutbox.sol"; - -/** - * Safe Bridge Receiver on Ethereum from Arbitrum - * Counterpart of `SafeBridgeSenderToEthereum` - */ -contract SafeBridgeReceiverOnEthereum is ISafeBridgeReceiver { - // ************************************* // - // * Storage * // - // ************************************* // - - address public governor; // The governor of the contract. - address public safeBridgeSender; // The address of the Safe Bridge sender on Arbitrum. - IInbox public inbox; // The address of the Arbitrum Inbox contract. - - // ************************************* // - // * Function Modifiers * // - // ************************************* // - - modifier onlyByGovernor() { - require(governor == msg.sender, "Access not allowed: Governor only."); - _; - } - - /** - * @dev Constructor. - * @param _governor The governor's address. - * @param _safeBridgeSender The address of the Safe Bridge sender on Arbitrum. - * @param _inbox The address of the Arbitrum Inbox contract. - */ - constructor( - address _governor, - address _safeBridgeSender, - address _inbox - ) { - governor = _governor; - inbox = IInbox(_inbox); - safeBridgeSender = _safeBridgeSender; - } - - // ************************************* // - // * Views * // - // ************************************* // - - function isSentBySafeBridge() internal view override returns (bool) { - IOutbox outbox = IOutbox(inbox.bridge().activeOutbox()); - return outbox.l2ToL1Sender() == safeBridgeSender; - } - - // ************************ // - // * Governance * // - // ************************ // - - function setSafeBridgeSender(address _safeBridgeSender) external onlyByGovernor { - safeBridgeSender = _safeBridgeSender; - } - - function setInbox(address _inbox) external onlyByGovernor { - inbox = IInbox(_inbox); - } -} diff --git a/contracts/src/bridge/SafeBridgeRouter.sol b/contracts/src/bridge/SafeBridgeRouter.sol new file mode 100644 index 000000000..64e12c60f --- /dev/null +++ b/contracts/src/bridge/SafeBridgeRouter.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: MIT + +/** + * @authors: [@shotaronowhere] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ + +pragma solidity ^0.8.0; + +import "./interfaces/ISafeBridgeReceiver.sol"; +import "./interfaces/ISafeBridgeSender.sol"; +import "./interfaces/gnosis-chain/IAMB.sol"; +import "./interfaces/arbitrum/IInbox.sol"; +import "./interfaces/arbitrum/IOutbox.sol"; + +/** + * Router on Ethereum from Arbitrum to Gnosis Chain. + */ +contract SafeBridgeRouter is ISafeBridgeReceiver, ISafeBridgeSender { + // ************************************* // + // * Events * // + // ************************************* // + + event safeRelayed(bytes32 indexed txID); + + // ************************************* // + // * Storage * // + // ************************************* // + + IInbox public immutable inbox; // The address of the Arbitrum Inbox contract. + IAMB public immutable amb; // The address of the AMB contract on Ethereum. + address public immutable safeBridgeSender; // The address of the Safe Bridge sender on Arbitrum. + address public immutable fastBridgeReceiverOnGnosisChain; // The address of the Fast Bridge Receiver on Gnosis Chain. + + /** + * @dev Constructor. + * @param _inbox The address of the inbox contract on Ethereum. + * @param _amb The duration of the period allowing to challenge a claim. + * @param _safeBridgeSender The safe bridge sender on Arbitrum. + * @param _fastBridgeReceiverOnGnosisChain The fast bridge receiver on Gnosis Chain. + */ + constructor( + IInbox _inbox, + IAMB _amb, + address _safeBridgeSender, + address _fastBridgeReceiverOnGnosisChain + ) { + inbox = _inbox; + amb = _amb; + safeBridgeSender = _safeBridgeSender; + fastBridgeReceiverOnGnosisChain = _fastBridgeReceiverOnGnosisChain; + } + + /** + * Routes an arbitrary message from one domain to another. + * Note: Access restricted to the Safe Bridge. + * @param _epoch The epoch associated with the _batchmerkleRoot. + * @param _batchMerkleRoot The true batch merkle root for the epoch sent by the safe bridge. * @return Unique id to track the message request/transaction. + */ + function verifySafe(uint256 _epoch, bytes32 _batchMerkleRoot) external override onlyFromSafeBridge { + require(isSentBySafeBridge(), "Access not allowed: SafeBridgeSender only."); + + bytes4 methodSelector = ISafeBridgeReceiver.verifySafe.selector; + bytes memory safeMessageData = abi.encodeWithSelector(methodSelector, _epoch, _batchMerkleRoot); + + // replace maxGasPerTx with safe level for production deployment + bytes32 txID = _sendSafe(fastBridgeReceiverOnGnosisChain, safeMessageData); + emit safeRelayed(txID); + } + + function _sendSafe(address _receiver, bytes memory _calldata) internal override returns (bytes32) { + return amb.requireToPassMessage(_receiver, _calldata, amb.maxGasPerTx()); + } + + // ************************************* // + // * Views * // + // ************************************* // + + function isSentBySafeBridge() internal view override returns (bool) { + IOutbox outbox = IOutbox(inbox.bridge().activeOutbox()); + return outbox.l2ToL1Sender() == safeBridgeSender; + } +} diff --git a/contracts/src/bridge/SafeBridgeSenderToArbitrumFromEthereum.sol b/contracts/src/bridge/SafeBridgeSenderToArbitrumFromEthereum.sol deleted file mode 100644 index d10baf8d9..000000000 --- a/contracts/src/bridge/SafeBridgeSenderToArbitrumFromEthereum.sol +++ /dev/null @@ -1,89 +0,0 @@ -// SPDX-License-Identifier: MIT - -/** - * @authors: [@jaybuidl, @shalzz] - * @reviewers: [] - * @auditors: [] - * @bounties: [] - * @deployments: [] - */ - -pragma solidity ^0.8.0; - -import "./interfaces/arbitrum/IInbox.sol"; -import "./interfaces/arbitrum/IOutbox.sol"; -import "./interfaces/arbitrum/IArbRetryableTx.sol"; -import "./interfaces/ISafeBridgeSender.sol"; - -/** - * Safe Bridge Sender to Arbitrum from Ethereum - * Counterpart of `SafeBridgeReceiverOnArbitrumFromEthereum` if any - */ -contract SafeBridgeSenderToArbitrumFromEthereum is ISafeBridgeSender { - IArbRetryableTx public constant ARBITRUM_RETRYABLE_TX = IArbRetryableTx(address(110)); - address public immutable safeBridgeSender; - IInbox public immutable inbox; - uint256 public immutable maxGas; - uint256 public immutable gasPriceBid; - - event RetryableTicketCreated(uint256 indexed ticketId); - - /** - * @param _inbox The Arbitrum Inbox address on Ethereum. - * @param _maxGas Gas limit for immediate L2 execution attempt. - * @param _gasPriceBid L2 Gas price bid for immediate L2 execution attempt. - */ - constructor( - address _safeBridgeSender, - address _inbox, - uint256 _maxGas, - uint256 _gasPriceBid - ) { - safeBridgeSender = _safeBridgeSender; - inbox = IInbox(_inbox); - maxGas = _maxGas; - gasPriceBid = _gasPriceBid; - } - - /** - * Sends an arbitrary message from one domain to another. - * - * @dev The caller needs to pay some ETH to cover the gas costs - * of the call on L2. Excess ETH that is not used by gas costs will - * be refunded to the `msg.sender` address on L2. - * - * @notice if a user does not desire immediate redemption, they should - * provide a DepositValue of at least CallValue + MaxSubmissionCost. - * If they do desire immediate execution, they should provide a DepositValue - * of at least CallValue + MaxSubmissionCost + (GasPrice x MaxGas). - * - * @param _receiver The cross-domain target on Arbitrum. - * @param _calldata The encoded message data. - * @return Unique id to track the message request/transaction. - */ - function _sendSafe(address _receiver, bytes memory _calldata) internal override returns (uint256) { - require(msg.sender == safeBridgeSender, "Access not allowed: Safe Bridge Sender only."); - - uint256 baseSubmissionCost = bridgingCost(_calldata.length); - require(msg.value >= baseSubmissionCost + (maxGas * gasPriceBid)); - - uint256 ticketID = inbox.createRetryableTicket{value: msg.value}( - _receiver, - 0, - baseSubmissionCost, - msg.sender, - msg.sender, - maxGas, - gasPriceBid, - _calldata - ); - - emit RetryableTicketCreated(ticketID); - return ticketID; - } - - function bridgingCost(uint256 _calldatasize) internal view returns (uint256) { - (uint256 submissionCost, ) = ARBITRUM_RETRYABLE_TX.getSubmissionPrice(_calldatasize); - return submissionCost; - } -} diff --git a/contracts/src/bridge/SafeBridgeSenderToEthereum.sol b/contracts/src/bridge/SafeBridgeSenderToEthereum.sol deleted file mode 100644 index 46ae5e3dc..000000000 --- a/contracts/src/bridge/SafeBridgeSenderToEthereum.sol +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-License-Identifier: MIT - -/** - * @authors: [@shalzz, @jaybuidl] - * @reviewers: [] - * @auditors: [] - * @bounties: [] - * @deployments: [] - */ - -pragma solidity ^0.8.0; - -import "./interfaces/arbitrum/IArbSys.sol"; -import "./interfaces/arbitrum/AddressAliasHelper.sol"; - -import "./interfaces/ISafeBridgeSender.sol"; - -/** - * Safe Bridge Sender to Ethereum from Arbitrum - * Counterpart of `SafeBridgeReceiverOnEthereum` - */ -contract SafeBridgeSenderToEthereum is ISafeBridgeSender { - // ************************************* // - // * Events * // - // ************************************* // - - event L2ToL1TxCreated(uint256 indexed withdrawalId); - - // ************************************* // - // * Storage * // - // ************************************* // - - IArbSys public constant ARB_SYS = IArbSys(address(100)); - - // ************************************* // - // * Function Modifiers * // - // ************************************* // - - function _sendSafe(address _receiver, bytes memory _calldata) internal override returns (uint256) { - uint256 withdrawalId = ARB_SYS.sendTxToL1(_receiver, _calldata); - - emit L2ToL1TxCreated(withdrawalId); - return withdrawalId; - } -} diff --git a/contracts/src/bridge/SafeBridgeSenderToGnosis.sol b/contracts/src/bridge/SafeBridgeSenderToGnosis.sol deleted file mode 100644 index 41f9189d8..000000000 --- a/contracts/src/bridge/SafeBridgeSenderToGnosis.sol +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: MIT - -/** - * @authors: [@shalzz] - * @reviewers: [] - * @auditors: [] - * @bounties: [] - * @deployments: [] - */ - -pragma solidity ^0.8.0; - -import "./interfaces/gnosis-chain/IAMB.sol"; -import "./interfaces/ISafeBridgeSender.sol"; - -/** - * Safe Bridge Sender to Gnosis from Ethereum - * Counterpart of `SafeBridgeReceiverOnGnosis` if any - */ -contract SafeBridgeSenderToGnosis is ISafeBridgeSender { - IAMB public immutable amb; - - constructor(IAMB _amb) { - amb = _amb; - } - - function _sendSafe(address _receiver, bytes memory _calldata) internal override returns (uint256) { - bytes32 id = amb.requireToPassMessage(_receiver, _calldata, amb.maxGasPerTx()); - return uint256(id); - } -} diff --git a/contracts/src/bridge/interfaces/IFastBridgeReceiver.sol b/contracts/src/bridge/interfaces/IFastBridgeReceiver.sol index 0b508f8d1..0f434894e 100644 --- a/contracts/src/bridge/interfaces/IFastBridgeReceiver.sol +++ b/contracts/src/bridge/interfaces/IFastBridgeReceiver.sol @@ -17,101 +17,71 @@ interface IFastBridgeReceiver { /** * @dev The Fast Bridge participants watch for these events to decide if a challenge should be submitted. - * @param ticketID The ticket identifier referring to a message going through the bridge. - * @param messageHash The claimed hash corresponding to this `ticketID`. It should match the hash from the sending side otherwise it will be challenged. - * @param claimedAt The timestamp of the claim creation. + * @param _epoch The epoch in the the claim was made. + * @param _batchMerkleRoot The timestamp of the claim creation. */ - event ClaimReceived(uint256 indexed ticketID, bytes32 indexed messageHash, uint256 claimedAt); + event ClaimReceived(uint256 _epoch, bytes32 indexed _batchMerkleRoot); /** * @dev The Fast Bridge participants watch for these events to call `sendSafeFallback()` on the sending side. - * @param ticketID The ticket identifier referring to a message going through the bridge. - * @param challengedAt The timestamp of the challenge creation. + * @param _epoch The epoch associated with the challenged claim. */ - event ClaimChallenged(uint256 indexed ticketID, uint256 challengedAt); + event ClaimChallenged(uint256 _epoch); // ************************************* // // * Function Modifiers * // // ************************************* // /** - * @dev Submit a claim about the `messageHash` for a particular Fast Bridge `ticketID` and submit a deposit. The `messageHash` should match the one on the sending side otherwise the sender will lose his deposit. - * @param _ticketID The ticket identifier referring to a message going through the bridge. - * @param _messageHash The hash claimed for the ticket. + * @dev Submit a claim about the `_batchMerkleRoot` for the latests completed Fast bridge epoch and submit a deposit. The `_batchMerkleRoot` should match the one on the sending side otherwise the sender will lose his deposit. + * @param _batchMerkleRoot The hash claimed for the ticket. */ - function claim(uint256 _ticketID, bytes32 _messageHash) external payable; + function claim(bytes32 _batchMerkleRoot) external payable; /** - * @dev Submit a challenge for a particular Fast Bridge `ticketID` and submit a deposit. The `messageHash` in the claim already made for this `ticketID` should be different from the one on the sending side, otherwise the sender will lose his deposit. - * @param _ticketID The ticket identifier referring to a message going through the bridge. + * @dev Submit a challenge for the claim of the current epoch's Fast Bridge batch merkleroot state and submit a deposit. The `batchMerkleRoot` in the claim already made for the last finalized epoch should be different from the one on the sending side, otherwise the sender will lose his deposit. */ - function challenge(uint256 _ticketID) external payable; + function challenge() external payable; /** - * @dev Relay the message for this `ticketID` if the challenge period has passed and the claim is unchallenged. The hash computed over `messageData` and the other parameters must match the hash provided by the claim. - * @param _ticketID The ticket identifier referring to a message going through the bridge. - * @param _blockNumber The block number on the cross-domain chain when the message with this ticketID has been sent. - * @param _messageData The data on the cross-domain chain for the message sent with this ticketID. + * @dev Verifies merkle proof for the given message and associated nonce for the most recent possible epoch and relays the message. + * @param _epoch The epoch in which the message was batched by the bridge. + * @param _proof The merkle proof to prove the membership of the message and nonce in the merkle tree for the epoch. + * @param _message The data on the cross-domain chain for the message. + * @param _nonce The nonce (index in the merkle tree) to avoid replay. */ - function verifyAndRelay( - uint256 _ticketID, - uint256 _blockNumber, - bytes calldata _messageData + function verifyAndRelayMessage( + uint256 _epoch, + bytes32[] calldata _proof, + bytes calldata _message, + uint256 _nonce ) external; /** - * Note: Access restricted to the Safe Bridge. - * @dev Relay the message for this `ticketID` as provided by the Safe Bridge. Resolve a challenged claim for this `ticketID` if any. - * @param _ticketID The ticket identifier referring to a message going through the bridge. - * @param _blockNumber The block number on the cross-domain chain when the message with this ticketID has been sent. - * @param _messageData The data on the cross-domain chain for the message sent with this ticketID. + * @dev Sends the deposit back to the Bridger if their claim is not successfully challenged. Includes a portion of the Challenger's deposit if unsuccessfully challenged. + * @param _epoch The epoch associated with the claim deposit to withraw. */ - function verifyAndRelaySafe( - uint256 _ticketID, - uint256 _blockNumber, - bytes calldata _messageData - ) external; - - /** - * @dev Sends the deposit back to the Bridger if his claim is not successfully challenged. Includes a portion of the Challenger's deposit if unsuccessfully challenged. - * @param _ticketID The ticket identifier referring to a message going through the bridge. - */ - function withdrawClaimDeposit(uint256 _ticketID) external; + function withdrawClaimDeposit(uint256 _epoch) external; /** * @dev Sends the deposit back to the Challenger if his challenge is successful. Includes a portion of the Bridger's deposit. - * @param _ticketID The ticket identifier referring to a message going through the bridge. + * @param _epoch The epoch associated with the challenge deposit to withraw. */ - function withdrawChallengeDeposit(uint256 _ticketID) external; + function withdrawChallengeDeposit(uint256 _epoch) external; // ************************************* // // * Public Views * // // ************************************* // /** - * @dev Returns the `start` and `end` time of challenge period for this `ticketID`. + * @dev Returns the `start` and `end` time of challenge period for this `epoch`. * @return start The start time of the challenge period. * @return end The end time of the challenge period. */ - function challengePeriod(uint256 _ticketID) external view returns (uint256 start, uint256 end); - - /** - * @return amount The deposit required to submit a claim. - */ - function claimDeposit() external view returns (uint256 amount); - - /** - * @return amount The deposit required to submit a challenge. - */ - function challengeDeposit() external view returns (uint256 amount); - - /** - * @return amount The duration of the period allowing to challenge a claim. - */ - function challengeDuration() external view returns (uint256 amount); + function challengePeriod() external view returns (uint256 start, uint256 end); /** - * @return amount Basis point of claim or challenge deposit that are lost when dishonest. + * @dev Returns the epoch period. */ - function alpha() external view returns (uint256 amount); + function epochPeriod() external view returns (uint256 epoch); } diff --git a/contracts/src/bridge/interfaces/IFastBridgeSender.sol b/contracts/src/bridge/interfaces/IFastBridgeSender.sol index c5ed12ab8..da95c6d24 100644 --- a/contracts/src/bridge/interfaces/IFastBridgeSender.sol +++ b/contracts/src/bridge/interfaces/IFastBridgeSender.sol @@ -9,42 +9,48 @@ interface IFastBridgeSender { /** * @dev The Fast Bridge participants need to watch for these events and relay the messageHash on the FastBridgeReceiverOnEthereum. - * @param ticketID The ticket identifier referring to a message going through the bridge. - * @param blockNumber The block number when the message with this ticketID has been created. * @param target The address of the cross-domain receiver of the message, generally the Foreign Gateway. - * @param messageHash The hash uniquely identifying this message. * @param message The message data. + * @param nonce The message nonce. */ - event OutgoingMessage( - uint256 indexed ticketID, - uint256 blockNumber, - address target, - bytes32 indexed messageHash, - bytes message - ); + event MessageReceived(address target, bytes message, uint256 nonce); // ************************************* // // * Function Modifiers * // // ************************************* // /** - * Note: Access must be restricted to the sending application. + * Note: Access must be restricted by the receiving contract. + * Message is sent with the message sender address as the first argument. * @dev Sends an arbitrary message across domain using the Fast Bridge. * @param _receiver The cross-domain contract address which receives the calldata. + * @param _functionSelector The function selector to call. * @param _calldata The receiving domain encoded message data. - * @return ticketID The identifier to provide to sendSafeFallback(). */ - function sendFast(address _receiver, bytes memory _calldata) external returns (uint256 ticketID); + function sendFast( + address _receiver, + bytes4 _functionSelector, + bytes memory _calldata + ) external; /** - * @dev Sends an arbitrary message across domain using the Safe Bridge, which relies on the chain's canonical bridge. It is unnecessary during normal operations but essential only in case of challenge. - * @param _ticketID The ticketID as returned by `sendFast()`. + * Note: Access must be restricted by the receiving contract. + * Message is sent with the message sender address as the first argument. + * @dev Sends an arbitrary message across domain using the Fast Bridge. * @param _receiver The cross-domain contract address which receives the calldata. - * @param _calldata The receiving domain encoded message data. + * @param _functionSelector The function selector to call. */ - function sendSafeFallback( - uint256 _ticketID, - address _receiver, - bytes memory _calldata - ) external payable; + function sendFast(address _receiver, bytes4 _functionSelector) external; + + /** + * Sends a batch of arbitrary message from one domain to another + * via the fast bridge mechanism. + */ + function sendEpoch() external; + + /** + * @dev Sends a markle root representing an arbitrary batch of messages across domain using the Safe Bridge, which relies on the chain's canonical bridge. It is unnecessary during normal operations but essential only in case of challenge. + * @param _epoch block number of batch + */ + function sendSafeFallback(uint256 _epoch) external payable; } diff --git a/contracts/src/bridge/interfaces/ISafeBridgeReceiver.sol b/contracts/src/bridge/interfaces/ISafeBridgeReceiver.sol index bb3916563..58e11a186 100644 --- a/contracts/src/bridge/interfaces/ISafeBridgeReceiver.sol +++ b/contracts/src/bridge/interfaces/ISafeBridgeReceiver.sol @@ -3,5 +3,18 @@ pragma solidity ^0.8.0; abstract contract ISafeBridgeReceiver { + /** + * Note: Access restricted to the Safe Bridge. + * @dev Resolves any challenge of the optimistic claim for '_epoch'. + * @param _epoch The epoch associated with the _batchmerkleRoot. + * @param _batchMerkleRoot The true batch merkle root for the epoch sent by the safe bridge. + */ + function verifySafe(uint256 _epoch, bytes32 _batchMerkleRoot) external virtual; + function isSentBySafeBridge() internal view virtual returns (bool); + + modifier onlyFromSafeBridge() { + require(isSentBySafeBridge(), "Safe Bridge only."); + _; + } } diff --git a/contracts/src/bridge/interfaces/ISafeBridgeSender.sol b/contracts/src/bridge/interfaces/ISafeBridgeSender.sol index 97cb8f9b4..ddcc8df06 100644 --- a/contracts/src/bridge/interfaces/ISafeBridgeSender.sol +++ b/contracts/src/bridge/interfaces/ISafeBridgeSender.sol @@ -6,9 +6,9 @@ abstract contract ISafeBridgeSender { /** * Sends an arbitrary message from one domain to another. * - * @param _receiver The L1 contract address who will receive the calldata - * @param _calldata The L2 encoded message data. + * @param _receiver The foreign chain contract address who will receive the calldata + * @param _calldata The home chain encoded message data. * @return Unique id to track the message request/transaction. */ - function _sendSafe(address _receiver, bytes memory _calldata) internal virtual returns (uint256); + function _sendSafe(address _receiver, bytes memory _calldata) internal virtual returns (bytes32); } diff --git a/contracts/src/bridge/interfaces/gnosis-chain/IAMB.sol b/contracts/src/bridge/interfaces/gnosis-chain/IAMB.sol index 3f5ca3d5e..da6a6efe1 100644 --- a/contracts/src/bridge/interfaces/gnosis-chain/IAMB.sol +++ b/contracts/src/bridge/interfaces/gnosis-chain/IAMB.sol @@ -1,4 +1,7 @@ // SPDX-License-Identifier: MIT +// Complete IAMB Interface +// https://github.com/poanetwork/tokenbridge-contracts/blob/master/contracts/interfaces/IAMB.sol + pragma solidity ^0.8.0; interface IAMB { @@ -12,7 +15,29 @@ interface IAMB { function messageSender() external view returns (address); - function messageSourceChainId() external view returns (bytes32); + function messageSourceChainId() external view returns (uint256); function messageId() external view returns (bytes32); + + function transactionHash() external view returns (bytes32); + + function messageCallStatus(bytes32 _messageId) external view returns (bool); + + function failedMessageDataHash(bytes32 _messageId) external view returns (bytes32); + + function failedMessageReceiver(bytes32 _messageId) external view returns (address); + + function failedMessageSender(bytes32 _messageId) external view returns (address); + + function requireToConfirmMessage( + address _contract, + bytes memory _data, + uint256 _gas + ) external returns (bytes32); + + function requireToGetInformation(bytes32 _requestSelector, bytes memory _data) external returns (bytes32); + + function sourceChainId() external view returns (uint256); + + function destinationChainId() external view returns (uint256); } diff --git a/contracts/src/bridge/merkle/MerkleProof.sol b/contracts/src/bridge/merkle/MerkleProof.sol index 09a92245e..fa91a3b35 100644 --- a/contracts/src/bridge/merkle/MerkleProof.sol +++ b/contracts/src/bridge/merkle/MerkleProof.sol @@ -29,24 +29,11 @@ contract MerkleProof { return (merkleRoot == calculateRoot(proof, leaf)); } - /** @dev Validates membership of leaf in merkle tree with merkle proof. - * @param proof The merkle proof. - * @param data The data to validate membership in merkle tree. - * @param merkleRoot The root of the merkle tree. - */ - function validateProof( - bytes32[] memory proof, - bytes memory data, - bytes32 merkleRoot - ) public pure returns (bool) { - return validateProof(proof, keccak256(data), merkleRoot); - } - /** @dev Calculates merkle root from proof. * @param proof The merkle proof. * @param leaf The leaf to validate membership in merkle tree.. */ - function calculateRoot(bytes32[] memory proof, bytes32 leaf) internal pure returns (bytes32) { + function calculateRoot(bytes32[] memory proof, bytes32 leaf) private pure returns (bytes32) { uint256 proofLength = proof.length; require(proofLength <= 32, "Invalid Proof"); bytes32 h = leaf; diff --git a/contracts/src/bridge/merkle/MerkleTreeHistory.sol b/contracts/src/bridge/merkle/MerkleTree.sol similarity index 61% rename from contracts/src/bridge/merkle/MerkleTreeHistory.sol rename to contracts/src/bridge/merkle/MerkleTree.sol index 43aa27ced..876a4028b 100644 --- a/contracts/src/bridge/merkle/MerkleTreeHistory.sol +++ b/contracts/src/bridge/merkle/MerkleTree.sol @@ -11,22 +11,19 @@ pragma solidity ^0.8.0; /** - * @title MerkleTreeHistory + * @title MerkleTree * @author Shotaro N. - - * @dev An efficient append only merkle tree with history. + * @dev An efficient append only merkle tree. */ -contract MerkleTreeHistory { +contract MerkleTree { // ***************************** // // * Storage * // // ***************************** // - // merkle tree representation - // supports 2^32-1 messages. - bytes32[32] public branch; - uint256 public count; - - // block number => merkle root history - mapping(uint256 => bytes32) private history; + // merkle tree representation of a batch of messages + // supports 2^64 messages. + bytes32[64] private batch; + uint256 internal batchSize; // ************************************* // // * State Modifiers * // @@ -37,74 +34,64 @@ contract MerkleTreeHistory { * `n` is the number of leaves. * Note: Although each insertion is O(log(n)), * Complexity of n insertions is O(n). - * @param data The data to insert in the merkle tree. + * @param leaf The leaf (already hashed) to insert in the merkle tree. */ - function append(bytes memory data) public { - bytes32 leaf = keccak256(data); - count += 1; - uint256 size = count; + function appendMessage(bytes32 leaf) internal { + // Differentiate leaves from interior nodes with different + // hash functions to prevent 2nd order pre-image attack. + // https://flawed.net.nz/2018/02/21/attacking-merkle-trees-with-a-second-preimage-attack/ + uint256 size = batchSize + 1; + batchSize = size; uint256 hashBitField = (size ^ (size - 1)) & size; - - for (uint256 height = 0; height < 32; height++) { - if ((hashBitField & 1) == 1) { - branch[height] = leaf; - return; - } - bytes32 node = branch[height]; - // effecient hash + uint256 height; + while ((hashBitField & 1) == 0) { + bytes32 node = batch[height]; if (node > leaf) assembly { + // effecient hash mstore(0x00, leaf) mstore(0x20, node) leaf := keccak256(0x00, 0x40) } else assembly { + // effecient hash mstore(0x00, node) mstore(0x20, leaf) leaf := keccak256(0x00, 0x40) } hashBitField /= 2; + height = height + 1; } + batch[height] = leaf; } /** @dev Saves the merkle root state in history and resets. * `O(log(n))` where * `n` is the number of leaves. */ - function reset() internal { - history[block.number] = getMerkleRoot(); - count = 0; - } - - /** @dev Gets the merkle root history - * `O(log(n))` where - * `n` is the number of leaves. - * @param blocknumber requested blocknumber. - */ - function getMerkleRootHistory(uint256 blocknumber) public view returns (bytes32) { - if (blocknumber == block.number) return getMerkleRoot(); - - return history[blocknumber]; + function getMerkleRootAndReset() internal returns (bytes32 batchMerkleRoot) { + batchMerkleRoot = getMerkleRoot(); + batchSize = 0; } /** @dev Gets the current merkle root. * `O(log(n))` where * `n` is the number of leaves. */ - function getMerkleRoot() public view returns (bytes32) { + function getMerkleRoot() internal view returns (bytes32) { bytes32 node; - uint256 size = count; + uint256 size = batchSize; uint256 height = 0; bool isFirstHash = true; while (size > 0) { - // avoid redundant calculation if ((size & 1) == 1) { + // avoid redundant calculation if (isFirstHash) { - node = branch[height]; + node = batch[height]; isFirstHash = false; } else { - bytes32 hash = branch[height]; + bytes32 hash = batch[height]; // effecient hash if (hash > node) assembly { diff --git a/contracts/src/bridge/merkle/test/MerkleProofExposed.sol b/contracts/src/bridge/merkle/test/MerkleProofExposed.sol new file mode 100644 index 000000000..ed97d88ab --- /dev/null +++ b/contracts/src/bridge/merkle/test/MerkleProofExposed.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT + +/** + * @authors: [@shotaronowhere] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ + +pragma solidity ^0.8.0; + +import "../MerkleProof.sol"; + +/** + * @title MerkleProofExpose + * @author Shotaro N. - + * @dev A set of exposed funcitons to test the MerkleProof contract + */ +contract MerkleProofExposed is MerkleProof { + /** @dev Validates membership of leaf in merkle tree with merkle proof. + * @param proof The merkle proof. + * @param leaf The leaf to validate membership in merkle tree. + * @param merkleRoot The root of the merkle tree. + */ + function _validateProof( + bytes32[] memory proof, + bytes32 leaf, + bytes32 merkleRoot + ) public pure returns (bool) { + return validateProof(proof, leaf, merkleRoot); + } +} diff --git a/contracts/src/bridge/merkle/test/MerkleTreeExposed.sol b/contracts/src/bridge/merkle/test/MerkleTreeExposed.sol new file mode 100644 index 000000000..912b37eb9 --- /dev/null +++ b/contracts/src/bridge/merkle/test/MerkleTreeExposed.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT + +/** + * @authors: [@shotaronowhere] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ + +pragma solidity ^0.8.0; + +import "../MerkleTree.sol"; + +/** + * @title MerkleTreeExposed + * @author Shotaro N. - + * @dev Exposes MerkleTree for testing + */ +contract MerkleTreeExposed is MerkleTree { + function _appendMessage(bytes memory _leaf) public { + appendMessage(sha256(_leaf)); + } + + function _getMerkleRoot() public view returns (bytes32 merkleroot) { + merkleroot = getMerkleRoot(); + } +} diff --git a/contracts/src/bridge/test/FastBridgeSenderMock.sol b/contracts/src/bridge/test/FastBridgeSenderMock.sol new file mode 100644 index 000000000..f099a1b87 --- /dev/null +++ b/contracts/src/bridge/test/FastBridgeSenderMock.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT + +/** + * @authors: [@jaybuidl, @shalzz, @hrishibhat, @shotaronowhere] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ + +pragma solidity ^0.8.0; + +import "../FastBridgeSenderBase.sol"; +import "../interfaces/arbitrum/IArbSys.sol"; + +/** + * Fast Sender from Arbitrum Mock + * Counterpart of `FastReceiverOnEthereum` + */ +contract FastBridgeSenderMock is FastBridgeSenderBase { + // ************************************* // + // * Events * // + // ************************************* // + + event L2ToL1TxCreated(uint256 indexed txID); + + // ************************************* // + // * Storage * // + // ************************************* // + + IArbSys public immutable arbsys; + + /** + * @dev Constructor. + * @param _epochPeriod The immutable period between epochs. + * @param _genesis The immutable genesis state variable from the FastBridgeSeneder. + * @param _fastBridgeReceiver The address of the Fast Bridge on Ethereum. + */ + constructor( + uint256 _epochPeriod, + uint256 _genesis, + address _fastBridgeReceiver, + address _arbsys + ) FastBridgeSenderBase(_epochPeriod, _genesis, _fastBridgeReceiver) { + arbsys = IArbSys(address(_arbsys)); + } + + /** + * @dev Sends the merkle root state for _epoch to Ethereum using the Safe Bridge, which relies on Arbitrum's canonical bridge. It is unnecessary during normal operations but essential only in case of challenge. + * @param _epoch The blocknumber of the batch + */ + function sendSafeFallback(uint256 _epoch) external payable override { + bytes32 batchMerkleRoot = fastOutbox[_epoch]; + + // Safe Bridge message envelope + bytes4 methodSelector = ISafeBridgeReceiver.verifySafe.selector; + bytes memory safeMessageData = abi.encodeWithSelector(methodSelector, _epoch, batchMerkleRoot); + + _sendSafe(safeRouter, safeMessageData); + } + + function _sendSafe(address _receiver, bytes memory _calldata) internal override returns (bytes32) { + uint256 txID = arbsys.sendTxToL1(_receiver, _calldata); + + emit L2ToL1TxCreated(txID); + return bytes32(txID); + } +} diff --git a/contracts/src/bridge/test/arbitrum/ArbSysMock.sol b/contracts/src/bridge/test/arbitrum/ArbSysMock.sol new file mode 100644 index 000000000..7febe655b --- /dev/null +++ b/contracts/src/bridge/test/arbitrum/ArbSysMock.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT + +/** + * @authors: [@hrishibhat] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ + +pragma solidity ^0.8.0; + +import "../../interfaces/arbitrum/IArbSys.sol"; + +contract ArbSysMock { + function sendTxToL1(address destination, bytes calldata calldataForL1) + external + payable + returns (uint256 _withdrawal_ID) + { + (bool success, ) = address(destination).call(calldataForL1); + require(success, "Failed TxToL1"); + return _withdrawal_ID; + } +} diff --git a/contracts/src/bridge/test/arbitrum/BridgeMock.sol b/contracts/src/bridge/test/arbitrum/BridgeMock.sol new file mode 100644 index 000000000..f98b04c36 --- /dev/null +++ b/contracts/src/bridge/test/arbitrum/BridgeMock.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT + +/** + * @authors: [@hrishibhat] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ + +pragma solidity ^0.8.0; + +import "../../interfaces/arbitrum/IInbox.sol"; + +contract BridgeMock is IBridge { + address public outbox; + + constructor(address _outbox) { + outbox = _outbox; + } + + function activeOutbox() external view returns (address _outbox) { + return address(outbox); + } + + function deliverMessageToInbox( + uint8 kind, + address sender, + bytes32 messageDataHash + ) external payable returns (uint256) {} + + function executeCall( + address destAddr, + uint256 amount, + bytes calldata data + ) external returns (bool success, bytes memory returnData) {} + + // These are only callable by the admin + function setInbox(address inbox, bool enabled) external {} + + function setOutbox(address inbox, bool enabled) external {} + + // View functions + + function allowedInboxes(address inbox) external view returns (bool) {} + + function allowedOutboxes(address outbox) external view returns (bool) {} + + function inboxAccs(uint256 index) external view returns (bytes32) {} + + function messageCount() external view returns (uint256) {} +} diff --git a/contracts/src/bridge/test/arbitrum/InboxMock.sol b/contracts/src/bridge/test/arbitrum/InboxMock.sol new file mode 100644 index 000000000..682cfb25e --- /dev/null +++ b/contracts/src/bridge/test/arbitrum/InboxMock.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT + +/** + * @authors: [@hrishibhat] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ + +pragma solidity ^0.8.0; + +import "../../interfaces/arbitrum/IInbox.sol"; + +contract InboxMock is IInbox { + IBridge public arbBridge; + + constructor(address _bridge) { + arbBridge = IBridge(_bridge); + } + + function bridge() external view returns (IBridge) { + return arbBridge; + } + + function sendL2Message(bytes calldata messageData) external returns (uint256) {} + + function sendUnsignedTransaction( + uint256 maxGas, + uint256 gasPriceBid, + uint256 nonce, + address destAddr, + uint256 amount, + bytes calldata data + ) external returns (uint256) {} + + function sendContractTransaction( + uint256 maxGas, + uint256 gasPriceBid, + address destAddr, + uint256 amount, + bytes calldata data + ) external returns (uint256) {} + + function sendL1FundedUnsignedTransaction( + uint256 maxGas, + uint256 gasPriceBid, + uint256 nonce, + address destAddr, + bytes calldata data + ) external payable returns (uint256) {} + + function sendL1FundedContractTransaction( + uint256 maxGas, + uint256 gasPriceBid, + address destAddr, + bytes calldata data + ) external payable returns (uint256) {} + + function createRetryableTicket( + address destAddr, + uint256 arbTxCallValue, + uint256 maxSubmissionCost, + address submissionRefundAddress, + address valueRefundAddress, + uint256 maxGas, + uint256 gasPriceBid, + bytes calldata data + ) external payable returns (uint256) {} + + function depositEth(uint256 maxSubmissionCost) external payable returns (uint256) {} +} diff --git a/contracts/src/bridge/test/arbitrum/OutboxMock.sol b/contracts/src/bridge/test/arbitrum/OutboxMock.sol new file mode 100644 index 000000000..684c87ea6 --- /dev/null +++ b/contracts/src/bridge/test/arbitrum/OutboxMock.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT + +/** + * @authors: [@hrishibhat] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ + +pragma solidity ^0.8.0; + +import "../../interfaces/arbitrum/IOutbox.sol"; + +contract OutboxMock is IOutbox { + address public safeBridgeSender; + + constructor(address _safeBridgeSender) { + safeBridgeSender = _safeBridgeSender; + } + + function l2ToL1Sender() external view returns (address) { + return address(safeBridgeSender); + } + + function l2ToL1Block() external view returns (uint256) {} + + function l2ToL1EthBlock() external view returns (uint256) {} + + function l2ToL1Timestamp() external view returns (uint256) {} + + function processOutgoingMessages(bytes calldata sendsData, uint256[] calldata sendLengths) external {} +} diff --git a/contracts/src/bridge/test/gnosis-chian/MockAMB.sol b/contracts/src/bridge/test/gnosis-chian/MockAMB.sol new file mode 100644 index 000000000..944bd1f0b --- /dev/null +++ b/contracts/src/bridge/test/gnosis-chian/MockAMB.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: MIT +// https://github.com/poanetwork/tokenbridge-contracts/blob/master/contracts/mocks/AMBMock.sol +pragma solidity ^0.8.0; + +import "../../interfaces/gnosis-chain/IAMB.sol"; +import "../../../libraries/gnosis-chain/Bytes.sol"; + +contract MockAMB is IAMB { + event MockedEvent(bytes32 indexed messageId, bytes encodedData); + + address public messageSender; + uint256 public maxGasPerTx; + bytes32 public transactionHash; + bytes32 public messageId; + uint64 public nonce; + uint256 public messageSourceChainId; + mapping(bytes32 => bool) public messageCallStatus; + mapping(bytes32 => address) public failedMessageSender; + mapping(bytes32 => address) public failedMessageReceiver; + mapping(bytes32 => bytes32) public failedMessageDataHash; + + event MessagePassed(address _contract, bytes _data, uint256 _gas); + + function setMaxGasPerTx(uint256 _value) public { + maxGasPerTx = _value; + } + + function executeMessageCall( + address _contract, + address _sender, + bytes memory _data, + bytes32 _messageId, + uint256 _gas + ) public { + messageSender = _sender; + messageId = _messageId; + transactionHash = _messageId; + messageSourceChainId = 1337; + (bool status, ) = _contract.call{gas: _gas}(_data); + messageSender = address(0); + messageId = bytes32(0); + transactionHash = bytes32(0); + messageSourceChainId = 0; + + messageCallStatus[_messageId] = status; + if (!status) { + failedMessageDataHash[_messageId] = keccak256(_data); + failedMessageReceiver[_messageId] = _contract; + failedMessageSender[_messageId] = _sender; + } + } + + function requireToPassMessage( + address _contract, + bytes memory _data, + uint256 _gas + ) external returns (bytes32) { + return _sendMessage(_contract, _data, _gas, 0x00); + } + + function requireToConfirmMessage( + address _contract, + bytes memory _data, + uint256 _gas + ) external returns (bytes32) { + return _sendMessage(_contract, _data, _gas, 0x80); + } + + function _sendMessage( + address _contract, + bytes memory _data, + uint256 _gas, + uint256 _dataType + ) internal returns (bytes32) { + require(messageId == bytes32(0)); + bytes32 bridgeId = keccak256(abi.encodePacked(uint16(1337), address(this))) & + 0x00000000ffffffffffffffffffffffffffffffffffffffff0000000000000000; + + bytes32 _messageId = bytes32(uint256(0x11223344 << 224)) | bridgeId | bytes32(uint256(nonce)); + nonce += 1; + bytes memory eventData = abi.encodePacked( + _messageId, + msg.sender, + _contract, + uint32(_gas), + uint8(2), + uint8(2), + uint8(_dataType), + uint16(1337), + uint16(1338), + _data + ); + + emit MockedEvent(_messageId, eventData); + return _messageId; + } + + function requireToGetInformation(bytes32 _requestSelector, bytes memory _data) external returns (bytes32) {} + + function sourceChainId() external view returns (uint256) {} + + function destinationChainId() external view returns (uint256) {} +} diff --git a/contracts/src/gateway/ForeignGatewayOnEthereum.sol b/contracts/src/gateway/ForeignGateway.sol similarity index 71% rename from contracts/src/gateway/ForeignGatewayOnEthereum.sol rename to contracts/src/gateway/ForeignGateway.sol index c0614bc72..ea440c6d0 100644 --- a/contracts/src/gateway/ForeignGatewayOnEthereum.sol +++ b/contracts/src/gateway/ForeignGateway.sol @@ -16,10 +16,10 @@ import "../bridge/interfaces/IFastBridgeReceiver.sol"; import "./interfaces/IForeignGateway.sol"; /** - * Foreign Gateway on Ethereum - * Counterpart of `HomeGatewayToEthereum` + * Foreign Gateway + * Counterpart of `HomeGateway` */ -contract ForeignGatewayOnEthereum is IForeignGateway { +contract ForeignGateway is IForeignGateway { // The global default minimum number of jurors in a dispute. uint256 public constant MIN_JURORS = 3; @@ -31,8 +31,8 @@ contract ForeignGatewayOnEthereum is IForeignGateway { // feeForJuror by subcourtID uint256[] internal feeForJuror; - uint256 public chainID; - uint256 public homeChainID; + uint256 public immutable override chainID; + uint256 public immutable override homeChainID; struct DisputeData { uint248 id; @@ -45,7 +45,9 @@ contract ForeignGatewayOnEthereum is IForeignGateway { address public governor; IFastBridgeReceiver public fastbridge; - address public homeGateway; + IFastBridgeReceiver public depreciatedFastbridge; + uint256 public fastbridgeExpiration; + address public immutable override homeGateway; event OutgoingDispute( bytes32 disputeHash, @@ -57,7 +59,11 @@ contract ForeignGatewayOnEthereum is IForeignGateway { ); modifier onlyFromFastBridge() { - require(address(fastbridge) == msg.sender, "Access not allowed: Fast Bridge only."); + require( + address(fastbridge) == msg.sender || + ((block.timestamp < fastbridgeExpiration) && address(depreciatedFastbridge) == msg.sender), + "Access not allowed: Fast Bridge only." + ); _; } @@ -77,11 +83,23 @@ contract ForeignGatewayOnEthereum is IForeignGateway { fastbridge = _fastbridge; feeForJuror = _feeForJuror; homeGateway = _homeGateway; - homeChainID = _homeChainID; - + uint256 id; assembly { - sstore(chainID.slot, chainid()) + id := chainid() } + chainID = id; + homeChainID = _homeChainID; + } + + /** @dev Changes the fastBridge, useful to increase the claim deposit. + * @param _fastbridge The address of the new fastBridge. + * @param _gracePeriod The duration to accept messages from the deprecated bridge (if at all). + */ + function changeFastbridge(IFastBridgeReceiver _fastbridge, uint256 _gracePeriod) external onlyByGovernor { + // grace period to relay remaining messages in the relay / bridging process + fastbridgeExpiration = block.timestamp + _fastbridge.epochPeriod() + _gracePeriod; // 2 weeks + depreciatedFastbridge = fastbridge; + fastbridge = _fastbridge; } /** @dev Changes the `feeForJuror` property value of a specified subcourt. @@ -92,7 +110,19 @@ contract ForeignGatewayOnEthereum is IForeignGateway { feeForJuror[_subcourtID] = _feeForJuror; } - function createDispute(uint256 _choices, bytes calldata _extraData) external payable returns (uint256 disputeID) { + /** @dev Creates the `feeForJuror` property value for a new subcourt. + * @param _feeForJuror The new value for the `feeForJuror` property value. + */ + function createSubcourtJurorFee(uint256 _feeForJuror) external onlyByGovernor { + feeForJuror.push(_feeForJuror); + } + + function createDispute(uint256 _choices, bytes calldata _extraData) + external + payable + override + returns (uint256 disputeID) + { require(msg.value >= arbitrationCost(_extraData), "Not paid enough for arbitration"); disputeID = localDisputeID++; @@ -120,7 +150,7 @@ contract ForeignGatewayOnEthereum is IForeignGateway { emit DisputeCreation(disputeID, IArbitrable(msg.sender)); } - function arbitrationCost(bytes calldata _extraData) public view returns (uint256 cost) { + function arbitrationCost(bytes calldata _extraData) public view override returns (uint256 cost) { (uint96 subcourtID, uint256 minJurors) = extraDataToSubcourtIDMinJurors(_extraData); cost = feeForJuror[subcourtID] * minJurors; @@ -130,10 +160,12 @@ contract ForeignGatewayOnEthereum is IForeignGateway { * Relay the rule call from the home gateway to the arbitrable. */ function relayRule( + address _messageSender, bytes32 _disputeHash, uint256 _ruling, address _relayer - ) external onlyFromFastBridge { + ) external override onlyFromFastBridge { + require(_messageSender == homeGateway, "Only the homegateway is allowed."); DisputeData storage dispute = disputeHashtoDisputeData[_disputeHash]; require(dispute.id != 0, "Dispute does not exist"); @@ -146,7 +178,7 @@ contract ForeignGatewayOnEthereum is IForeignGateway { arbitrable.rule(dispute.id, _ruling); } - function withdrawFees(bytes32 _disputeHash) external { + function withdrawFees(bytes32 _disputeHash) external override { DisputeData storage dispute = disputeHashtoDisputeData[_disputeHash]; require(dispute.id != 0, "Dispute does not exist"); require(dispute.ruled, "Not ruled yet"); @@ -156,7 +188,7 @@ contract ForeignGatewayOnEthereum is IForeignGateway { payable(dispute.relayer).transfer(amount); } - function disputeHashToForeignID(bytes32 _disputeHash) external view returns (uint256) { + function disputeHashToForeignID(bytes32 _disputeHash) external view override returns (uint256) { return disputeHashtoDisputeData[_disputeHash].id; } diff --git a/contracts/src/gateway/ForeignGatewayOnGnosis.sol b/contracts/src/gateway/ForeignGatewayOnGnosis.sol deleted file mode 100644 index 6dc1cfd0e..000000000 --- a/contracts/src/gateway/ForeignGatewayOnGnosis.sol +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: MIT - -/** - * @authors: [@jaybuidl] - * @reviewers: [] - * @auditors: [] - * @bounties: [] - * @deployments: [] - */ - -pragma solidity ^0.8.0; - -import "./interfaces/IForeignGateway.sol"; - -/** - * Foreign Gateway on Gnosis chain - * Counterpart of `HomeGatewayToGnosis` - */ -abstract contract ForeignGatewayOnGnosis is IForeignGateway { - // TODO in prealpha-3 -} diff --git a/contracts/src/gateway/HomeGatewayToEthereum.sol b/contracts/src/gateway/HomeGateway.sol similarity index 77% rename from contracts/src/gateway/HomeGatewayToEthereum.sol rename to contracts/src/gateway/HomeGateway.sol index 6ce6cff60..431d0c8ea 100644 --- a/contracts/src/gateway/HomeGatewayToEthereum.sol +++ b/contracts/src/gateway/HomeGateway.sol @@ -17,18 +17,19 @@ import "./interfaces/IForeignGateway.sol"; import "./interfaces/IHomeGateway.sol"; /** - * Home Gateway to Ethereum - * Counterpart of `ForeignGatewayOnEthereum` + * Home Gateway + * Counterpart of `ForeignGateway` */ -contract HomeGatewayToEthereum is IHomeGateway { +contract HomeGateway is IHomeGateway { mapping(uint256 => bytes32) public disputeIDtoHash; mapping(bytes32 => uint256) public disputeHashtoID; - IArbitrator public arbitrator; + address public governor; + IArbitrator public immutable arbitrator; IFastBridgeSender public fastbridge; - address public foreignGateway; - uint256 public chainID; - uint256 public foreignChainID; + address public override foreignGateway; + uint256 public immutable override chainID; + uint256 public immutable override foreignChainID; struct RelayedData { uint256 arbitrationCost; @@ -37,19 +38,22 @@ contract HomeGatewayToEthereum is IHomeGateway { mapping(bytes32 => RelayedData) public disputeHashtoRelayedData; constructor( + address _governor, IArbitrator _arbitrator, IFastBridgeSender _fastbridge, address _foreignGateway, uint256 _foreignChainID ) { + governor = _governor; arbitrator = _arbitrator; fastbridge = _fastbridge; foreignGateway = _foreignGateway; foreignChainID = _foreignChainID; - + uint256 id; assembly { - sstore(chainID.slot, chainid()) + id := chainid() } + chainID = id; emit MetaEvidence(0, "BRIDGE"); } @@ -74,7 +78,7 @@ contract HomeGatewayToEthereum is IHomeGateway { uint256 _choices, bytes calldata _extraData, address _arbitrable - ) external payable { + ) external payable override { bytes32 disputeHash = keccak256( abi.encodePacked( _originalChainID, @@ -101,19 +105,27 @@ contract HomeGatewayToEthereum is IHomeGateway { emit Dispute(arbitrator, disputeID, 0, 0); } - function rule(uint256 _disputeID, uint256 _ruling) external { + function rule(uint256 _disputeID, uint256 _ruling) external override { require(msg.sender == address(arbitrator), "Only Arbitrator"); bytes32 disputeHash = disputeIDtoHash[_disputeID]; RelayedData memory relayedData = disputeHashtoRelayedData[disputeHash]; bytes4 methodSelector = IForeignGateway.relayRule.selector; - bytes memory data = abi.encodeWithSelector(methodSelector, disputeHash, _ruling, relayedData.relayer); + bytes memory data = abi.encode(disputeHash, _ruling, relayedData.relayer); + + fastbridge.sendFast(foreignGateway, methodSelector, data); + } - fastbridge.sendFast(foreignGateway, data); + /** @dev Changes the fastBridge, useful to increase the claim deposit. + * @param _fastbridge The address of the new fastBridge. + */ + function changeFastbridge(IFastBridgeSender _fastbridge) external { + require(governor == msg.sender, "Access not allowed: Governor only."); + fastbridge = _fastbridge; } - function disputeHashToHomeID(bytes32 _disputeHash) external view returns (uint256) { + function disputeHashToHomeID(bytes32 _disputeHash) external view override returns (uint256) { return disputeHashtoID[_disputeHash]; } } diff --git a/contracts/src/gateway/HomeGatewayToGnosis.sol b/contracts/src/gateway/HomeGatewayToGnosis.sol deleted file mode 100644 index 10a5b7858..000000000 --- a/contracts/src/gateway/HomeGatewayToGnosis.sol +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: MIT - -/** - * @authors: [@jaybuidl] - * @reviewers: [] - * @auditors: [] - * @bounties: [] - * @deployments: [] - */ - -pragma solidity ^0.8.0; - -import "./interfaces/IHomeGateway.sol"; - -/** - * Home Gateway to Gnosis chain - * Counterpart of `ForeignGatewayOnGnosis` - */ -abstract contract HomeGatewayToGnosis is IHomeGateway { - // TODO in prealpha-3 -} diff --git a/contracts/src/gateway/interfaces/IForeignGateway.sol b/contracts/src/gateway/interfaces/IForeignGateway.sol index a507b0ed3..c2e55ab99 100644 --- a/contracts/src/gateway/interfaces/IForeignGateway.sol +++ b/contracts/src/gateway/interfaces/IForeignGateway.sol @@ -11,6 +11,7 @@ interface IForeignGateway is IArbitrator { * Relay the rule call from the home gateway to the arbitrable. */ function relayRule( + address _messageSender, bytes32 _disputeHash, uint256 _ruling, address _forwarder diff --git a/contracts/src/gateway/interfaces/IForeignGatewayBase.sol b/contracts/src/gateway/interfaces/IForeignGatewayBase.sol new file mode 100644 index 000000000..d61baefb0 --- /dev/null +++ b/contracts/src/gateway/interfaces/IForeignGatewayBase.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../../bridge/interfaces/IFastBridgeReceiver.sol"; + +interface IForeignGatewayBase { + /** + * Receive the message from the home gateway. + */ + function receiveMessage(address _messageSender) external; + + function fastBridgeReceiver() external view returns (address); + + function homeChainID() external view returns (uint256); + + function homeGateway() external view returns (address); +} diff --git a/contracts/src/gateway/interfaces/IHomeGatewayBase.sol b/contracts/src/gateway/interfaces/IHomeGatewayBase.sol new file mode 100644 index 000000000..02e06337d --- /dev/null +++ b/contracts/src/gateway/interfaces/IHomeGatewayBase.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../../bridge/interfaces/IFastBridgeSender.sol"; + +interface IHomeGatewayBase { + /** + * Send fast message to foreign gateway. + */ + function sendFastMessage() external; + + function fastBridgeSender() external view returns (IFastBridgeSender); + + function foreignChainID() external view returns (uint256); + + function foreignGateway() external view returns (address); +} diff --git a/contracts/src/gateway/test/ForeignGatewayMock.sol b/contracts/src/gateway/test/ForeignGatewayMock.sol new file mode 100644 index 000000000..1c9337c1a --- /dev/null +++ b/contracts/src/gateway/test/ForeignGatewayMock.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT + +/** + * @authors: [@shotaronowhere] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ + +pragma solidity ^0.8.0; + +import "../interfaces/IForeignGatewayBase.sol"; + +/** + * Foreign Gateway Mock + * Counterpart of `HomeGatewayMock` + */ +contract ForeignGatewayMock is IForeignGatewayBase { + address public immutable fastBridgeReceiver; + address public immutable override homeGateway; + uint256 public immutable override homeChainID; + + uint256 public messageCount; + uint256 public data; + + constructor( + address _fastBridgeReceiver, + address _homeGateway, + uint256 _homeChainID + ) { + fastBridgeReceiver = _fastBridgeReceiver; + homeGateway = _homeGateway; + homeChainID = _homeChainID; + } + + modifier onlyFromFastBridge() { + require(address(fastBridgeReceiver) == msg.sender, "Fast Bridge only."); + _; + } + + /** + * Receive the message from the home gateway. + */ + function receiveMessage(address _messageSender) external onlyFromFastBridge { + require(_messageSender == homeGateway, "Only the homegateway is allowed."); + _receiveMessage(); + } + + /** + * Receive the message from the home gateway. + */ + function receiveMessage(address _messageSender, uint256 _data) external onlyFromFastBridge { + require(_messageSender == homeGateway, "Only the homegateway is allowed."); + _receiveMessage(_data); + } + + function _receiveMessage() internal { + messageCount++; + } + + function _receiveMessage(uint256 _data) internal { + messageCount++; + data = _data; + } +} diff --git a/contracts/src/gateway/test/HomeGatewayMock.sol b/contracts/src/gateway/test/HomeGatewayMock.sol new file mode 100644 index 000000000..c23a85c1e --- /dev/null +++ b/contracts/src/gateway/test/HomeGatewayMock.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT + +/** + * @authors: [@shotaronowhere] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ + +pragma solidity ^0.8.0; + +import "../interfaces/IForeignGatewayBase.sol"; +import "../interfaces/IHomeGatewayBase.sol"; + +/** + * Home Gateway + * Counterpart of `ForeignGatewayMock` + */ +contract HomeGatewayMock is IHomeGatewayBase { + IFastBridgeSender public immutable fastBridgeSender; + address public override foreignGateway; + uint256 public immutable override foreignChainID; + + struct RelayedData { + uint256 arbitrationCost; + address relayer; + } + mapping(bytes32 => RelayedData) public disputeHashtoRelayedData; + + constructor( + IFastBridgeSender _fastBridgeSender, + address _foreignGateway, + uint256 _foreignChainID + ) { + fastBridgeSender = _fastBridgeSender; + foreignGateway = _foreignGateway; + foreignChainID = _foreignChainID; + } + + function sendFastMessage() external { + bytes4 methodSelector = IForeignGatewayBase.receiveMessage.selector; + bytes memory data; + + fastBridgeSender.sendFast(foreignGateway, methodSelector, data); + } + + function sendFastMessage(uint256 _data) external { + bytes4 methodSelector = IForeignGatewayBase.receiveMessage.selector; + bytes memory data = abi.encode(_data); + + fastBridgeSender.sendFast(foreignGateway, methodSelector, data); + } +} diff --git a/contracts/src/libraries/gnosis-chain/Bytes.sol b/contracts/src/libraries/gnosis-chain/Bytes.sol new file mode 100644 index 000000000..a87e6e30a --- /dev/null +++ b/contracts/src/libraries/gnosis-chain/Bytes.sol @@ -0,0 +1,37 @@ +//https://github.com/poanetwork/tokenbridge-contracts/blob/master/contracts/libraries/Bytes.sol + +pragma solidity ^0.8.0; + +/** + * @title Bytes + * @dev Helper methods to transform bytes to other solidity types. + */ +library Bytes { + /** + * @dev Converts bytes array to bytes32. + * Truncates bytes array if its size is more than 32 bytes. + * NOTE: This function does not perform any checks on the received parameter. + * Make sure that the _bytes argument has a correct length, not less than 32 bytes. + * A case when _bytes has length less than 32 will lead to the undefined behaviour, + * since assembly will read data from memory that is not related to the _bytes argument. + * @param _bytes to be converted to bytes32 type + * @return result bytes32 type of the firsts 32 bytes array in parameter. + */ + function bytesToBytes32(bytes memory _bytes) internal pure returns (bytes32 result) { + assembly { + result := mload(add(_bytes, 32)) + } + } + + /** + * @dev Truncate bytes array if its size is more than 20 bytes. + * NOTE: Similar to the bytesToBytes32 function, make sure that _bytes is not shorter than 20 bytes. + * @param _bytes to be converted to address type + * @return addr address included in the firsts 20 bytes of the bytes array in parameter. + */ + function bytesToAddress(bytes memory _bytes) internal pure returns (address addr) { + assembly { + addr := mload(add(_bytes, 20)) + } + } +} diff --git a/contracts/test/bridge/merkle/MerkleTree.ts b/contracts/test/bridge/merkle/MerkleTree.ts index 1bc941eca..2bb1756ed 100644 --- a/contracts/test/bridge/merkle/MerkleTree.ts +++ b/contracts/test/bridge/merkle/MerkleTree.ts @@ -31,7 +31,7 @@ export class MerkleTree { * @return node The `sha3` (A.K.A. `keccak256`) hash of `first, ...params` as a 32-byte hex string. */ public static makeLeafNode(data: string): string { - const result = ethers.utils.keccak256(data); + const result = ethers.utils.sha256(data); if (!result) { throw new Error("Leaf node must not be empty"); diff --git a/contracts/test/bridge/merkle/index.ts b/contracts/test/bridge/merkle/index.ts index c6af5edd6..1c80daeeb 100644 --- a/contracts/test/bridge/merkle/index.ts +++ b/contracts/test/bridge/merkle/index.ts @@ -30,17 +30,17 @@ import { MerkleTree } from "./MerkleTree"; describe("Merkle", function () { describe("Sanity tests", async () => { - let merkleTreeHistory, merkleProof; + let merkleTreeExposed, merkleProofExposed; let data,nodes,mt; let rootOnChain,rootOffChain, proof; before("Deploying", async () => { - const merkleTreeHistoryFactory = await ethers.getContractFactory("MerkleTreeHistory"); - const merkleProofFactory = await ethers.getContractFactory("MerkleProof"); - merkleTreeHistory = await merkleTreeHistoryFactory.deploy(); - merkleProof = await merkleProofFactory.deploy(); - await merkleTreeHistory.deployed(); - await merkleProof.deployed(); + const merkleTreeExposedFactory = await ethers.getContractFactory("MerkleTreeExposed"); + const merkleProofExposedFactory = await ethers.getContractFactory("MerkleProofExposed"); + merkleTreeExposed = await merkleTreeExposedFactory.deploy(); + merkleProofExposed = await merkleProofExposedFactory.deploy(); + await merkleTreeExposed.deployed(); + await merkleProofExposed.deployed(); }); it("Merkle Root verification", async function () { @@ -51,19 +51,24 @@ describe("Merkle", function () { ]; nodes = []; for (var message of data) { - await merkleTreeHistory.append(message); + await merkleTreeExposed._appendMessage(message); nodes.push(MerkleTree.makeLeafNode(message)); } mt = new MerkleTree(nodes); rootOffChain = mt.getHexRoot(); - rootOnChain = await merkleTreeHistory.getMerkleRoot(); + rootOnChain = await merkleTreeExposed._getMerkleRoot(); + console.log("######"); + console.log(rootOffChain); + console.log(rootOnChain); + console.log("########################"); + expect(rootOffChain == rootOnChain).equal(true); }); it("Should correctly verify all nodes in the tree", async () => { for (var message of data) { - const leaf = ethers.utils.keccak256(message); + const leaf = ethers.utils.sha256(message); proof = mt.getHexProof(leaf); - const validation = await merkleProof.validateProof(proof, message,rootOnChain); + const validation = await merkleProofExposed._validateProof(proof, ethers.utils.sha256(message),rootOnChain); expect(validation).equal(true); expect(verify(proof, rootOffChain, leaf)).equal(true); } diff --git a/contracts/test/pre-alpha1/index.ts b/contracts/test/pre-alpha1/index.ts index a86d4548b..a34c59736 100644 --- a/contracts/test/pre-alpha1/index.ts +++ b/contracts/test/pre-alpha1/index.ts @@ -1,18 +1,21 @@ import { expect } from "chai"; import { deployments, ethers, getNamedAccounts, network } from "hardhat"; -import { BigNumber } from "ethers"; +import { BigNumber, utils } from "ethers"; import { IncrementalNG, PNK, KlerosCore, FastBridgeReceiverOnEthereum, - ForeignGatewayOnEthereum, + ForeignGateway, ArbitrableExample, - FastBridgeSenderToEthereum, - HomeGatewayToEthereum, + FastBridgeSender, + HomeGateway, + DisputeKitClassic, + InboxMock, } from "../../typechain-types"; /* eslint-disable no-unused-vars */ +/* eslint-disable no-unused-expressions */ // https://github.com/standard/standard/issues/690#issuecomment-278533482 describe("Demo pre-alpha1", function () { const ONE_TENTH_ETH = BigNumber.from(10).pow(17); @@ -21,35 +24,48 @@ describe("Demo pre-alpha1", function () { const ONE_THOUSAND_PNK = BigNumber.from(10).pow(21); const enum Period { - evidence, - commit, - vote, - appeal, - execution, + evidence, // Evidence can be submitted. This is also when drawing has to take place. + commit, // Jurors commit a hashed vote. This is skipped for courts without hidden votes. + vote, // Jurors reveal/cast their vote depending on whether the court has hidden votes or not. + appeal, // The dispute can be appealed. + execution, // Tokens are redistributed and the ruling is executed. + } + + const enum Phase { + staking, // Stake can be updated during this phase. + freezing, // Phase during which the dispute kits can undergo the drawing process. Staking is not allowed during this phase. + } + + const enum DisputeKitPhase { + resolving, // No disputes that need drawing. + generating, // Waiting for a random number. Pass as soon as it is ready. + drawing, // Jurors can be drawn. } let deployer, relayer, bridger, challenger, innocentBystander; - let ng, disputeKit, pnk, core, fastBridgeReceiver, foreignGateway, arbitrable, fastBridgeSender, homeGateway; + let ng, disputeKit, pnk, core, fastBridgeReceiver, foreignGateway, arbitrable, fastBridgeSender, homeGateway, inbox; - before("Setup", async () => { + beforeEach("Setup", async () => { deployer = (await getNamedAccounts()).deployer; relayer = (await getNamedAccounts()).relayer; + console.log("deployer:%s", deployer); console.log("named accounts: %O", await getNamedAccounts()); await deployments.fixture(["Arbitration", "ForeignGateway", "HomeGateway"], { fallbackToGlobal: true, - keepExistingDeployments: true, + keepExistingDeployments: false, }); ng = await ethers.getContract("IncrementalNG"); - disputeKit = await ethers.getContract("DisputeKitClassic"); + disputeKit = await ethers.getContract("DisputeKitClassic"); pnk = await ethers.getContract("PNK"); core = await ethers.getContract("KlerosCore"); fastBridgeReceiver = await ethers.getContract("FastBridgeReceiverOnEthereum"); - foreignGateway = await ethers.getContract("ForeignGatewayOnEthereum"); + foreignGateway = await ethers.getContract("ForeignGatewayOnEthereum"); arbitrable = await ethers.getContract("ArbitrableExample"); - fastBridgeSender = await ethers.getContract("FastBridgeSenderToEthereum"); - homeGateway = await ethers.getContract("HomeGatewayToEthereum"); + fastBridgeSender = await ethers.getContract("FastBridgeSenderToEthereumMock"); + homeGateway = await ethers.getContract("HomeGatewayToEthereum"); + inbox = await ethers.getContract("InboxMock"); }); it("RNG", async () => { @@ -65,8 +81,9 @@ describe("Demo pre-alpha1", function () { expect(rn).to.equal(rnOld.add(1)); }); - it("Demo", async () => { + it("Demo - Honest Claim - No Challenge - Bridger paid", async () => { const arbitrationCost = ONE_TENTH_ETH.mul(3); + const [bridger, challenger] = await ethers.getSigners(); await pnk.approve(core.address, ONE_THOUSAND_PNK.mul(100)); @@ -97,19 +114,161 @@ describe("Demo pre-alpha1", function () { expect(result.locked).to.equal(0); logJurorBalance(result); }); - - const tx = await foreignGateway.createDispute(2, "0x00", { value: arbitrationCost }); + const tx = await arbitrable.createDispute(2, "0x00", 0, { value: arbitrationCost }); const trace = await network.provider.send("debug_traceTransaction", [tx.hash]); const [disputeId] = ethers.utils.defaultAbiCoder.decode(["uint"], `0x${trace.returnValue}`); + console.log("Dispute Created"); expect(tx).to.emit(foreignGateway, "DisputeCreation"); //.withArgs(disputeId, deployer.address); expect(tx).to.emit(foreignGateway, "OutgoingDispute"); //.withArgs(disputeId, deployer.address); console.log(`disputeId: ${disputeId}`); + const lastBlock = await ethers.provider.getBlock(tx.blockNumber - 1); + const disputeHash = ethers.utils.solidityKeccak256( + ["uint", "bytes", "bytes", "uint", "uint", "bytes", "address"], + [31337, lastBlock.hash, ethers.utils.toUtf8Bytes("createDispute"), disputeId, 2, "0x00", arbitrable.address] + ); + const events = (await tx.wait()).events; + + // Relayer tx + const tx2 = await homeGateway + .connect(await ethers.getSigner(relayer)) + .relayCreateDispute(31337, lastBlock.hash, disputeId, 2, "0x00", arbitrable.address, { + value: arbitrationCost, + }); + expect(tx2).to.emit(homeGateway, "Dispute"); + const events2 = (await tx2.wait()).events; + + await network.provider.send("evm_increaseTime", [130]); // Wait for minStakingTime + await network.provider.send("evm_mine"); + + expect(await core.phase()).to.equal(Phase.staking); + expect(await disputeKit.phase()).to.equal(DisputeKitPhase.resolving); + expect(await disputeKit.disputesWithoutJurors()).to.equal(1); + expect(await disputeKit.isResolving()).to.equal(true); + console.log("KC phase: %d, DK phase: ", await core.phase(), await disputeKit.phase()); + + const disputesKitIDsThatNeedFreezing = await core.getDisputesKitIDsThatNeedFreezing(); + expect(disputesKitIDsThatNeedFreezing).to.be.deep.equal([BigNumber.from("1")]); + await core.passPhase(); // Staking -> Freezing + expect(await core.phase()).to.equal(Phase.freezing); + console.log("KC phase: %d, DK phase: ", await core.phase(), await disputeKit.phase()); + + await mineNBlocks(20); // Wait for 20 blocks finality + await disputeKit.passPhase(); // Resolving -> Generating + expect(await disputeKit.phase()).to.equal(DisputeKitPhase.generating); + console.log("KC phase: %d, DK phase: ", await core.phase(), await disputeKit.phase()); + + await disputeKit.passPhase(); // Generating -> Drawing + expect(await disputeKit.phase()).to.equal(DisputeKitPhase.drawing); + console.log("KC phase: %d, DK phase: ", await core.phase(), await disputeKit.phase()); + + const tx3 = await core.draw(0, 1000); + console.log("draw successful"); + const events3 = (await tx3.wait()).events; + + const roundInfo = await core.getRoundInfo(0, 0); + expect(roundInfo.drawnJurors).deep.equal([deployer, deployer, deployer]); + expect(roundInfo.tokensAtStakePerJuror).to.equal(ONE_HUNDRED_PNK.mul(2)); + expect(roundInfo.totalFeesForJurors).to.equal(arbitrationCost); + + expect((await core.disputes(0)).period).to.equal(Period.evidence); + + await core.passPeriod(0); + expect((await core.disputes(0)).period).to.equal(Period.vote); + await disputeKit.connect(await ethers.getSigner(deployer)).castVote(0, [0, 1, 2], 0, 0); + await core.passPeriod(0); + await core.passPeriod(0); + expect((await core.disputes(0)).period).to.equal(Period.execution); + await core.execute(0, 0, 1000); + const ticket1 = await fastBridgeSender.currentTicketID(); + expect(ticket1).to.equal(1); + + const tx4 = await core.executeRuling(0); + expect(tx4).to.emit(fastBridgeSender, "OutgoingMessage"); + + const OutgoingMessage = fastBridgeSender.filters.OutgoingMessage(); + const event5 = await fastBridgeSender.queryFilter(OutgoingMessage); + console.log("Executed ruling"); + + const ticket2 = await fastBridgeSender.currentTicketID(); + expect(ticket2).to.equal(2); + + const ticketID = event5[0].args.ticketID; + const messageHash = event5[0].args.messageHash; + const blockNumber = event5[0].args.blockNumber; + const messageData = event5[0].args.message; + + const bridgerBalance = await ethers.provider.getBalance(bridger.address); + // bridger tx starts - Honest Bridger + const tx5 = await fastBridgeReceiver.connect(bridger).claim(ticketID, messageHash, { value: ONE_TENTH_ETH }); + const blockNumBefore = await ethers.provider.getBlockNumber(); + const blockBefore = await ethers.provider.getBlock(blockNumBefore); + const timestampBefore = blockBefore.timestamp; + expect(tx5).to.emit(fastBridgeReceiver, "ClaimReceived").withArgs(ticketID, messageHash, timestampBefore); + + // wait for challenge period to pass + await network.provider.send("evm_increaseTime", [300]); + await network.provider.send("evm_mine"); + + const tx7 = await fastBridgeReceiver.connect(bridger).verifyAndRelay(ticketID, blockNumber, messageData); + expect(tx7).to.emit(arbitrable, "Ruling"); + + const tx8 = await fastBridgeReceiver.withdrawClaimDeposit(ticketID); + }); + + it("Demo - Honest Claim - Challenged - Bridger Paid, Challenger deposit forfeited", async () => { + const arbitrationCost = ONE_TENTH_ETH.mul(3); + const [bridger, challenger] = await ethers.getSigners(); + + await pnk.approve(core.address, ONE_THOUSAND_PNK.mul(100)); + + console.log("KC phase: %d, DK phase: ", await core.phase(), await disputeKit.phase()); + + await core.setStake(0, ONE_THOUSAND_PNK); + await core.getJurorBalance(deployer, 0).then((result) => { + expect(result.staked).to.equal(ONE_THOUSAND_PNK); + expect(result.locked).to.equal(0); + logJurorBalance(result); + }); + + await core.setStake(0, ONE_HUNDRED_PNK.mul(5)); + await core.getJurorBalance(deployer, 0).then((result) => { + expect(result.staked).to.equal(ONE_HUNDRED_PNK.mul(5)); + expect(result.locked).to.equal(0); + logJurorBalance(result); + }); + + await core.setStake(0, 0); + await core.getJurorBalance(deployer, 0).then((result) => { + expect(result.staked).to.equal(0); + expect(result.locked).to.equal(0); + logJurorBalance(result); + }); + + await core.setStake(0, ONE_THOUSAND_PNK.mul(4)); + await core.getJurorBalance(deployer, 0).then((result) => { + expect(result.staked).to.equal(ONE_THOUSAND_PNK.mul(4)); + expect(result.locked).to.equal(0); + logJurorBalance(result); + }); + const tx = await arbitrable.createDispute(2, "0x00", 0, { value: arbitrationCost }); + const trace = await network.provider.send("debug_traceTransaction", [tx.hash]); + const [disputeId] = ethers.utils.defaultAbiCoder.decode(["uint"], `0x${trace.returnValue}`); + console.log("Dispute Created"); + expect(tx).to.emit(foreignGateway, "DisputeCreation"); //.withArgs(disputeId, deployer.address); + expect(tx).to.emit(foreignGateway, "OutgoingDispute"); //.withArgs(disputeId, deployer.address); + console.log(`disputeId: ${disputeId}`); + + const eventOutgoingDispute = foreignGateway.filters.OutgoingDispute(); + const events = await foreignGateway.queryFilter(eventOutgoingDispute, "latest"); + const eventDisputeCreation = foreignGateway.filters.DisputeCreation(); + const events2 = await foreignGateway.queryFilter(eventDisputeCreation, "latest"); + const lastBlock = await ethers.provider.getBlock(tx.blockNumber - 1); const disputeHash = ethers.utils.solidityKeccak256( ["uint", "bytes", "bytes", "uint", "uint", "bytes", "address"], - [31337, lastBlock.hash, ethers.utils.toUtf8Bytes("createDispute"), disputeId, 2, "0x00", deployer] + [31337, lastBlock.hash, ethers.utils.toUtf8Bytes("createDispute"), disputeId, 2, "0x00", arbitrable.address] ); expect(events[0].event).to.equal("OutgoingDispute"); @@ -118,27 +277,47 @@ describe("Demo pre-alpha1", function () { expect(events[0].args.localDisputeID).to.equal(disputeId); expect(events[0].args._choices).to.equal(2); expect(events[0].args._extraData).to.equal("0x00"); - expect(events[0].args.arbitrable).to.equal(deployer); - - expect(events[1].event).to.equal("DisputeCreation"); - expect(events[1].args._arbitrable).to.equal(deployer); - expect(events[1].args._disputeID).to.equal(disputeId); + expect(events[0].args.arbitrable).to.equal(arbitrable.address); + expect(events2[0].event).to.equal("DisputeCreation"); + expect(events2[0].args._arbitrable).to.equal(arbitrable.address); + expect(events2[0].args._disputeID).to.equal(disputeId); // Relayer tx const tx2 = await homeGateway .connect(await ethers.getSigner(relayer)) - .relayCreateDispute(31337, lastBlock.hash, disputeId, 2, "0x00", deployer, { + .relayCreateDispute(31337, lastBlock.hash, disputeId, 2, "0x00", arbitrable.address, { value: arbitrationCost, }); + expect(tx2).to.emit(homeGateway, "Dispute"); - const events2 = (await tx2.wait()).events; - // console.log("event=%O", events2); + + await network.provider.send("evm_increaseTime", [130]); // Wait for minStakingTime + await network.provider.send("evm_mine"); + + expect(await core.phase()).to.equal(Phase.staking); + expect(await disputeKit.phase()).to.equal(DisputeKitPhase.resolving); + expect(await disputeKit.disputesWithoutJurors()).to.equal(1); + expect(await disputeKit.isResolving()).to.equal(true); + console.log("KC phase: %d, DK phase: ", await core.phase(), await disputeKit.phase()); + + let disputesKitIDsThatNeedFreezing = await core.getDisputesKitIDsThatNeedFreezing(); + expect(disputesKitIDsThatNeedFreezing).to.be.deep.equal([BigNumber.from("1")]); + await core.passPhase(); // Staking -> Freezing + expect(await core.phase()).to.equal(Phase.freezing); + console.log("KC phase: %d, DK phase: ", await core.phase(), await disputeKit.phase()); + + await mineNBlocks(20); // Wait for 20 blocks finality + await disputeKit.passPhase(); // Resolving -> Generating + expect(await disputeKit.phase()).to.equal(DisputeKitPhase.generating); + console.log("KC phase: %d, DK phase: ", await core.phase(), await disputeKit.phase()); + + await disputeKit.passPhase(); // Generating -> Drawing + expect(await disputeKit.phase()).to.equal(DisputeKitPhase.drawing); + console.log("KC phase: %d, DK phase: ", await core.phase(), await disputeKit.phase()); const tx3 = await core.draw(0, 1000); + console.log("draw successful"); const events3 = (await tx3.wait()).events; - console.log("event=%O", events3[0].args); - console.log("event=%O", events3[1].args); - console.log("event=%O", events3[2].args); const roundInfo = await core.getRoundInfo(0, 0); expect(roundInfo.drawnJurors).deep.equal([deployer, deployer, deployer]); @@ -148,7 +327,277 @@ describe("Demo pre-alpha1", function () { expect((await core.disputes(0)).period).to.equal(Period.evidence); await core.passPeriod(0); expect((await core.disputes(0)).period).to.equal(Period.vote); + + console.log("KC phase: %d, DK phase: ", await core.phase(), await disputeKit.phase()); + + await disputeKit.passPhase(); // Drawing -> Resolving + expect(await disputeKit.phase()).to.equal(DisputeKitPhase.resolving); + expect(await disputeKit.disputesWithoutJurors()).to.equal(0); + expect(await disputeKit.isResolving()).to.equal(true); + + disputesKitIDsThatNeedFreezing = await core.getDisputesKitIDsThatNeedFreezing(); + expect(disputesKitIDsThatNeedFreezing).to.be.deep.equal([BigNumber.from("1")]); + await core.passPhase(); // Freezing -> Staking + expect(await core.phase()).to.equal(Phase.staking); + + console.log("KC phase: %d, DK phase: ", await core.phase(), await disputeKit.phase()); + + await disputeKit.connect(await ethers.getSigner(deployer)).castVote(0, [0, 1, 2], 0, 0); + await core.passPeriod(0); + await core.passPeriod(0); + expect((await core.disputes(0)).period).to.equal(Period.execution); + await core.execute(0, 0, 1000); + const ticket1 = await fastBridgeSender.currentTicketID(); + expect(ticket1).to.equal(1); + + const tx4 = await core.executeRuling(0); + + expect(tx4).to.emit(fastBridgeSender, "OutgoingMessage"); + + console.log("Executed ruling"); + + const ticket2 = await fastBridgeSender.currentTicketID(); + expect(ticket2).to.equal(2); + const eventFilter = fastBridgeSender.filters.OutgoingMessage(); + const event5 = await fastBridgeSender.queryFilter(eventFilter, "latest"); + const event6 = await ethers.provider.getLogs(eventFilter); + + const ticketID = event5[0].args.ticketID.toNumber(); + const messageHash = event5[0].args.messageHash; + const blockNumber = event5[0].args.blockNumber; + const messageData = event5[0].args.message; + console.log("TicketID: %d", ticketID); + console.log("Block: %d", blockNumber); + console.log("Message Data: %s", messageData); + console.log("Message Hash: %s", messageHash); + const expectedHash = utils.keccak256( + utils.defaultAbiCoder.encode(["uint256", "uint256", "bytes"], [ticketID, blockNumber, messageData]) + ); + expect(messageHash).to.equal(expectedHash); + + const currentID = await fastBridgeSender.currentTicketID(); + expect(currentID).to.equal(2); + + // bridger tx starts + const tx5 = await fastBridgeReceiver.connect(bridger).claim(ticketID, messageHash, { value: ONE_TENTH_ETH }); + let blockNumBefore = await ethers.provider.getBlockNumber(); + let blockBefore = await ethers.provider.getBlock(blockNumBefore); + let timestampBefore = blockBefore.timestamp; + expect(tx5).to.emit(fastBridgeReceiver, "ClaimReceived").withArgs(ticketID, messageHash, timestampBefore); + + // Challenger tx starts + const tx6 = await fastBridgeReceiver.connect(challenger).challenge(ticketID, { value: ONE_TENTH_ETH }); + blockNumBefore = await ethers.provider.getBlockNumber(); + blockBefore = await ethers.provider.getBlock(blockNumBefore); + timestampBefore = blockBefore.timestamp; + console.log("Block: %d", blockNumBefore); + expect(tx6).to.emit(fastBridgeReceiver, "ClaimChallenged").withArgs(ticketID, timestampBefore); + + // wait for challenge period to pass + await network.provider.send("evm_increaseTime", [300]); + await network.provider.send("evm_mine"); + + await expect( + fastBridgeReceiver.connect(bridger).verifyAndRelay(ticketID, blockNumber, messageData) + ).to.be.revertedWith("Claim is challenged"); + + const data = await ethers.utils.defaultAbiCoder.decode(["address", "bytes"], messageData); + const tx7 = await fastBridgeSender + .connect(bridger) + .sendSafeFallbackMock(ticketID, foreignGateway.address, data[1], { gasLimit: 1000000 }); + expect(tx7).to.emit(fastBridgeSender, "L2ToL1TxCreated"); + expect(tx7).to.emit(arbitrable, "Ruling"); + + await expect(fastBridgeReceiver.withdrawChallengeDeposit(ticketID)).to.be.revertedWith( + "Claim verified: deposit forfeited" + ); + }); + + it("Demo - Dishonest Claim - Challenged - Bridger deposit forfeited, Challenger paid", async () => { + const arbitrationCost = ONE_TENTH_ETH.mul(3); + const [bridger, challenger] = await ethers.getSigners(); + + await pnk.approve(core.address, ONE_THOUSAND_PNK.mul(100)); + + await core.setStake(0, ONE_THOUSAND_PNK); + await core.getJurorBalance(deployer, 0).then((result) => { + expect(result.staked).to.equal(ONE_THOUSAND_PNK); + expect(result.locked).to.equal(0); + logJurorBalance(result); + }); + + await core.setStake(0, ONE_HUNDRED_PNK.mul(5)); + await core.getJurorBalance(deployer, 0).then((result) => { + expect(result.staked).to.equal(ONE_HUNDRED_PNK.mul(5)); + expect(result.locked).to.equal(0); + logJurorBalance(result); + }); + + await core.setStake(0, 0); + await core.getJurorBalance(deployer, 0).then((result) => { + expect(result.staked).to.equal(0); + expect(result.locked).to.equal(0); + logJurorBalance(result); + }); + + await core.setStake(0, ONE_THOUSAND_PNK.mul(4)); + await core.getJurorBalance(deployer, 0).then((result) => { + expect(result.staked).to.equal(ONE_THOUSAND_PNK.mul(4)); + expect(result.locked).to.equal(0); + logJurorBalance(result); + }); + const tx = await arbitrable.createDispute(2, "0x00", 0, { value: arbitrationCost }); + const trace = await network.provider.send("debug_traceTransaction", [tx.hash]); + const [disputeId] = ethers.utils.defaultAbiCoder.decode(["uint"], `0x${trace.returnValue}`); + console.log("Dispute Created"); + expect(tx).to.emit(foreignGateway, "DisputeCreation"); //.withArgs(disputeId, deployer.address); + expect(tx).to.emit(foreignGateway, "OutgoingDispute"); //.withArgs(disputeId, deployer.address); + console.log(`disputeId: ${disputeId}`); + const coreId = disputeId - 1; + // let events = await foreignGateway.queryFilter(OutgoingMessage); + + const lastBlock = await ethers.provider.getBlock(tx.blockNumber - 1); + const disputeHash = ethers.utils.solidityKeccak256( + ["uint", "bytes", "bytes", "uint", "uint", "bytes", "address"], + [31337, lastBlock.hash, ethers.utils.toUtf8Bytes("createDispute"), disputeId, 2, "0x00", arbitrable.address] + ); + + // Relayer tx + const tx2 = await homeGateway + .connect(await ethers.getSigner(relayer)) + .relayCreateDispute(31337, lastBlock.hash, disputeId, 2, "0x00", arbitrable.address, { + value: arbitrationCost, + }); + expect(tx2).to.emit(homeGateway, "Dispute"); + + await network.provider.send("evm_increaseTime", [130]); // Wait for minStakingTime + await network.provider.send("evm_mine"); + + expect(await core.phase()).to.equal(Phase.staking); + expect(await disputeKit.phase()).to.equal(DisputeKitPhase.resolving); + expect(await disputeKit.disputesWithoutJurors()).to.equal(1); + expect(await disputeKit.isResolving()).to.equal(true); + console.log("KC phase: %d, DK phase: ", await core.phase(), await disputeKit.phase()); + + const disputesKitIDsThatNeedFreezing = await core.getDisputesKitIDsThatNeedFreezing(); + expect(disputesKitIDsThatNeedFreezing).to.be.deep.equal([BigNumber.from("1")]); + await core.passPhase(); // Staking -> Freezing + expect(await core.phase()).to.equal(Phase.freezing); + console.log("KC phase: %d, DK phase: ", await core.phase(), await disputeKit.phase()); + + await mineNBlocks(20); // Wait for 20 blocks finality + await disputeKit.passPhase(); // Resolving -> Generating + expect(await disputeKit.phase()).to.equal(DisputeKitPhase.generating); + console.log("KC phase: %d, DK phase: ", await core.phase(), await disputeKit.phase()); + + await disputeKit.passPhase(); // Generating -> Drawing + expect(await disputeKit.phase()).to.equal(DisputeKitPhase.drawing); + console.log("KC phase: %d, DK phase: ", await core.phase(), await disputeKit.phase()); + + const tx3 = await core.draw(0, 1000); + console.log("draw successful"); + const events3 = (await tx3.wait()).events; + + const roundInfo = await core.getRoundInfo(coreId, 0); + expect(roundInfo.drawnJurors).deep.equal([deployer, deployer, deployer]); + expect(roundInfo.tokensAtStakePerJuror).to.equal(ONE_HUNDRED_PNK.mul(2)); + expect(roundInfo.totalFeesForJurors).to.equal(arbitrationCost); + + expect((await core.disputes(coreId)).period).to.equal(Period.evidence); + await core.passPeriod(coreId); + expect((await core.disputes(coreId)).period).to.equal(Period.vote); + + await disputeKit.connect(await ethers.getSigner(deployer)).castVote(coreId, [0, 1, 2], 0, 0); + await core.passPeriod(coreId); + await core.passPeriod(coreId); + expect((await core.disputes(coreId)).period).to.equal(Period.execution); + await core.execute(coreId, 0, 1000); + const ticket1 = await fastBridgeSender.currentTicketID(); + expect(ticket1).to.equal(1); + + const tx4 = await core.executeRuling(coreId); + + expect(tx4).to.emit(fastBridgeSender, "OutgoingMessage"); + + console.log("Executed ruling"); + + const ticket2 = await fastBridgeSender.currentTicketID(); + expect(ticket2).to.equal(2); + const eventFilter = fastBridgeSender.filters.OutgoingMessage(); + const event5 = await fastBridgeSender.queryFilter(eventFilter, "latest"); + const event6 = await ethers.provider.getLogs(eventFilter); + + const ticketID = event5[0].args.ticketID.toNumber(); + const messageHash = event5[0].args.messageHash; + const blockNumber = event5[0].args.blockNumber; + const messageData = event5[0].args.message; + console.log("TicketID: %d", ticketID); + console.log("Block: %d", blockNumber); + console.log("Message Data: %s", messageData); + console.log("Message Hash: %s", messageHash); + const expectedHash = utils.keccak256( + utils.defaultAbiCoder.encode(["uint256", "uint256", "bytes"], [ticketID, blockNumber, messageData]) + ); + expect(messageHash).to.equal(expectedHash); + + const currentID = await fastBridgeSender.currentTicketID(); + expect(currentID).to.equal(2); + + // bridger tx starts - bridger creates fakeData & fakeHash for dishonest ruling + const fakeData = "0x0000000000000000000000009a9f2ccfde556a7e9ff0848998aa4a0cfd8863ae000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000643496987923bd6a8aa2bdce6c5b15551665079e7acfb1b4d2149ac7e2f72260417d541b7f000000000000000000000000000000000000000000000000000000000000000100000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c800000000000000000000000000000000000000000000000000000000"; + const fakeHash = utils.keccak256( + utils.defaultAbiCoder.encode(["uint256", "uint256", "bytes"], [ticketID, blockNumber, fakeData]) + ); + + const tx5 = await fastBridgeReceiver.connect(bridger).claim(ticketID, fakeHash, { value: ONE_TENTH_ETH }); + let blockNumBefore = await ethers.provider.getBlockNumber(); + let blockBefore = await ethers.provider.getBlock(blockNumBefore); + let timestampBefore = blockBefore.timestamp; + console.log("Block: %d", blockNumBefore); + expect(tx5).to.emit(fastBridgeReceiver, "ClaimReceived").withArgs(ticketID, fakeHash, timestampBefore); + + // Challenger tx starts + const tx6 = await fastBridgeReceiver.connect(challenger).challenge(ticketID, { value: ONE_TENTH_ETH }); + blockNumBefore = await ethers.provider.getBlockNumber(); + blockBefore = await ethers.provider.getBlock(blockNumBefore); + timestampBefore = blockBefore.timestamp; + console.log("Block: %d", blockNumBefore); + expect(tx6).to.emit(fastBridgeReceiver, "ClaimChallenged").withArgs(ticketID, timestampBefore); + + // wait for challenge period to pass + await network.provider.send("evm_increaseTime", [300]); + await network.provider.send("evm_mine"); + + await expect( + fastBridgeReceiver.connect(bridger).verifyAndRelay(ticketID, blockNumber, fakeData) + ).to.be.revertedWith("Claim is challenged"); + + let data = await ethers.utils.defaultAbiCoder.decode(["address", "bytes"], fakeData); + + await expect( + fastBridgeSender + .connect(bridger) + .sendSafeFallbackMock(ticketID, foreignGateway.address, data[1], { gasLimit: 1000000 }) + ).to.be.revertedWith("Invalid message for ticketID."); + + data = await ethers.utils.defaultAbiCoder.decode(["address", "bytes"], messageData); + const tx8 = await fastBridgeSender + .connect(bridger) + .sendSafeFallbackMock(ticketID, foreignGateway.address, data[1], { gasLimit: 1000000 }); + expect(tx8).to.emit(fastBridgeSender, "L2ToL1TxCreated"); + expect(tx8).to.emit(arbitrable, "Ruling"); + + await expect(fastBridgeReceiver.withdrawClaimDeposit(ticketID)).to.be.revertedWith( + "Claim not verified: deposit forfeited" + ); + await fastBridgeReceiver.withdrawChallengeDeposit(ticketID); }); + + async function mineNBlocks(n) { + for (let index = 0; index < n; index++) { + await network.provider.send("evm_mine"); + } + } }); const logJurorBalance = function (result) {