diff --git a/contracts/deploy/01-foreign-chain.ts b/contracts/deploy/01-foreign-chain.ts index 71d07070a..93130c624 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, // 24 hours + challengePeriod: 14400, // 4 hours homeChainId: 42161, arbitrumInbox: "0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f", }, 4: { - claimDeposit: parseEther("0.1"), - challengeDuration: 120, // 2 min + deposit: parseEther("0.1"), + epochPeriod: 86400, // 24 hours + challengePeriod: 14400, // 4 hours homeChainId: 421611, arbitrumInbox: "0x578BAde599406A8fE3d24Fd7f7211c0911F5B29e", }, 31337: { - claimDeposit: parseEther("0.1"), - challengeDuration: 120, // 2 min + deposit: parseEther("0.1"), + epochPeriod: 86400, // 24 hours + challengePeriod: 14400, // 4 hours homeChainId: 31337, arbitrumInbox: "0x00", }, @@ -59,9 +62,7 @@ const deployForeignGateway: DeployFunction = async (hre: HardhatRuntimeEnvironme 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, arbitrumInbox } = paramsByChainId[chainId]; - const challengeDeposit = claimDeposit; - const bridgeAlpha = 5000; + const { deposit, epochPeriod, challengePeriod, homeChainId, arbitrumInbox } = paramsByChainId[chainId]; const homeChainIdAsBytes32 = hexZeroPad(homeChainId, 32); const homeGatewayAddress = getContractAddress(deployer, nonce); @@ -71,7 +72,7 @@ const deployForeignGateway: DeployFunction = async (hre: HardhatRuntimeEnvironme const fastBridgeSenderAddress = getContractAddress(deployer, nonce); console.log("calculated future FastSender for nonce %d: %s", nonce, fastBridgeSenderAddress); - nonce += 5; + nonce += 4; const inboxAddress = chainId === ForeignChains.HARDHAT ? getContractAddress(deployer, nonce) : arbitrumInbox; console.log("calculated future inboxAddress for nonce %d: %s", nonce, inboxAddress); @@ -79,19 +80,18 @@ const deployForeignGateway: DeployFunction = async (hre: HardhatRuntimeEnvironme const fastBridgeReceiver = await deploy("FastBridgeReceiverOnEthereum", { from: deployer, args: [ - deployer, + deposit, + epochPeriod, + challengePeriod, fastBridgeSenderAddress, - inboxAddress, - claimDeposit, - challengeDeposit, - challengeDuration, - bridgeAlpha, + inboxAddress ], log: true, }); const foreignGateway = await deploy("ForeignGatewayOnEthereum", { from: deployer, + contract: "ForeignGateway", args: [ deployer, fastBridgeReceiver.address, diff --git a/contracts/deploy/02-home-chain.ts b/contracts/deploy/02-home-chain.ts index 6beef9c4a..f4b45c5fa 100644 --- a/contracts/deploy/02-home-chain.ts +++ b/contracts/deploy/02-home-chain.ts @@ -3,6 +3,7 @@ import { DeployFunction } from "hardhat-deploy/types"; import { ethers } from "hardhat"; const HOME_CHAIN_IDS = [42161, 421611, 31337]; // ArbOne, ArbRinkeby, Hardhat +const epochPeriod = 86400; // 24 hours // TODO: use deterministic deployments @@ -22,7 +23,8 @@ const deployHomeGateway: DeployFunction = async (hre: HardhatRuntimeEnvironment) const fastBridgeSender = await deploy("FastBridgeSenderToEthereumMock", { from: deployer, - args: [deployer, fastBridgeReceiver.address, ethers.constants.AddressZero, arbSysMock.address], + contract: "FastBridgeSenderMock", + args: [epochPeriod, fastBridgeReceiver.address, arbSysMock.address], log: true, }); // nonce+0 @@ -32,44 +34,29 @@ const deployHomeGateway: DeployFunction = async (hre: HardhatRuntimeEnvironment) const homeGateway = await deploy("HomeGatewayToEthereum", { from: deployer, - args: [klerosCore.address, fastBridgeSender.address, foreignGateway.address, foreignChainId], + contract: "HomeGateway", + args: [deployer, klerosCore.address, fastBridgeSender.address, foreignGateway.address, foreignChainId], gasLimit: 4000000, log: true, }); // nonce+1 - const fastSender = await hre.ethers - .getContractAt("FastBridgeSenderToEthereumMock", fastBridgeSender.address) - .then((contract) => contract.fastBridgeSender()); - - if (fastSender === ethers.constants.AddressZero) { - await execute( - "FastBridgeSenderToEthereumMock", - { - from: deployer, - log: true, - }, - "changeFastSender", - homeGateway.address - ); - - 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 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, + }); }; // ---------------------------------------------------------------------------------------------- @@ -78,7 +65,8 @@ const deployHomeGateway: DeployFunction = async (hre: HardhatRuntimeEnvironment) const fastBridgeSender = await deploy("FastBridgeSenderToEthereum", { from: deployer, - args: [deployer, fastBridgeReceiver.address, ethers.constants.AddressZero], + contract: "FastBridgeSender", + args: [epochPeriod, fastBridgeReceiver.address], log: true, }); // nonce+0 @@ -87,22 +75,10 @@ const deployHomeGateway: DeployFunction = async (hre: HardhatRuntimeEnvironment) const foreignChainId = Number(await hre.companionNetworks.foreign.getChainId()); const homeGateway = await deploy("HomeGatewayToEthereum", { from: deployer, - args: [klerosCore.address, fastBridgeSender.address, foreignGateway.address, foreignChainId], + 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 - ); - } + }); // nonce+ }; // ---------------------------------------------------------------------------------------------- diff --git a/contracts/hardhat.config.ts b/contracts/hardhat.config.ts index 976d2b170..1a246ed37 100644 --- a/contracts/hardhat.config.ts +++ b/contracts/hardhat.config.ts @@ -141,9 +141,15 @@ const config: HardhatUserConfig = { relayer: { default: 1, }, + bridger: { + default: 2, + }, + challenger: { + default: 3, + }, }, gasReporter: { - enabled: process.env.REPORT_GAS !== undefined, + enabled: process.env.REPORT_GAS !== undefined ? process.env.REPORT_GAS === "true" : false, currency: "USD", }, verify: { @@ -166,7 +172,7 @@ const config: HardhatUserConfig = { }, }, docgen: { - path: './docs', + path: "./docs", clear: true, runOnCompile: false, }, diff --git a/contracts/src/bridge/FastBridgeReceiverOnEthereum.sol b/contracts/src/bridge/FastBridgeReceiverOnEthereum.sol index e70cd9f07..8058c6979 100644 --- a/contracts/src/bridge/FastBridgeReceiverOnEthereum.sol +++ b/contracts/src/bridge/FastBridgeReceiverOnEthereum.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT /** - * @authors: [@jaybuidl, @shalzz, @hrishibhat, @shotaronowhere] + * @authors: [@jaybuidl, @shotaronowhere, @hrishibhat] * @reviewers: [] * @auditors: [] * @bounties: [] @@ -10,252 +10,336 @@ pragma solidity ^0.8.0; -import "./SafeBridgeReceiverOnEthereum.sol"; import "./interfaces/IFastBridgeReceiver.sol"; +import "./interfaces/ISafeBridgeReceiver.sol"; +import "./canonical/arbitrum/IInbox.sol"; +import "./canonical/arbitrum/IOutbox.sol"; /** - * Fast Bridge Receiver on Ethereum from Arbitrum - * Counterpart of `FastBridgeSenderToEthereum` + * Fast Receiver On Ethereum + * Counterpart of `FastSenderFromArbitrum` */ -contract FastBridgeReceiverOnEthereum is SafeBridgeReceiverOnEthereum, IFastBridgeReceiver { +contract FastBridgeReceiverOnEthereum is IFastBridgeReceiver, ISafeBridgeReceiver { + // **************************************** // + // * * // + // * Ethereum Receiver Specific * // + // * * // + // **************************************** // + + // ************************************* // + // * Storage * // + // ************************************* // + + IInbox public immutable inbox; // The address of the Arbitrum Inbox contract. + + // ************************************* // + // * Views * // + // ************************************* // + + function isSentBySafeBridge() internal view override returns (bool) { + IOutbox outbox = IOutbox(inbox.bridge().activeOutbox()); + return outbox.l2ToL1Sender() == safeBridgeSender; + } + + /** + * @dev Constructor. + * @param _deposit The deposit amount to submit a claim in wei. + * @param _epochPeriod The duration of each epoch. + * @param _challengePeriod The duration of the period allowing to challenge a claim. + * @param _safeBridgeSender The address of the Safe Bridge Sender on the connecting chain. + * @param _inbox Ethereum receiver specific: The address of the inbox contract on Ethereum. + */ + constructor( + uint256 _deposit, + uint256 _epochPeriod, + uint256 _challengePeriod, + address _safeBridgeSender, + address _inbox // Ethereum receiver specific + ) { + deposit = _deposit; + epochPeriod = _epochPeriod; + challengePeriod = _challengePeriod; + safeBridgeSender = _safeBridgeSender; + inbox = IInbox(_inbox); // Ethereum receiver specific + } + + // ************************************** // + // * * // + // * General Receiver * // + // * * // + // ************************************** // + // ************************************* // // * Enums / Structs * // // ************************************* // struct Claim { - bytes32 messageHash; + bytes32 batchMerkleRoot; address bridger; - uint256 claimedAt; - uint256 claimDeposit; - bool verified; + uint32 timestamp; + bool honest; + bool verificationAttempted; + bool depositAndRewardWithdrawn; } struct Challenge { address challenger; - uint256 challengedAt; - uint256 challengeDeposit; - } - - struct Ticket { - Claim claim; - Challenge challenge; - bool relayed; + bool honest; + bool depositAndRewardWithdrawn; } // ************************************* // // * 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. + uint256 public immutable deposit; // The deposit required to submit a claim or challenge + uint256 public immutable override epochPeriod; // Epochs mark the period between potential batches of messages. + uint256 public immutable override challengePeriod; // Epochs mark the period between potential batches of messages. + address public immutable safeBridgeSender; // The address of the Safe Bridge Sender on the connecting chain. - /** - * @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. - */ - constructor( - address _governor, - address _safeBridgeSender, - address _inbox, - uint256 _claimDeposit, - uint256 _challengeDeposit, - uint256 _challengeDuration, - uint256 _alpha - ) SafeBridgeReceiverOnEthereum(_governor, _safeBridgeSender, _inbox) { - claimDeposit = _claimDeposit; - challengeDeposit = _challengeDeposit; - challengeDuration = _challengeDuration; - alpha = _alpha; - } + 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 // ************************************* // // * State 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 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 _epoch The epoch in which the batch to claim. + * @param _batchMerkleRoot The batch merkle root claimed for the last completed epoch. */ - 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, + function claim(uint256 _epoch, bytes32 _batchMerkleRoot) external payable override { + require(msg.value >= deposit, "Insufficient claim deposit."); + require(_batchMerkleRoot != bytes32(0), "Invalid claim."); + + uint256 epochNow = block.timestamp / epochPeriod; + // allow claim about current or previous epoch + require(_epoch == epochNow || _epoch == epochNow + 1, "Invalid Claim"); + require(claims[_epoch].bridger == address(0), "Claim already made for most recent finalized epoch."); + + claims[_epoch] = Claim({ + batchMerkleRoot: _batchMerkleRoot, bridger: msg.sender, - claimedAt: block.timestamp, - claimDeposit: msg.value, - verified: false + timestamp: uint32(block.timestamp), + honest: false, + verificationAttempted: false, + depositAndRewardWithdrawn: false }); - - emit ClaimReceived(_ticketID, _messageHash, block.timestamp); + emit ClaimReceived(_epoch, _batchMerkleRoot); } /** - * @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. + * @param _epoch The epoch of the claim to challenge. */ - 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 - }); + function challenge(uint256 _epoch) external payable override { + require(msg.value >= deposit, "Not enough claim deposit"); + + // Can only challenge the only active claim, about the previous epoch + require(claims[_epoch].bridger != address(0), "No claim to challenge."); + require(block.timestamp < uint256(claims[_epoch].timestamp) + challengePeriod, "Challenge period elapsed."); - emit ClaimChallenged(_ticketID, block.timestamp); + challenges[_epoch] = Challenge({challenger: msg.sender, honest: false, depositAndRewardWithdrawn: false}); + emit ClaimChallenged(_epoch); } /** - * @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 Resolves the optimistic claim for '_epoch'. + * @param _epoch The epoch of the optimistic claim. */ - 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"); + function verifyBatch(uint256 _epoch) external override { + Claim storage claim = claims[_epoch]; + require(claim.bridger != address(0), "Invalid epoch, no claim to verify."); + require(claim.verificationAttempted == false, "Optimistic verification already attempted."); require( - ticket.claim.messageHash == keccak256(abi.encode(_ticketID, _blockNumber, _messageData)), - "Invalid hash" + block.timestamp > uint256(claims[_epoch].timestamp) + challengePeriod, + "Challenge period has not yet elapsed." ); - 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 + if (challenges[_epoch].challenger == address(0)) { + // optimistic happy path + claim.honest = true; + fastInbox[_epoch] = claim.batchMerkleRoot; + emit BatchVerified(_epoch); + } + claim.verificationAttempted = true; } /** * 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 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 verifyAndRelaySafe( - uint256 _ticketID, - uint256 _blockNumber, - bytes calldata _messageData - ) external override { + function verifySafeBatch(uint256 _epoch, bytes32 _batchMerkleRoot) external override onlyFromSafeBridge { 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; + fastInbox[_epoch] = _batchMerkleRoot; + + // Corner cases: + // a) No claim submitted, + // b) Receiving the root of an empty batch, + // c) Batch root is zero. + if (claims[_epoch].bridger != address(0)) { + if (_batchMerkleRoot == claims[_epoch].batchMerkleRoot) { + claims[_epoch].honest = true; + } else { + claims[_epoch].honest = false; + challenges[_epoch].honest = true; + } } - - ticket.relayed = true; - require(_relay(_messageData), "Failed to call contract"); // Checks-Effects-Interaction + emit BatchSafeVerified(_epoch, claims[_epoch].honest, challenges[_epoch].honest); } /** - * @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. + * @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. */ - 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 + function verifyAndRelayMessage( + uint256 _epoch, + bytes32[] calldata _proof, + bytes calldata _message + ) external override { + bytes32 batchMerkleRoot = fastInbox[_epoch]; + require(batchMerkleRoot != bytes32(0), "Invalid epoch."); + + // Claim assessment if any + require(validateProof(_proof, sha256(_message), batchMerkleRoot) == true, "Invalid proof."); + require(_checkReplayAndRelay(_epoch, _message), "Failed to call contract"); // 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. + * @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 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. + function withdrawClaimDeposit(uint256 _epoch) external override { + Claim storage claim = claims[_epoch]; + + require(claim.bridger != address(0), "Claim does not exist"); + require(claim.honest == true, "Claim failed."); + require(claim.depositAndRewardWithdrawn == false, "Claim deposit and any rewards already withdrawn."); + + uint256 amount = deposit; + if (challenges[_epoch].challenger != address(0) && challenges[_epoch].honest == false) { + amount += deposit / 2; // half burnt + } + + claim.depositAndRewardWithdrawn = true; + emit ClaimDepositWithdrawn(_epoch, claim.bridger); + + payable(claim.bridger).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. + * @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 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"); + function withdrawChallengeDeposit(uint256 _epoch) external override { + Challenge storage challenge = challenges[_epoch]; - start = ticket.claim.claimedAt; - end = start + challengeDuration; - return (start, end); - } + require(challenge.challenger != address(0), "Challenge does not exist"); + require(challenge.honest == true, "Challenge failed."); + require(challenge.depositAndRewardWithdrawn == false, "Challenge deposit and rewards already withdrawn."); - // ************************ // - // * Governance * // - // ************************ // + uint256 amount = deposit; + if (claims[_epoch].bridger != address(0) && claims[_epoch].honest == false) { + amount += deposit / 2; // half burnt + } - function changeClaimDeposit(uint256 _claimDeposit) external onlyByGovernor { - claimDeposit = _claimDeposit; + challenge.depositAndRewardWithdrawn = true; + emit ChallengeDepositWithdrawn(_epoch, challenge.challenger); + + payable(challenge.challenger).send(amount); // Use of send to prevent reverting fallback. User is responsibility for accepting ETH. + // Checks-Effects-Interaction } - function changeChallengeDeposit(uint256 _challengeDeposit) external onlyByGovernor { - challengeDeposit = _challengeDeposit; + // ********************************** // + // * Merkle Proof * // + // ********************************** // + + /** + * @dev Validates membership of leaf in merkle tree with merkle proof. + * Note: Inlined from `merkle/MerkleProof.sol` for performance. + * @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 + ) internal pure returns (bool) { + return (merkleRoot == calculateRoot(proof, leaf)); } - function changeChallengePeriodDuration(uint256 _challengeDuration) external onlyByGovernor { - challengeDuration = _challengeDuration; + /** + * @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) private pure returns (bytes32) { + uint256 proofLength = proof.length; + require(proofLength <= 32, "Invalid Proof"); + bytes32 h = leaf; + for (uint256 i = 0; i < proofLength; i++) { + bytes32 proofElement = proof[i]; + // effecient hash + if (proofElement > h) + assembly { + mstore(0x00, h) + mstore(0x20, proofElement) + h := keccak256(0x00, 0x40) + } + else + assembly { + mstore(0x00, proofElement) + mstore(0x20, h) + h := keccak256(0x00, 0x40) + } + } + return h; } - function changeAlpha(uint256 _alpha) external onlyByGovernor { - alpha = _alpha; + // ************************************* // + // * Public Views * // + // ************************************* // + + /** + * @dev Returns the `start` and `end` time of challenge period for this `epoch`. + * @param _epoch The epoch of the claim to request the challenge period. + * @return start The start time of the challenge period. + * @return end The end time of the challenge period. + */ + function claimChallengePeriod(uint256 _epoch) 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 + start = claims[_epoch].timestamp; + end = start + challengePeriod; } // ************************ // // * Internal * // // ************************ // - function _relay(bytes calldata _messageData) internal returns (bool success) { + function _checkReplayAndRelay(uint256 _epoch, 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); + (uint256 nonce, address receiver, bytes memory data) = abi.decode(_messageData, (uint256, address, bytes)); + + uint256 index = nonce / 256; + uint256 offset = nonce % 256; + bytes32 replay = relayed[_epoch][index]; + require(((replay >> offset) & bytes32(uint256(1))) == 0, "Message already relayed"); + relayed[_epoch][index] = replay | bytes32(1 << offset); + emit MessageRelayed(_epoch, nonce); + + (success, ) = receiver.call(data); + // Checks-Effects-Interaction } } diff --git a/contracts/src/bridge/FastBridgeReceiverOnGnosis.sol b/contracts/src/bridge/FastBridgeReceiverOnGnosis.sol index ef8df9478..879010969 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, @shotaronowhere, @hrishibhat] * @reviewers: [] * @auditors: [] * @bounties: [] @@ -11,11 +11,333 @@ pragma solidity ^0.8.0; import "./interfaces/IFastBridgeReceiver.sol"; +import "./interfaces/ISafeBridgeReceiver.sol"; +import "./canonical/gnosis-chain/IAMB.sol"; /** - * Fast Bridge Receiver on Gnosis from Arbitrum - * Counterpart of `FastBridgeSenderToGnosis` + * Fast Receiver On Gnosis + * Counterpart of `FastSenderFromArbitrum` */ -abstract contract FastBridgeReceiverOnGnosis is IFastBridgeReceiver { - // TODO in prealpha-3 +contract FastBridgeReceiverOnGnosis is IFastBridgeReceiver, ISafeBridgeReceiver { + // **************************************** // + // * * // + // * Gnosis Receiver Specific * // + // * * // + // **************************************** // + + // ************************************* // + // * Storage * // + // ************************************* // + + IAMB public immutable amb; // The address of the AMB contract on GC. + + // ************************************* // + // * Views * // + // ************************************* // + + function isSentBySafeBridge() internal view override returns (bool) { + return (msg.sender == address(amb)) && (amb.messageSender() == safeBridgeSender); + } + + /** + * @dev Constructor. + * @param _deposit The deposit amount to submit a claim in wei. + * @param _epochPeriod The duration of each epoch. + * @param _challengePeriod The duration of the period allowing to challenge a claim. + * @param _safeBridgeSender The address of the Safe Bridge Sender on the connecting chain. + * @param _amb The AMB contract on Gnosis Chain. + */ + constructor( + uint256 _deposit, + uint256 _epochPeriod, + uint256 _challengePeriod, + address _safeBridgeSender, // Gnosis receiver specific + address _amb // Gnosis receiver specific + ) { + deposit = _deposit; + epochPeriod = _epochPeriod; + challengePeriod = _challengePeriod; + safeBridgeSender = _safeBridgeSender; + amb = IAMB(_amb); // Gnosis receiver specific + } + + // ************************************** // + // * * // + // * General Receiver * // + // * * // + // ************************************** // + + // ************************************* // + // * Enums / Structs * // + // ************************************* // + + struct Claim { + bytes32 batchMerkleRoot; + address bridger; + uint32 timestamp; + bool honest; + bool verificationAttempted; + bool depositAndRewardWithdrawn; + } + + struct Challenge { + address challenger; + bool honest; + bool depositAndRewardWithdrawn; + } + + // ************************************* // + // * Storage * // + // ************************************* // + + uint256 public immutable deposit; // The deposit required to submit a claim or challenge + uint256 public immutable override epochPeriod; // Epochs mark the period between potential batches of messages. + uint256 public immutable override challengePeriod; // Epochs mark the period between potential batches of messages. + address public immutable safeBridgeSender; // The address of the Safe Bridge Sender 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 + + // ************************************* // + // * 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 _epoch The epoch in which the batch to claim. + * @param _batchMerkleRoot The batch merkle root claimed for the last completed epoch. + */ + function claim(uint256 _epoch, bytes32 _batchMerkleRoot) external payable override { + require(msg.value >= deposit, "Insufficient claim deposit."); + require(_batchMerkleRoot != bytes32(0), "Invalid claim."); + + uint256 epochNow = block.timestamp / epochPeriod; + // allow claim about current or previous epoch + require(_epoch == epochNow || _epoch == epochNow + 1, "Invalid Claim"); + require(claims[_epoch].bridger == address(0), "Claim already made for most recent finalized epoch."); + + claims[_epoch] = Claim({ + batchMerkleRoot: _batchMerkleRoot, + bridger: msg.sender, + timestamp: uint32(block.timestamp), + honest: false, + verificationAttempted: false, + depositAndRewardWithdrawn: false + }); + emit ClaimReceived(_epoch, _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. + * @param _epoch The epoch of the claim to challenge. + */ + function challenge(uint256 _epoch) external payable override { + require(msg.value >= deposit, "Not enough claim deposit"); + + // Can only challenge the only active claim, about the previous epoch + require(claims[_epoch].bridger != address(0), "No claim to challenge."); + require(block.timestamp < uint256(claims[_epoch].timestamp) + challengePeriod, "Challenge period elapsed."); + + challenges[_epoch] = Challenge({challenger: msg.sender, honest: false, depositAndRewardWithdrawn: false}); + emit ClaimChallenged(_epoch); + } + + /** + * @dev Resolves the optimistic claim for '_epoch'. + * @param _epoch The epoch of the optimistic claim. + */ + function verifyBatch(uint256 _epoch) external override { + Claim storage claim = claims[_epoch]; + require(claim.bridger != address(0), "Invalid epoch, no claim to verify."); + require(claim.verificationAttempted == false, "Optimistic verification already attempted."); + require( + block.timestamp > uint256(claims[_epoch].timestamp) + challengePeriod, + "Challenge period has not yet elapsed." + ); + + if (challenges[_epoch].challenger == address(0)) { + // optimistic happy path + claim.honest = true; + fastInbox[_epoch] = claim.batchMerkleRoot; + emit BatchVerified(_epoch); + } + claim.verificationAttempted = true; + } + + /** + * 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 verifySafeBatch(uint256 _epoch, bytes32 _batchMerkleRoot) external override onlyFromSafeBridge { + require(isSentBySafeBridge(), "Access not allowed: SafeBridgeSender only."); + + fastInbox[_epoch] = _batchMerkleRoot; + + // Corner cases: + // a) No claim submitted, + // b) Receiving the root of an empty batch, + // c) Batch root is zero. + if (claims[_epoch].bridger != address(0)) { + if (_batchMerkleRoot == claims[_epoch].batchMerkleRoot) { + claims[_epoch].honest = true; + } else { + claims[_epoch].honest = false; + challenges[_epoch].honest = true; + } + } + emit BatchSafeVerified(_epoch, claims[_epoch].honest, challenges[_epoch].honest); + } + + /** + * @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. + */ + function verifyAndRelayMessage( + uint256 _epoch, + bytes32[] calldata _proof, + bytes calldata _message + ) external override { + bytes32 batchMerkleRoot = fastInbox[_epoch]; + require(batchMerkleRoot != bytes32(0), "Invalid epoch."); + + // Claim assessment if any + require(validateProof(_proof, sha256(_message), batchMerkleRoot) == true, "Invalid proof."); + require(_checkReplayAndRelay(_epoch, _message), "Failed to call contract"); // Checks-Effects-Interaction + } + + /** + * @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 storage claim = claims[_epoch]; + + require(claim.bridger != address(0), "Claim does not exist"); + require(claim.honest == true, "Claim not verified."); + require(claim.depositAndRewardWithdrawn == false, "Claim deposit and any rewards already withdrawn."); + + uint256 amount = deposit; + if (challenges[_epoch].challenger != address(0) && challenges[_epoch].honest == false) { + amount += deposit / 2; // half burnt + } + + claim.depositAndRewardWithdrawn = true; + emit ClaimDepositWithdrawn(_epoch, claim.bridger); + + 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 storage challenge = challenges[_epoch]; + + require(challenge.challenger != address(0), "Challenge does not exist"); + require(challenge.honest == true, "Challenge not verified."); + require(challenge.depositAndRewardWithdrawn == false, "Challenge deposit and rewards already withdrawn."); + + uint256 amount = deposit; + if (claims[_epoch].bridger != address(0) && claims[_epoch].honest == false) { + amount += deposit / 2; // half burnt + } + + challenge.depositAndRewardWithdrawn = true; + emit ChallengeDepositWithdrawn(_epoch, challenge.challenger); + + payable(challenge.challenger).send(amount); // Use of send to prevent reverting fallback. User is responsibility for accepting ETH. + // Checks-Effects-Interaction + } + + // ********************************** // + // * Merkle Proof * // + // ********************************** // + + /** + * @dev Validates membership of leaf in merkle tree with merkle proof. + * Note: Inlined from `merkle/MerkleProof.sol` for performance. + * @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 + ) internal pure returns (bool) { + return (merkleRoot == calculateRoot(proof, leaf)); + } + + /** + * @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) private pure returns (bytes32) { + uint256 proofLength = proof.length; + require(proofLength <= 32, "Invalid Proof"); + bytes32 h = leaf; + for (uint256 i = 0; i < proofLength; i++) { + bytes32 proofElement = proof[i]; + // effecient hash + if (proofElement > h) + assembly { + mstore(0x00, h) + mstore(0x20, proofElement) + h := keccak256(0x00, 0x40) + } + else + assembly { + mstore(0x00, proofElement) + mstore(0x20, h) + h := keccak256(0x00, 0x40) + } + } + return h; + } + + // ************************************* // + // * Public Views * // + // ************************************* // + + /** + * @dev Returns the `start` and `end` time of challenge period for this `epoch`. + * @param _epoch The epoch of the claim to request the challenge period. + * @return start The start time of the challenge period. + * @return end The end time of the challenge period. + */ + function claimChallengePeriod(uint256 _epoch) 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 + start = claims[_epoch].timestamp; + end = start + challengePeriod; + } + + // ************************ // + // * Internal * // + // ************************ // + + function _checkReplayAndRelay(uint256 _epoch, bytes calldata _messageData) internal returns (bool success) { + // Decode the receiver address from the data encoded by the IFastBridgeSender + (uint256 nonce, address receiver, bytes memory data) = abi.decode(_messageData, (uint256, address, bytes)); + + uint256 index = nonce / 256; + uint256 offset = nonce % 256; + bytes32 replay = relayed[_epoch][index]; + require(((replay >> offset) & bytes32(uint256(1))) == 0, "Message already relayed"); + relayed[_epoch][index] = replay | bytes32(1 << offset); + emit MessageRelayed(_epoch, nonce); + + (success, ) = receiver.call(data); + // Checks-Effects-Interaction + } } diff --git a/contracts/src/bridge/FastBridgeReceiverOnPolygon.sol b/contracts/src/bridge/FastBridgeReceiverOnPolygon.sol new file mode 100644 index 000000000..6436fcc4a --- /dev/null +++ b/contracts/src/bridge/FastBridgeReceiverOnPolygon.sol @@ -0,0 +1,346 @@ +// SPDX-License-Identifier: MIT + +/** + * @authors: [@jaybuidl, @shotaronowhere, @hrishibhat] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ + +pragma solidity ^0.8.0; + +import "./interfaces/IFastBridgeReceiver.sol"; +import "./interfaces/ISafeBridgeReceiver.sol"; +import "./canonical/polygon/FxBaseChildTunnel.sol"; + +/** + * Fast Receiver On Polygon + * Counterpart of `FastSenderFromArbitrum` + */ +contract FastBridgeReceiverOnPolygon is FxBaseChildTunnel, IFastBridgeReceiver, ISafeBridgeReceiver { + // **************************************** // + // * * // + // * Polygon Receiver Specific * // + // * * // + // **************************************** // + + // ************************************* // + // * Views * // + // ************************************* // + + function isSentBySafeBridge() internal view override returns (bool) { + return (msg.sender == fxChild); + } + + /** + * @dev Constructor. + * @param _deposit The deposit amount to submit a claim in wei. + * @param _epochPeriod The duration of each epoch. + * @param _challengePeriod The duration of the period allowing to challenge a claim. + * @param _safeBridgeSender The address of the Safe Bridge Sender on the connecting chain. fxRootTunnel contract in ethereum + * @param _fxChild The the fxChild contract on Polygon Chain. + */ + constructor( + uint256 _deposit, + uint256 _epochPeriod, + uint256 _challengePeriod, + address _safeBridgeSender, // Polygon receiver specific + address _fxChild // Polygon receiver specific + ) FxBaseChildTunnel(_fxChild) { + deposit = _deposit; + epochPeriod = _epochPeriod; + challengePeriod = _challengePeriod; + safeBridgeSender = _safeBridgeSender; + setFxRootTunnel(_safeBridgeSender); + } + + // ************************************** // + // * * // + // * General Receiver * // + // * * // + // ************************************** // + + // ************************************* // + // * Enums / Structs * // + // ************************************* // + + struct Claim { + bytes32 batchMerkleRoot; + address bridger; + uint32 timestamp; + bool honest; + bool verificationAttempted; + bool depositAndRewardWithdrawn; + } + + struct Challenge { + address challenger; + bool honest; + bool depositAndRewardWithdrawn; + } + + // ************************************* // + // * Storage * // + // ************************************* // + + uint256 public immutable deposit; // The deposit required to submit a claim or challenge + uint256 public immutable override epochPeriod; // Epochs mark the period between potential batches of messages. + uint256 public immutable override challengePeriod; // Epochs mark the period between potential batches of messages. + address public immutable safeBridgeSender; // The address of the Safe Bridge Sender 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 + + // ************************************* // + // * 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 _epoch The epoch in which the batch to claim. + * @param _batchMerkleRoot The batch merkle root claimed for the last completed epoch. + */ + function claim(uint256 _epoch, bytes32 _batchMerkleRoot) external payable override { + require(msg.value >= deposit, "Insufficient claim deposit."); + require(_batchMerkleRoot != bytes32(0), "Invalid claim."); + + uint256 epochNow = block.timestamp / epochPeriod; + // allow claim about current or previous epoch + require(_epoch == epochNow || _epoch == epochNow + 1, "Invalid Claim"); + require(claims[_epoch].bridger == address(0), "Claim already made for most recent finalized epoch."); + + claims[_epoch] = Claim({ + batchMerkleRoot: _batchMerkleRoot, + bridger: msg.sender, + timestamp: uint32(block.timestamp), + honest: false, + verificationAttempted: false, + depositAndRewardWithdrawn: false + }); + emit ClaimReceived(_epoch, _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. + * @param _epoch The epoch of the claim to challenge. + */ + function challenge(uint256 _epoch) external payable override { + require(msg.value >= deposit, "Not enough claim deposit"); + + // Can only challenge the only active claim, about the previous epoch + require(claims[_epoch].bridger != address(0), "No claim to challenge."); + require(block.timestamp < uint256(claims[_epoch].timestamp) + challengePeriod, "Challenge period elapsed."); + + challenges[_epoch] = Challenge({challenger: msg.sender, honest: false, depositAndRewardWithdrawn: false}); + emit ClaimChallenged(_epoch); + } + + /** + * @dev Resolves the optimistic claim for '_epoch'. + * @param _epoch The epoch of the optimistic claim. + */ + function verifyBatch(uint256 _epoch) external override { + Claim storage claim = claims[_epoch]; + require(claim.bridger != address(0), "Invalid epoch, no claim to verify."); + require(claim.verificationAttempted == false, "Optimistic verification already attempted."); + require( + block.timestamp > uint256(claims[_epoch].timestamp) + challengePeriod, + "Challenge period has not yet elapsed." + ); + + if (challenges[_epoch].challenger == address(0)) { + // optimistic happy path + claim.honest = true; + fastInbox[_epoch] = claim.batchMerkleRoot; + emit BatchVerified(_epoch); + } + claim.verificationAttempted = true; + } + + /** + * 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 verifySafeBatch(uint256 _epoch, bytes32 _batchMerkleRoot) external override onlyFromSafeBridge { + // TODO + revert("Not implemented"); + } + + /** + * @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. + */ + function verifyAndRelayMessage( + uint256 _epoch, + bytes32[] calldata _proof, + bytes calldata _message + ) external override { + bytes32 batchMerkleRoot = fastInbox[_epoch]; + require(batchMerkleRoot != bytes32(0), "Invalid epoch."); + + // Claim assessment if any + require(validateProof(_proof, sha256(_message), batchMerkleRoot) == true, "Invalid proof."); + require(_checkReplayAndRelay(_epoch, _message), "Failed to call contract"); // Checks-Effects-Interaction + } + + /** + * @dev Handles incoming messages from Ethereum via the canonical Polygon bridge. + * @param _stateId The epoch in which the message was batched by the bridge. + * @param _sender The merkle proof to prove the membership of the message and nonce in the merkle tree for the epoch. + * @param _data The data on the cross-domain chain for the message. + */ + function _processMessageFromRoot( + uint256 _stateId, + address _sender, + bytes memory _data + ) internal override validateSender(_sender) { + // TODO + revert("Not implemented"); + // (uint256 _epoch, bytes32 _batchMerkleRoot) = abi.decode(data, (uint256, bytes32)); + + // 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 storage claim = claims[_epoch]; + + require(claim.bridger != address(0), "Claim does not exist"); + require(claim.honest == true, "Claim not verified."); + require(claim.depositAndRewardWithdrawn == false, "Claim deposit and any rewards already withdrawn."); + + uint256 amount = deposit; + if (challenges[_epoch].challenger != address(0) && challenges[_epoch].honest == false) { + amount += deposit / 2; // half burnt + } + + claim.depositAndRewardWithdrawn = true; + emit ClaimDepositWithdrawn(_epoch, claim.bridger); + + 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 storage challenge = challenges[_epoch]; + + require(challenge.challenger != address(0), "Challenge does not exist"); + require(challenge.honest == true, "Challenge not verified."); + require(challenge.depositAndRewardWithdrawn == false, "Challenge deposit and rewards already withdrawn."); + + uint256 amount = deposit; + if (claims[_epoch].bridger != address(0) && claims[_epoch].honest == false) { + amount += deposit / 2; // half burnt + } + + challenge.depositAndRewardWithdrawn = true; + emit ChallengeDepositWithdrawn(_epoch, challenge.challenger); + + payable(challenge.challenger).send(amount); // Use of send to prevent reverting fallback. User is responsibility for accepting ETH. + // Checks-Effects-Interaction + } + + // ********************************** // + // * Merkle Proof * // + // ********************************** // + + /** + * @dev Validates membership of leaf in merkle tree with merkle proof. + * Note: Inlined from `merkle/MerkleProof.sol` for performance. + * @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 + ) internal pure returns (bool) { + return (merkleRoot == calculateRoot(proof, leaf)); + } + + /** + * @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) private pure returns (bytes32) { + uint256 proofLength = proof.length; + require(proofLength <= 32, "Invalid Proof"); + bytes32 h = leaf; + for (uint256 i = 0; i < proofLength; i++) { + bytes32 proofElement = proof[i]; + // effecient hash + if (proofElement > h) + assembly { + mstore(0x00, h) + mstore(0x20, proofElement) + h := keccak256(0x00, 0x40) + } + else + assembly { + mstore(0x00, proofElement) + mstore(0x20, h) + h := keccak256(0x00, 0x40) + } + } + return h; + } + + // ************************************* // + // * Public Views * // + // ************************************* // + + /** + * @dev Returns the `start` and `end` time of challenge period for this `epoch`. + * @param _epoch The epoch of the claim to request the challenge period. + * @return start The start time of the challenge period. + * @return end The end time of the challenge period. + */ + function claimChallengePeriod(uint256 _epoch) 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 + start = claims[_epoch].timestamp; + end = start + challengePeriod; + } + + // ************************ // + // * Internal * // + // ************************ // + + function _checkReplayAndRelay(uint256 _epoch, bytes calldata _messageData) internal returns (bool success) { + // Decode the receiver address from the data encoded by the IFastBridgeSender + (uint256 nonce, address receiver, bytes memory data) = abi.decode(_messageData, (uint256, address, bytes)); + + uint256 index = nonce / 256; + uint256 offset = nonce % 256; + bytes32 replay = relayed[_epoch][index]; + require(((replay >> offset) & bytes32(uint256(1))) == 0, "Message already relayed"); + relayed[_epoch][index] = replay | bytes32(1 << offset); + emit MessageRelayed(_epoch, nonce); + + (success, ) = receiver.call(data); + // Checks-Effects-Interaction + } +} diff --git a/contracts/src/bridge/FastBridgeSender.sol b/contracts/src/bridge/FastBridgeSender.sol new file mode 100644 index 000000000..21ca6c549 --- /dev/null +++ b/contracts/src/bridge/FastBridgeSender.sol @@ -0,0 +1,256 @@ +// SPDX-License-Identifier: MIT + +/** + * @authors: [@jaybuidl, @shotaronowhere] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ + +pragma solidity ^0.8.0; + +import "./interfaces/IFastBridgeSender.sol"; +import "./interfaces/ISafeBridgeSender.sol"; +import "./interfaces/ISafeBridgeReceiver.sol"; +import "./canonical/arbitrum/IArbSys.sol"; // Arbitrum sender specific + +/** + * Fast Bridge Sender + * Counterpart of `FastBridgeReceiver` + */ +contract FastBridgeSender is IFastBridgeSender, ISafeBridgeSender { + // **************************************** // + // * * // + // * Arbitrum Sender Specific * // + // * * // + // **************************************** // + + // ************************************* // + // * Storage * // + // ************************************* // + + IArbSys public constant ARB_SYS = IArbSys(address(100)); + + /** + * @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 { + require(_epoch <= currentBatchID, "Invalid epoch."); + bytes32 batchMerkleRoot = fastOutbox[_epoch]; + + // Safe Bridge message envelope + bytes4 methodSelector = ISafeBridgeReceiver.verifySafeBatch.selector; + bytes memory safeMessageData = abi.encodeWithSelector(methodSelector, _epoch, batchMerkleRoot); + + bytes32 ticketID = _sendSafe(safeBridgeReceiver, safeMessageData); + emit SentSafe(_epoch, ticketID); + } + + function _sendSafe(address _receiver, bytes memory _calldata) internal override returns (bytes32) { + uint256 ticketID = ARB_SYS.sendTxToL1(_receiver, _calldata); + + return bytes32(ticketID); + } + + /** + * @dev Constructor. + * @param _epochPeriod The duration between epochs. + * @param _safeBridgeReceiver The the Safe Bridge Router on Ethereum to the receiving chain. + */ + constructor(uint256 _epochPeriod, address _safeBridgeReceiver) { + epochPeriod = _epochPeriod; + safeBridgeReceiver = _safeBridgeReceiver; + unchecked { + currentBatchID = block.timestamp / _epochPeriod - 1; + } + } + + // ************************************** // + // * * // + // * General Sender * // + // * * // + // ************************************** // + + // ************************************* // + // * Storage * // + // ************************************* // + + uint256 public immutable epochPeriod; // Epochs mark the period between potential batches of messages. + uint256 public currentBatchID; + mapping(uint256 => bytes32) public fastOutbox; // epoch count => merkle root of batched messages + address public immutable safeBridgeReceiver; + + // merkle tree representation of a batch of messages + // supports 2^64 messages. + bytes32[64] public batch; + uint256 public batchSize; + + // ************************************* // + // * 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 _calldata The receiving domain encoded message data / function arguments. + */ + function sendFast(address _receiver, bytes memory _calldata) external override { + (bytes32 fastMessageHash, bytes memory fastMessage) = _encode(_receiver, _calldata); + emit MessageReceived(fastMessage, fastMessageHash); + appendMessage(fastMessageHash); // add message to merkle tree + } + + /** + * Sends a batch of arbitrary message from one domain to another via the fast bridge mechanism. + */ + function sendBatch() external override { + uint256 epoch = block.timestamp / epochPeriod; + require(fastOutbox[epoch] == 0, "Batch already sent for the current epoch."); + require(batchSize > 0, "No messages to send."); + + // set merkle root in outbox + bytes32 batchMerkleRoot = getMerkleRoot(); + fastOutbox[epoch] = batchMerkleRoot; + emit BatchOutgoing(currentBatchID, batchSize, epoch, batchMerkleRoot); + + // reset + batchSize = 0; + currentBatchID = epoch; + } + + // ************************ // + // * Internal * // + // ************************ // + + function _encode(address _receiver, bytes memory _calldata) + internal + view + returns (bytes32 fastMessageHash, bytes memory fastMessage) + { + // Encode the receiver address with the function signature + arguments i.e calldata + bytes32 sender = bytes32(bytes20(msg.sender)); + bytes32 receiver = bytes32(bytes20(_receiver)); + uint256 nonce = batchSize; + // add sender and receiver with proper function selector formatting + // [length][receiver: 1 slot padded][offset][function selector: 4 bytes no padding][msg.sender: 1 slot padded][function arguments: 1 slot padded] + assembly { + fastMessage := mload(0x40) // free memory pointer + let lengthCalldata := mload(_calldata) // calldata length + let lengthFastMesssageCalldata := add(lengthCalldata, 0x20) // add msg.sender + let lengthEncodedMessage := add(lengthFastMesssageCalldata, 0x80) // 1 offsets, receiver, and lengthFastMesssageCalldata + mstore(fastMessage, lengthEncodedMessage) // bytes length + mstore(add(fastMessage, 0x20), nonce) // nonce + mstore(add(fastMessage, 0x4c), receiver) // receiver + mstore(add(fastMessage, 0x60), 0x60) // offset + mstore(add(fastMessage, 0x80), lengthFastMesssageCalldata) // fast message length + mstore( + add(fastMessage, 0xa0), + and(mload(add(_calldata, 0x20)), 0xFFFFFFFF00000000000000000000000000000000000000000000000000000000) + ) // function selector + mstore(add(fastMessage, 0xb0), sender) // sender + + let _cursor := add(fastMessage, 0xc4) // begin copying arguments of function call + let _cursorCalldata := add(_calldata, 0x24) // beginning of arguments + + // copy all arguments + for { + let j := 0x00 + } lt(j, lengthCalldata) { + j := add(j, 0x20) + } { + mstore(_cursor, mload(add(_cursorCalldata, j))) + _cursor := add(_cursor, 0x20) + } + // update free pointer + mstore(0x40, _cursor) + } + // Compute the hash over the message header (batchSize as nonce) and body (fastMessage). + fastMessageHash = sha256(fastMessage); + } + + // ********************************* // + // * Merkle Tree * // + // ********************************* // + + /** + * @dev Append data into merkle tree. + * `O(log(n))` where `n` is the number of leaves. + * Note: Although each insertion is O(log(n)), complexity of n insertions is O(n). + * Note: Inlined from `merkle/MerkleTree.sol` for performance. + * @param leaf The leaf (already hashed) to insert in the merkle tree. + */ + function appendMessage(bytes32 leaf) internal { + unchecked { + // 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; + uint256 height; + while ((hashBitField & 1) == 0) { + bytes32 node = batch[height]; + if (node > leaf) + assembly { + // efficient hash + mstore(0x00, leaf) + mstore(0x20, node) + leaf := keccak256(0x00, 0x40) + } + else + assembly { + // efficient hash + mstore(0x00, node) + mstore(0x20, leaf) + leaf := keccak256(0x00, 0x40) + } + hashBitField /= 2; + height++; + } + batch[height] = leaf; + } + } + + /** + * @dev Gets the current merkle root. + * `O(log(n))` where `n` is the number of leaves. + * Note: Inlined from `merkle/MerkleTree.sol` for performance. + */ + function getMerkleRoot() internal view returns (bytes32) { + unchecked { + bytes32 node; + uint256 size = batchSize; + uint256 height = 0; + bool isFirstHash = true; + while (size > 0) { + if ((size & 1) == 1) { + // avoid redundant calculation + if (isFirstHash) { + node = batch[height]; + isFirstHash = false; + } else { + bytes32 hash = batch[height]; + // efficient hash + if (hash > node) + assembly { + mstore(0x00, node) + mstore(0x20, hash) + node := keccak256(0x00, 0x40) + } + else + assembly { + mstore(0x00, hash) + mstore(0x20, node) + node := keccak256(0x00, 0x40) + } + } + } + size /= 2; + height++; + } + return node; + } + } +} diff --git a/contracts/src/bridge/SafeBridgeRouterToGnosis.sol b/contracts/src/bridge/SafeBridgeRouterToGnosis.sol new file mode 100644 index 000000000..0b04ca4f1 --- /dev/null +++ b/contracts/src/bridge/SafeBridgeRouterToGnosis.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT + +/** + * @authors: [@shotaronowhere, @jaybuidl] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ + +pragma solidity ^0.8.0; + +import "./interfaces/ISafeBridgeRouter.sol"; +import "./canonical/gnosis-chain/IAMB.sol"; +import "./canonical/arbitrum/IInbox.sol"; +import "./canonical/arbitrum/IOutbox.sol"; + +/** + * Router on Ethereum from Arbitrum to Gnosis Chain. + */ +contract SafeBridgeRouter is ISafeBridgeRouter { + // ************************************* // + // * 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 address of the AMB contract on Ethereum. + * @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 verifySafeBatch(uint256 _epoch, bytes32 _batchMerkleRoot) external override onlyFromSafeBridge { + require(isSentBySafeBridge(), "Access not allowed: SafeBridgeSender only."); + + bytes4 methodSelector = ISafeBridgeReceiver.verifySafeBatch.selector; + bytes memory safeMessageData = abi.encodeWithSelector(methodSelector, _epoch, _batchMerkleRoot); + + // replace maxGasPerTx with safe level for production deployment + bytes32 ticketID = _sendSafe(fastBridgeReceiverOnGnosisChain, safeMessageData); + emit SafeRelayed(ticketID); + } + + 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/SafeBridgeRouterToPolygon.sol b/contracts/src/bridge/SafeBridgeRouterToPolygon.sol new file mode 100644 index 000000000..56653e4bb --- /dev/null +++ b/contracts/src/bridge/SafeBridgeRouterToPolygon.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: MIT + +/** + * @authors: [@shotaronowhere, @hrishibhat, @jaybuidl] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ + +pragma solidity ^0.8.0; + +import "./interfaces/ISafeBridgeRouter.sol"; +import "./canonical/polygon/FxBaseRootTunnel.sol"; +import "./canonical/arbitrum/IInbox.sol"; +import "./canonical/arbitrum/IOutbox.sol"; + +/** + * Router on Ethereum from Arbitrum to Polygon Chain. + */ +contract SafeBridgeRouterToPolygon is ISafeBridgeRouter, FxBaseRootTunnel { + // ************************************* // + // * Storage * // + // ************************************* // + + IInbox public immutable inbox; // The address of the Arbitrum Inbox contract. + address public immutable safeBridgeSender; // The address of the Safe Bridge sender on Arbitrum. + + /** + * @dev Constructor. + * @param _inbox The address of the inbox contract on Ethereum. + * @param _fxRoot The address of the fxRoot contract in Ethereum. + * @param _safeBridgeSender The safe bridge sender on Arbitrum. + * @param _fastBridgeReceiverOnPolygon The fast bridge receiver on Polygon Chain. + */ + constructor( + IInbox _inbox, + address _checkpointManager, + address _fxRoot, + address _safeBridgeSender, + address _fastBridgeReceiverOnPolygon + ) FxBaseRootTunnel(_checkpointManager, _fxRoot) { + inbox = _inbox; + safeBridgeSender = _safeBridgeSender; + setFxChildTunnel(_fastBridgeReceiverOnPolygon); + } + + /** + * 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 batchP merkle root for the epoch sent by the safe bridge. * @return Unique id to track the message request/transaction. + */ + function verifySafeBatch(uint256 _epoch, bytes32 _batchMerkleRoot) external override onlyFromSafeBridge { + // Note: fxRoot sends message directly to fxchild hence no need for encodeWithSelector + bytes memory safeMessageData = abi.encode(_epoch, _batchMerkleRoot); + + // replace maxGasPerTx with safe level for production deployment + _sendSafe(safeMessageData); + // TODO: Consider an event emit here + } + + function _sendSafe(bytes memory _calldata) internal { + _sendMessageToChild(_calldata); + } + + function _processMessageFromChild(bytes memory message) internal override { + // TODO + revert("Not implemented"); + } + + function _sendSafe(address _receiver, bytes memory _calldata) internal override returns (bytes32) { + // TODO + revert("Not implemented"); + } + + // ************************************* // + // * Views * // + // ************************************* // + + function isSentBySafeBridge() internal view override returns (bool) { + IOutbox outbox = IOutbox(inbox.bridge().activeOutbox()); + return outbox.l2ToL1Sender() == safeBridgeSender; + } +} diff --git a/contracts/src/bridge/interfaces/arbitrum/AddressAliasHelper.sol b/contracts/src/bridge/canonical/arbitrum/AddressAliasHelper.sol similarity index 100% rename from contracts/src/bridge/interfaces/arbitrum/AddressAliasHelper.sol rename to contracts/src/bridge/canonical/arbitrum/AddressAliasHelper.sol diff --git a/contracts/src/bridge/interfaces/arbitrum/IArbRetryableTx.sol b/contracts/src/bridge/canonical/arbitrum/IArbRetryableTx.sol similarity index 100% rename from contracts/src/bridge/interfaces/arbitrum/IArbRetryableTx.sol rename to contracts/src/bridge/canonical/arbitrum/IArbRetryableTx.sol diff --git a/contracts/src/bridge/interfaces/arbitrum/IArbSys.sol b/contracts/src/bridge/canonical/arbitrum/IArbSys.sol similarity index 100% rename from contracts/src/bridge/interfaces/arbitrum/IArbSys.sol rename to contracts/src/bridge/canonical/arbitrum/IArbSys.sol diff --git a/contracts/src/bridge/interfaces/arbitrum/IInbox.sol b/contracts/src/bridge/canonical/arbitrum/IInbox.sol similarity index 100% rename from contracts/src/bridge/interfaces/arbitrum/IInbox.sol rename to contracts/src/bridge/canonical/arbitrum/IInbox.sol diff --git a/contracts/src/bridge/interfaces/arbitrum/IOutbox.sol b/contracts/src/bridge/canonical/arbitrum/IOutbox.sol similarity index 100% rename from contracts/src/bridge/interfaces/arbitrum/IOutbox.sol rename to contracts/src/bridge/canonical/arbitrum/IOutbox.sol diff --git a/contracts/src/bridge/canonical/gnosis-chain/IAMB.sol b/contracts/src/bridge/canonical/gnosis-chain/IAMB.sol new file mode 100644 index 000000000..da6a6efe1 --- /dev/null +++ b/contracts/src/bridge/canonical/gnosis-chain/IAMB.sol @@ -0,0 +1,43 @@ +// 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 { + function requireToPassMessage( + address _contract, + bytes memory _data, + uint256 _gas + ) external returns (bytes32); + + function maxGasPerTx() external view returns (uint256); + + function messageSender() external view returns (address); + + 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/canonical/polygon/FxBaseChildTunnel.sol b/contracts/src/bridge/canonical/polygon/FxBaseChildTunnel.sol new file mode 100644 index 000000000..90612f85f --- /dev/null +++ b/contracts/src/bridge/canonical/polygon/FxBaseChildTunnel.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +// https://github.com/fx-portal/contracts/blob/main/contracts/tunnel/FxBaseChildTunnel.sol +pragma solidity ^0.8.0; + +// IFxMessageProcessor represents interface to process message +interface IFxMessageProcessor { + function processMessageFromRoot( + uint256 stateId, + address rootMessageSender, + bytes calldata data + ) external; +} + +/** + * @dev Polygon-side abstract contract of the bidirectional Polygon/Ethereum bridge + */ +abstract contract FxBaseChildTunnel is IFxMessageProcessor { + // MessageTunnel on L1 will get data from this event + event MessageSent(bytes message); + + // fx child + address public fxChild; + + // fx root tunnel + address public fxRootTunnel; + + constructor(address _fxChild) { + fxChild = _fxChild; + } + + // Sender must be fxRootTunnel in case of ERC20 tunnel + modifier validateSender(address sender) { + require(sender == fxRootTunnel, "FxBaseChildTunnel: INVALID_SENDER_FROM_ROOT"); + _; + } + + // set fxRootTunnel if not set already + function setFxRootTunnel(address _fxRootTunnel) public virtual { + require(fxRootTunnel == address(0x0), "FxBaseChildTunnel: ROOT_TUNNEL_ALREADY_SET"); + fxRootTunnel = _fxRootTunnel; + } + + function processMessageFromRoot( + uint256 stateId, + address rootMessageSender, + bytes calldata data + ) external override { + require(msg.sender == fxChild, "FxBaseChildTunnel: INVALID_SENDER"); + _processMessageFromRoot(stateId, rootMessageSender, data); + } + + /** + * @notice Emit message that can be received on Root Tunnel + * @dev Call the internal function when need to emit message + * @param message bytes message that will be sent to Root Tunnel + * some message examples - + * abi.encode(tokenId); + * abi.encode(tokenId, tokenMetadata); + * abi.encode(messageType, messageData); + */ + function _sendMessageToRoot(bytes memory message) internal { + emit MessageSent(message); + } + + /** + * @notice Process message received from Root Tunnel + * @dev function needs to be implemented to handle message as per requirement + * This is called by onStateReceive function. + * Since it is called via a system call, any event will not be emitted during its execution. + * @param stateId unique state id + * @param sender root message sender + * @param message bytes message that was sent from Root Tunnel + */ + function _processMessageFromRoot( + uint256 stateId, + address sender, + bytes memory message + ) internal virtual; +} diff --git a/contracts/src/bridge/canonical/polygon/FxBaseRootTunnel.sol b/contracts/src/bridge/canonical/polygon/FxBaseRootTunnel.sol new file mode 100644 index 000000000..fd9f0f2ee --- /dev/null +++ b/contracts/src/bridge/canonical/polygon/FxBaseRootTunnel.sol @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: MIT +// https://github.com/fx-portal/contracts/blob/main/contracts/tunnel/FxBaseRootTunnel.sol +pragma solidity ^0.8.0; + +import {RLPReader} from "./lib/RLPReader.sol"; +import {MerklePatriciaProof} from "./lib/MerklePatriciaProof.sol"; +import {Merkle} from "./lib/Merkle.sol"; +import "./lib/ExitPayloadReader.sol"; + +interface IFxStateSender { + function sendMessageToChild(address _receiver, bytes calldata _data) external; +} + +contract ICheckpointManager { + struct HeaderBlock { + bytes32 root; + uint256 start; + uint256 end; + uint256 createdAt; + address proposer; + } + + /** + * @notice mapping of checkpoint header numbers to block details + * @dev These checkpoints are submited by plasma contracts + */ + mapping(uint256 => HeaderBlock) public headerBlocks; +} + +/** + * @dev Ethereum-side abstract contract of the bidirectional Polygon/Ethereum bridge + */ +abstract contract FxBaseRootTunnel { + using RLPReader for RLPReader.RLPItem; + using Merkle for bytes32; + using ExitPayloadReader for bytes; + using ExitPayloadReader for ExitPayloadReader.ExitPayload; + using ExitPayloadReader for ExitPayloadReader.Log; + using ExitPayloadReader for ExitPayloadReader.LogTopics; + using ExitPayloadReader for ExitPayloadReader.Receipt; + + // keccak256(MessageSent(bytes)) + bytes32 public constant SEND_MESSAGE_EVENT_SIG = 0x8c5261668696ce22758910d05bab8f186d6eb247ceac2af2e82c7dc17669b036; + + // state sender contract + IFxStateSender public fxRoot; + // root chain manager + ICheckpointManager public checkpointManager; + // child tunnel contract which receives and sends messages + address public fxChildTunnel; + + // storage to avoid duplicate exits + mapping(bytes32 => bool) public processedExits; + + constructor(address _checkpointManager, address _fxRoot) { + checkpointManager = ICheckpointManager(_checkpointManager); + fxRoot = IFxStateSender(_fxRoot); + } + + // set fxChildTunnel if not set already + function setFxChildTunnel(address _fxChildTunnel) public virtual { + require(fxChildTunnel == address(0x0), "FxBaseRootTunnel: CHILD_TUNNEL_ALREADY_SET"); + fxChildTunnel = _fxChildTunnel; + } + + /** + * @notice Send bytes message to Child Tunnel + * @param message bytes message that will be sent to Child Tunnel + * some message examples - + * abi.encode(tokenId); + * abi.encode(tokenId, tokenMetadata); + * abi.encode(messageType, messageData); + */ + function _sendMessageToChild(bytes memory message) internal { + fxRoot.sendMessageToChild(fxChildTunnel, message); + } + + function _validateAndExtractMessage(bytes memory inputData) internal returns (bytes memory) { + ExitPayloadReader.ExitPayload memory payload = inputData.toExitPayload(); + + bytes memory branchMaskBytes = payload.getBranchMaskAsBytes(); + uint256 blockNumber = payload.getBlockNumber(); + // checking if exit has already been processed + // unique exit is identified using hash of (blockNumber, branchMask, receiptLogIndex) + bytes32 exitHash = keccak256( + abi.encodePacked( + blockNumber, + // first 2 nibbles are dropped while generating nibble array + // this allows branch masks that are valid but bypass exitHash check (changing first 2 nibbles only) + // so converting to nibble array and then hashing it + MerklePatriciaProof._getNibbleArray(branchMaskBytes), + payload.getReceiptLogIndex() + ) + ); + require(processedExits[exitHash] == false, "FxRootTunnel: EXIT_ALREADY_PROCESSED"); + processedExits[exitHash] = true; + + ExitPayloadReader.Receipt memory receipt = payload.getReceipt(); + ExitPayloadReader.Log memory log = receipt.getLog(); + + // check child tunnel + require(fxChildTunnel == log.getEmitter(), "FxRootTunnel: INVALID_FX_CHILD_TUNNEL"); + + bytes32 receiptRoot = payload.getReceiptRoot(); + // verify receipt inclusion + require( + MerklePatriciaProof.verify(receipt.toBytes(), branchMaskBytes, payload.getReceiptProof(), receiptRoot), + "FxRootTunnel: INVALID_RECEIPT_PROOF" + ); + + // verify checkpoint inclusion + _checkBlockMembershipInCheckpoint( + blockNumber, + payload.getBlockTime(), + payload.getTxRoot(), + receiptRoot, + payload.getHeaderNumber(), + payload.getBlockProof() + ); + + ExitPayloadReader.LogTopics memory topics = log.getTopics(); + + require( + bytes32(topics.getField(0).toUint()) == SEND_MESSAGE_EVENT_SIG, // topic0 is event sig + "FxRootTunnel: INVALID_SIGNATURE" + ); + + // received message data + bytes memory message = abi.decode(log.getData(), (bytes)); // event decodes params again, so decoding bytes to get message + return message; + } + + function _checkBlockMembershipInCheckpoint( + uint256 blockNumber, + uint256 blockTime, + bytes32 txRoot, + bytes32 receiptRoot, + uint256 headerNumber, + bytes memory blockProof + ) private view returns (uint256) { + (bytes32 headerRoot, uint256 startBlock, , uint256 createdAt, ) = checkpointManager.headerBlocks(headerNumber); + + require( + keccak256(abi.encodePacked(blockNumber, blockTime, txRoot, receiptRoot)).checkMembership( + blockNumber - startBlock, + headerRoot, + blockProof + ), + "FxRootTunnel: INVALID_HEADER" + ); + return createdAt; + } + + /** + * @notice receive message from L2 to L1, validated by proof + * @dev This function verifies if the transaction actually happened on child chain + * + * @param inputData RLP encoded data of the reference tx containing following list of fields + * 0 - headerNumber - Checkpoint header block number containing the reference tx + * 1 - blockProof - Proof that the block header (in the child chain) is a leaf in the submitted merkle root + * 2 - blockNumber - Block number containing the reference tx on child chain + * 3 - blockTime - Reference tx block time + * 4 - txRoot - Transactions root of block + * 5 - receiptRoot - Receipts root of block + * 6 - receipt - Receipt of the reference transaction + * 7 - receiptProof - Merkle proof of the reference receipt + * 8 - branchMask - 32 bits denoting the path of receipt in merkle tree + * 9 - receiptLogIndex - Log Index to read from the receipt + */ + function receiveMessage(bytes memory inputData) public virtual { + bytes memory message = _validateAndExtractMessage(inputData); + _processMessageFromChild(message); + } + + /** + * @notice Process message received from Child Tunnel + * @dev function needs to be implemented to handle message as per requirement + * This is called by onStateReceive function. + * Since it is called via a system call, any event will not be emitted during its execution. + * @param message bytes message that was sent from Child Tunnel + */ + function _processMessageFromChild(bytes memory message) internal virtual; +} diff --git a/contracts/src/bridge/canonical/polygon/lib/ExitPayloadReader.sol b/contracts/src/bridge/canonical/polygon/lib/ExitPayloadReader.sol new file mode 100644 index 000000000..1136c2b6b --- /dev/null +++ b/contracts/src/bridge/canonical/polygon/lib/ExitPayloadReader.sol @@ -0,0 +1,160 @@ +pragma solidity ^0.8.0; + +import {RLPReader} from "./RLPReader.sol"; + +library ExitPayloadReader { + using RLPReader for bytes; + using RLPReader for RLPReader.RLPItem; + + uint8 constant WORD_SIZE = 32; + + struct ExitPayload { + RLPReader.RLPItem[] data; + } + + struct Receipt { + RLPReader.RLPItem[] data; + bytes raw; + uint256 logIndex; + } + + struct Log { + RLPReader.RLPItem data; + RLPReader.RLPItem[] list; + } + + struct LogTopics { + RLPReader.RLPItem[] data; + } + + // copy paste of private copy() from RLPReader to avoid changing of existing contracts + function copy( + uint256 src, + uint256 dest, + uint256 len + ) private pure { + if (len == 0) return; + + // copy as many word sizes as possible + for (; len >= WORD_SIZE; len -= WORD_SIZE) { + assembly { + mstore(dest, mload(src)) + } + + src += WORD_SIZE; + dest += WORD_SIZE; + } + + // left over bytes. Mask is used to remove unwanted bytes from the word + uint256 mask = 256**(WORD_SIZE - len) - 1; + assembly { + let srcpart := and(mload(src), not(mask)) // zero out src + let destpart := and(mload(dest), mask) // retrieve the bytes + mstore(dest, or(destpart, srcpart)) + } + } + + function toExitPayload(bytes memory data) internal pure returns (ExitPayload memory) { + RLPReader.RLPItem[] memory payloadData = data.toRlpItem().toList(); + + return ExitPayload(payloadData); + } + + function getHeaderNumber(ExitPayload memory payload) internal pure returns (uint256) { + return payload.data[0].toUint(); + } + + function getBlockProof(ExitPayload memory payload) internal pure returns (bytes memory) { + return payload.data[1].toBytes(); + } + + function getBlockNumber(ExitPayload memory payload) internal pure returns (uint256) { + return payload.data[2].toUint(); + } + + function getBlockTime(ExitPayload memory payload) internal pure returns (uint256) { + return payload.data[3].toUint(); + } + + function getTxRoot(ExitPayload memory payload) internal pure returns (bytes32) { + return bytes32(payload.data[4].toUint()); + } + + function getReceiptRoot(ExitPayload memory payload) internal pure returns (bytes32) { + return bytes32(payload.data[5].toUint()); + } + + function getReceipt(ExitPayload memory payload) internal pure returns (Receipt memory receipt) { + receipt.raw = payload.data[6].toBytes(); + RLPReader.RLPItem memory receiptItem = receipt.raw.toRlpItem(); + + if (receiptItem.isList()) { + // legacy tx + receipt.data = receiptItem.toList(); + } else { + // pop first byte before parsting receipt + bytes memory typedBytes = receipt.raw; + bytes memory result = new bytes(typedBytes.length - 1); + uint256 srcPtr; + uint256 destPtr; + assembly { + srcPtr := add(33, typedBytes) + destPtr := add(0x20, result) + } + + copy(srcPtr, destPtr, result.length); + receipt.data = result.toRlpItem().toList(); + } + + receipt.logIndex = getReceiptLogIndex(payload); + return receipt; + } + + function getReceiptProof(ExitPayload memory payload) internal pure returns (bytes memory) { + return payload.data[7].toBytes(); + } + + function getBranchMaskAsBytes(ExitPayload memory payload) internal pure returns (bytes memory) { + return payload.data[8].toBytes(); + } + + function getBranchMaskAsUint(ExitPayload memory payload) internal pure returns (uint256) { + return payload.data[8].toUint(); + } + + function getReceiptLogIndex(ExitPayload memory payload) internal pure returns (uint256) { + return payload.data[9].toUint(); + } + + // Receipt methods + function toBytes(Receipt memory receipt) internal pure returns (bytes memory) { + return receipt.raw; + } + + function getLog(Receipt memory receipt) internal pure returns (Log memory) { + RLPReader.RLPItem memory logData = receipt.data[3].toList()[receipt.logIndex]; + return Log(logData, logData.toList()); + } + + // Log methods + function getEmitter(Log memory log) internal pure returns (address) { + return RLPReader.toAddress(log.list[0]); + } + + function getTopics(Log memory log) internal pure returns (LogTopics memory) { + return LogTopics(log.list[1].toList()); + } + + function getData(Log memory log) internal pure returns (bytes memory) { + return log.list[2].toBytes(); + } + + function toRlpBytes(Log memory log) internal pure returns (bytes memory) { + return log.data.toRlpBytes(); + } + + // LogTopics methods + function getField(LogTopics memory topics, uint256 index) internal pure returns (RLPReader.RLPItem memory) { + return topics.data[index]; + } +} diff --git a/contracts/src/bridge/canonical/polygon/lib/Merkle.sol b/contracts/src/bridge/canonical/polygon/lib/Merkle.sol new file mode 100644 index 000000000..2dd3a8667 --- /dev/null +++ b/contracts/src/bridge/canonical/polygon/lib/Merkle.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +library Merkle { + function checkMembership( + bytes32 leaf, + uint256 index, + bytes32 rootHash, + bytes memory proof + ) internal pure returns (bool) { + require(proof.length % 32 == 0, "Invalid proof length"); + uint256 proofHeight = proof.length / 32; + // Proof of size n means, height of the tree is n+1. + // In a tree of height n+1, max #leafs possible is 2 ^ n + require(index < 2**proofHeight, "Leaf index is too big"); + + bytes32 proofElement; + bytes32 computedHash = leaf; + for (uint256 i = 32; i <= proof.length; i += 32) { + assembly { + proofElement := mload(add(proof, i)) + } + + if (index % 2 == 0) { + computedHash = keccak256(abi.encodePacked(computedHash, proofElement)); + } else { + computedHash = keccak256(abi.encodePacked(proofElement, computedHash)); + } + + index = index / 2; + } + return computedHash == rootHash; + } +} diff --git a/contracts/src/bridge/canonical/polygon/lib/MerklePatriciaProof.sol b/contracts/src/bridge/canonical/polygon/lib/MerklePatriciaProof.sol new file mode 100644 index 000000000..430d7dcd1 --- /dev/null +++ b/contracts/src/bridge/canonical/polygon/lib/MerklePatriciaProof.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {RLPReader} from "./RLPReader.sol"; + +library MerklePatriciaProof { + /* + * @dev Verifies a merkle patricia proof. + * @param value The terminating value in the trie. + * @param encodedPath The path in the trie leading to value. + * @param rlpParentNodes The rlp encoded stack of nodes. + * @param root The root hash of the trie. + * @return The boolean validity of the proof. + */ + function verify( + bytes memory value, + bytes memory encodedPath, + bytes memory rlpParentNodes, + bytes32 root + ) internal pure returns (bool) { + RLPReader.RLPItem memory item = RLPReader.toRlpItem(rlpParentNodes); + RLPReader.RLPItem[] memory parentNodes = RLPReader.toList(item); + + bytes memory currentNode; + RLPReader.RLPItem[] memory currentNodeList; + + bytes32 nodeKey = root; + uint256 pathPtr = 0; + + bytes memory path = _getNibbleArray(encodedPath); + if (path.length == 0) { + return false; + } + + for (uint256 i = 0; i < parentNodes.length; i++) { + if (pathPtr > path.length) { + return false; + } + + currentNode = RLPReader.toRlpBytes(parentNodes[i]); + if (nodeKey != keccak256(currentNode)) { + return false; + } + currentNodeList = RLPReader.toList(parentNodes[i]); + + if (currentNodeList.length == 17) { + if (pathPtr == path.length) { + if (keccak256(RLPReader.toBytes(currentNodeList[16])) == keccak256(value)) { + return true; + } else { + return false; + } + } + + uint8 nextPathNibble = uint8(path[pathPtr]); + if (nextPathNibble > 16) { + return false; + } + nodeKey = bytes32(RLPReader.toUintStrict(currentNodeList[nextPathNibble])); + pathPtr += 1; + } else if (currentNodeList.length == 2) { + uint256 traversed = _nibblesToTraverse(RLPReader.toBytes(currentNodeList[0]), path, pathPtr); + if (pathPtr + traversed == path.length) { + //leaf node + if (keccak256(RLPReader.toBytes(currentNodeList[1])) == keccak256(value)) { + return true; + } else { + return false; + } + } + + //extension node + if (traversed == 0) { + return false; + } + + pathPtr += traversed; + nodeKey = bytes32(RLPReader.toUintStrict(currentNodeList[1])); + } else { + return false; + } + } + } + + function _nibblesToTraverse( + bytes memory encodedPartialPath, + bytes memory path, + uint256 pathPtr + ) private pure returns (uint256) { + uint256 len = 0; + // encodedPartialPath has elements that are each two hex characters (1 byte), but partialPath + // and slicedPath have elements that are each one hex character (1 nibble) + bytes memory partialPath = _getNibbleArray(encodedPartialPath); + bytes memory slicedPath = new bytes(partialPath.length); + + // pathPtr counts nibbles in path + // partialPath.length is a number of nibbles + for (uint256 i = pathPtr; i < pathPtr + partialPath.length; i++) { + bytes1 pathNibble = path[i]; + slicedPath[i - pathPtr] = pathNibble; + } + + if (keccak256(partialPath) == keccak256(slicedPath)) { + len = partialPath.length; + } else { + len = 0; + } + return len; + } + + // bytes b must be hp encoded + function _getNibbleArray(bytes memory b) internal pure returns (bytes memory) { + bytes memory nibbles = ""; + if (b.length > 0) { + uint8 offset; + uint8 hpNibble = uint8(_getNthNibbleOfBytes(0, b)); + if (hpNibble == 1 || hpNibble == 3) { + nibbles = new bytes(b.length * 2 - 1); + bytes1 oddNibble = _getNthNibbleOfBytes(1, b); + nibbles[0] = oddNibble; + offset = 1; + } else { + nibbles = new bytes(b.length * 2 - 2); + offset = 0; + } + + for (uint256 i = offset; i < nibbles.length; i++) { + nibbles[i] = _getNthNibbleOfBytes(i - offset + 2, b); + } + } + return nibbles; + } + + function _getNthNibbleOfBytes(uint256 n, bytes memory str) private pure returns (bytes1) { + return bytes1(n % 2 == 0 ? uint8(str[n / 2]) / 0x10 : uint8(str[n / 2]) % 0x10); + } +} diff --git a/contracts/src/bridge/canonical/polygon/lib/RLPReader.sol b/contracts/src/bridge/canonical/polygon/lib/RLPReader.sol new file mode 100644 index 000000000..e190e0046 --- /dev/null +++ b/contracts/src/bridge/canonical/polygon/lib/RLPReader.sol @@ -0,0 +1,340 @@ +/* + * @author Hamdi Allam hamdi.allam97@gmail.com + * Please reach out with any questions or concerns + */ +pragma solidity ^0.8.0; + +library RLPReader { + uint8 constant STRING_SHORT_START = 0x80; + uint8 constant STRING_LONG_START = 0xb8; + uint8 constant LIST_SHORT_START = 0xc0; + uint8 constant LIST_LONG_START = 0xf8; + uint8 constant WORD_SIZE = 32; + + struct RLPItem { + uint256 len; + uint256 memPtr; + } + + struct Iterator { + RLPItem item; // Item that's being iterated over. + uint256 nextPtr; // Position of the next item in the list. + } + + /* + * @dev Returns the next element in the iteration. Reverts if it has not next element. + * @param self The iterator. + * @return The next element in the iteration. + */ + function next(Iterator memory self) internal pure returns (RLPItem memory) { + require(hasNext(self)); + + uint256 ptr = self.nextPtr; + uint256 itemLength = _itemLength(ptr); + self.nextPtr = ptr + itemLength; + + return RLPItem(itemLength, ptr); + } + + /* + * @dev Returns true if the iteration has more elements. + * @param self The iterator. + * @return true if the iteration has more elements. + */ + function hasNext(Iterator memory self) internal pure returns (bool) { + RLPItem memory item = self.item; + return self.nextPtr < item.memPtr + item.len; + } + + /* + * @param item RLP encoded bytes + */ + function toRlpItem(bytes memory item) internal pure returns (RLPItem memory) { + uint256 memPtr; + assembly { + memPtr := add(item, 0x20) + } + + return RLPItem(item.length, memPtr); + } + + /* + * @dev Create an iterator. Reverts if item is not a list. + * @param self The RLP item. + * @return An 'Iterator' over the item. + */ + function iterator(RLPItem memory self) internal pure returns (Iterator memory) { + require(isList(self)); + + uint256 ptr = self.memPtr + _payloadOffset(self.memPtr); + return Iterator(self, ptr); + } + + /* + * @param item RLP encoded bytes + */ + function rlpLen(RLPItem memory item) internal pure returns (uint256) { + return item.len; + } + + /* + * @param item RLP encoded bytes + */ + function payloadLen(RLPItem memory item) internal pure returns (uint256) { + return item.len - _payloadOffset(item.memPtr); + } + + /* + * @param item RLP encoded list in bytes + */ + function toList(RLPItem memory item) internal pure returns (RLPItem[] memory) { + require(isList(item)); + + uint256 items = numItems(item); + RLPItem[] memory result = new RLPItem[](items); + + uint256 memPtr = item.memPtr + _payloadOffset(item.memPtr); + uint256 dataLen; + for (uint256 i = 0; i < items; i++) { + dataLen = _itemLength(memPtr); + result[i] = RLPItem(dataLen, memPtr); + memPtr = memPtr + dataLen; + } + + return result; + } + + // @return indicator whether encoded payload is a list. negate this function call for isData. + function isList(RLPItem memory item) internal pure returns (bool) { + if (item.len == 0) return false; + + uint8 byte0; + uint256 memPtr = item.memPtr; + assembly { + byte0 := byte(0, mload(memPtr)) + } + + if (byte0 < LIST_SHORT_START) return false; + return true; + } + + /* + * @dev A cheaper version of keccak256(toRlpBytes(item)) that avoids copying memory. + * @return keccak256 hash of RLP encoded bytes. + */ + function rlpBytesKeccak256(RLPItem memory item) internal pure returns (bytes32) { + uint256 ptr = item.memPtr; + uint256 len = item.len; + bytes32 result; + assembly { + result := keccak256(ptr, len) + } + return result; + } + + function payloadLocation(RLPItem memory item) internal pure returns (uint256, uint256) { + uint256 offset = _payloadOffset(item.memPtr); + uint256 memPtr = item.memPtr + offset; + uint256 len = item.len - offset; // data length + return (memPtr, len); + } + + /* + * @dev A cheaper version of keccak256(toBytes(item)) that avoids copying memory. + * @return keccak256 hash of the item payload. + */ + function payloadKeccak256(RLPItem memory item) internal pure returns (bytes32) { + (uint256 memPtr, uint256 len) = payloadLocation(item); + bytes32 result; + assembly { + result := keccak256(memPtr, len) + } + return result; + } + + /** RLPItem conversions into data types **/ + + // @returns raw rlp encoding in bytes + function toRlpBytes(RLPItem memory item) internal pure returns (bytes memory) { + bytes memory result = new bytes(item.len); + if (result.length == 0) return result; + + uint256 ptr; + assembly { + ptr := add(0x20, result) + } + + copy(item.memPtr, ptr, item.len); + return result; + } + + // any non-zero byte is considered true + function toBoolean(RLPItem memory item) internal pure returns (bool) { + require(item.len == 1); + uint256 result; + uint256 memPtr = item.memPtr; + assembly { + result := byte(0, mload(memPtr)) + } + + return result == 0 ? false : true; + } + + function toAddress(RLPItem memory item) internal pure returns (address) { + // 1 byte for the length prefix + require(item.len == 21); + + return address(uint160(toUint(item))); + } + + function toUint(RLPItem memory item) internal pure returns (uint256) { + require(item.len > 0 && item.len <= 33); + + uint256 offset = _payloadOffset(item.memPtr); + uint256 len = item.len - offset; + + uint256 result; + uint256 memPtr = item.memPtr + offset; + assembly { + result := mload(memPtr) + + // shfit to the correct location if neccesary + if lt(len, 32) { + result := div(result, exp(256, sub(32, len))) + } + } + + return result; + } + + // enforces 32 byte length + function toUintStrict(RLPItem memory item) internal pure returns (uint256) { + // one byte prefix + require(item.len == 33); + + uint256 result; + uint256 memPtr = item.memPtr + 1; + assembly { + result := mload(memPtr) + } + + return result; + } + + function toBytes(RLPItem memory item) internal pure returns (bytes memory) { + require(item.len > 0); + + uint256 offset = _payloadOffset(item.memPtr); + uint256 len = item.len - offset; // data length + bytes memory result = new bytes(len); + + uint256 destPtr; + assembly { + destPtr := add(0x20, result) + } + + copy(item.memPtr + offset, destPtr, len); + return result; + } + + /* + * Private Helpers + */ + + // @return number of payload items inside an encoded list. + function numItems(RLPItem memory item) private pure returns (uint256) { + if (item.len == 0) return 0; + + uint256 count = 0; + uint256 currPtr = item.memPtr + _payloadOffset(item.memPtr); + uint256 endPtr = item.memPtr + item.len; + while (currPtr < endPtr) { + currPtr = currPtr + _itemLength(currPtr); // skip over an item + count++; + } + + return count; + } + + // @return entire rlp item byte length + function _itemLength(uint256 memPtr) private pure returns (uint256) { + uint256 itemLen; + uint256 byte0; + assembly { + byte0 := byte(0, mload(memPtr)) + } + + if (byte0 < STRING_SHORT_START) itemLen = 1; + else if (byte0 < STRING_LONG_START) itemLen = byte0 - STRING_SHORT_START + 1; + else if (byte0 < LIST_SHORT_START) { + assembly { + let byteLen := sub(byte0, 0xb7) // # of bytes the actual length is + memPtr := add(memPtr, 1) // skip over the first byte + /* 32 byte word size */ + let dataLen := div(mload(memPtr), exp(256, sub(32, byteLen))) // right shifting to get the len + itemLen := add(dataLen, add(byteLen, 1)) + } + } else if (byte0 < LIST_LONG_START) { + itemLen = byte0 - LIST_SHORT_START + 1; + } else { + assembly { + let byteLen := sub(byte0, 0xf7) + memPtr := add(memPtr, 1) + + let dataLen := div(mload(memPtr), exp(256, sub(32, byteLen))) // right shifting to the correct length + itemLen := add(dataLen, add(byteLen, 1)) + } + } + + return itemLen; + } + + // @return number of bytes until the data + function _payloadOffset(uint256 memPtr) private pure returns (uint256) { + uint256 byte0; + assembly { + byte0 := byte(0, mload(memPtr)) + } + + if (byte0 < STRING_SHORT_START) return 0; + else if (byte0 < STRING_LONG_START || (byte0 >= LIST_SHORT_START && byte0 < LIST_LONG_START)) return 1; + else if (byte0 < LIST_SHORT_START) + // being explicit + return byte0 - (STRING_LONG_START - 1) + 1; + else return byte0 - (LIST_LONG_START - 1) + 1; + } + + /* + * @param src Pointer to source + * @param dest Pointer to destination + * @param len Amount of memory to copy from the source + */ + function copy( + uint256 src, + uint256 dest, + uint256 len + ) private pure { + if (len == 0) return; + + // copy as many word sizes as possible + for (; len >= WORD_SIZE; len -= WORD_SIZE) { + assembly { + mstore(dest, mload(src)) + } + + src += WORD_SIZE; + dest += WORD_SIZE; + } + + if (len == 0) return; + + // left over bytes. Mask is used to remove unwanted bytes from the word + uint256 mask = 256**(WORD_SIZE - len) - 1; + + assembly { + let srcpart := and(mload(src), not(mask)) // zero out src + let destpart := and(mload(dest), mask) // retrieve the bytes + mstore(dest, or(destpart, srcpart)) + } + } +} diff --git a/contracts/src/bridge/interfaces/IFastBridgeReceiver.sol b/contracts/src/bridge/interfaces/IFastBridgeReceiver.sol index 0b508f8d1..0ce42badf 100644 --- a/contracts/src/bridge/interfaces/IFastBridgeReceiver.sol +++ b/contracts/src/bridge/interfaces/IFastBridgeReceiver.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT /** - * @authors: [@jaybuidl, @shalzz, @hrishibhat, @shotaronowhere] + * @authors: [@jaybuidl, @shotaronowhere, @hrishibhat] * @reviewers: [] * @auditors: [] * @bounties: [] @@ -17,101 +17,118 @@ 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 for which 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 indexed _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. + * @dev This event indicates that `sendSafeFallback()` should be called on the sending side. + * @param _epoch The epoch associated with the challenged claim. */ - event ClaimChallenged(uint256 indexed ticketID, uint256 challengedAt); + event ClaimChallenged(uint256 indexed _epoch); + + /** + * @dev This events indicates that optimistic verification has succeeded. The messages are ready to be relayed. + * @param _epoch The epoch associated with the batch. + */ + event BatchVerified(uint256 indexed _epoch); + + /** + * @dev This event indicates that the batch has been received via the Safe Bridge. + * @param _epoch The epoch associated with the batch. + * @param _isBridgerHonest Whether the bridger made an honest claim. + * @param _isChallengerHonest Whether the bridger made an honest challenge. + */ + event BatchSafeVerified(uint256 indexed _epoch, bool _isBridgerHonest, bool _isChallengerHonest); + + /** + * @dev This event indicates that the claim deposit has been withdrawn. + * @param _epoch The epoch associated with the batch. + * @param _bridger The recipient of the claim deposit. + */ + event ClaimDepositWithdrawn(uint256 indexed _epoch, address indexed _bridger); + + /** + * @dev This event indicates that the challenge deposit has been withdrawn. + * @param _epoch The epoch associated with the batch. + * @param _challenger The recipient of the challenge deposit. + */ + event ChallengeDepositWithdrawn(uint256 indexed _epoch, address indexed _challenger); + + /** + * @dev This event indicates that a message has been relayed for the batch in this `_epoch`. + * @param _epoch The epoch associated with the batch. + * @param _nonce The nonce of the message that was relayed. + */ + event MessageRelayed(uint256 indexed _epoch, uint256 indexed _nonce); // ************************************* // // * 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 _epoch The epoch of the claim to claim. + * @param _batchMerkleRoot The hash claimed for the ticket. */ - function claim(uint256 _ticketID, bytes32 _messageHash) external payable; + function claim(uint256 _epoch, 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. + * @param _epoch The epoch of the claim to challenge. */ - function challenge(uint256 _ticketID) external payable; + function challenge(uint256 _epoch) 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 Resolves the optimistic claim for '_epoch'. + * @param _epoch The epoch of the optimistic claim. */ - function verifyAndRelay( - uint256 _ticketID, - uint256 _blockNumber, - bytes calldata _messageData - ) external; + function verifyBatch(uint256 _epoch) 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 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. */ - function verifyAndRelaySafe( - uint256 _ticketID, - uint256 _blockNumber, - bytes calldata _messageData + function verifyAndRelayMessage( + uint256 _epoch, + bytes32[] calldata _proof, + bytes calldata _message ) 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. + * @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 _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`. + * @param _epoch The epoch of the claim to request the challenge period. * @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); + function claimChallengePeriod(uint256 _epoch) external view returns (uint256 start, uint256 end); /** - * @return amount The duration of the period allowing to challenge a claim. + * @dev Returns the epoch period. */ - function challengeDuration() external view returns (uint256 amount); + function epochPeriod() external view returns (uint256 epochPeriod); /** - * @return amount Basis point of claim or challenge deposit that are lost when dishonest. + * @dev Returns the challenge period. */ - function alpha() external view returns (uint256 amount); + function challengePeriod() external view returns (uint256 challengePeriod); } diff --git a/contracts/src/bridge/interfaces/IFastBridgeSender.sol b/contracts/src/bridge/interfaces/IFastBridgeSender.sol index c5ed12ab8..bfa3ab1a6 100644 --- a/contracts/src/bridge/interfaces/IFastBridgeSender.sol +++ b/contracts/src/bridge/interfaces/IFastBridgeSender.sol @@ -1,5 +1,13 @@ // SPDX-License-Identifier: MIT +/** + * @authors: [@jaybuidl, @shotaronowhere] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ + pragma solidity ^0.8.0; interface IFastBridgeSender { @@ -9,42 +17,45 @@ 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 fastMessage The fast message data. + * @param fastMessage The hash of the fast message data encoded with the nonce. + */ + event MessageReceived(bytes fastMessage, bytes32 fastMessageHash); + + /** + * @dev The event is emitted when messages are sent through the canonical bridge. + * @param epoch The epoch of the batch requested to send. + * @param canonicalBridgeMessageID The unique identifier of the safe message returned by the canonical bridge. + */ + event SentSafe(uint256 indexed epoch, bytes32 canonicalBridgeMessageID); + + /** + * The bridgers need to watch for these events and relay the + * batchMerkleRoot on the FastBridgeReceiver. */ - event OutgoingMessage( - uint256 indexed ticketID, - uint256 blockNumber, - address target, - bytes32 indexed messageHash, - bytes message - ); + event BatchOutgoing(uint256 indexed batchID, uint256 batchSize, uint256 epoch, bytes32 batchMerkleRoot); // ************************************* // // * Function Modifiers * // // ************************************* // /** - * Note: Access must be restricted to the sending application. + * Note: Access must be restricted by the receiving gateway by checking the sender 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. - * @return ticketID The identifier to provide to sendSafeFallback(). */ - function sendFast(address _receiver, bytes memory _calldata) external returns (uint256 ticketID); + function sendFast(address _receiver, 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()`. - * @param _receiver The cross-domain contract address which receives the calldata. - * @param _calldata The receiving domain encoded message data. + * Sends a batch of arbitrary message from one domain to another + * via the fast bridge mechanism. + */ + function sendBatch() 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 _ticketID, - address _receiver, - bytes memory _calldata - ) external payable; + function sendSafeFallback(uint256 _epoch) external payable; } diff --git a/contracts/src/bridge/interfaces/IReceiverGateway.sol b/contracts/src/bridge/interfaces/IReceiverGateway.sol new file mode 100644 index 000000000..e12949109 --- /dev/null +++ b/contracts/src/bridge/interfaces/IReceiverGateway.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT + +/** + * @authors: [@jaybuidl, @shotaronowhere] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ + +pragma solidity ^0.8.0; + +import "../../bridge/interfaces/IFastBridgeReceiver.sol"; + +interface IReceiverGateway { + function fastBridgeReceiver() external view returns (IFastBridgeReceiver); + + function senderChainID() external view returns (uint256); + + function senderGateway() external view returns (address); +} diff --git a/contracts/src/bridge/interfaces/ISafeBridgeReceiver.sol b/contracts/src/bridge/interfaces/ISafeBridgeReceiver.sol index bb3916563..33941e792 100644 --- a/contracts/src/bridge/interfaces/ISafeBridgeReceiver.sol +++ b/contracts/src/bridge/interfaces/ISafeBridgeReceiver.sol @@ -1,7 +1,28 @@ // SPDX-License-Identifier: MIT +/** + * @authors: [@jaybuidl, @shotaronowhere] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ + 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 verifySafeBatch(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/ISafeBridgeRouter.sol b/contracts/src/bridge/interfaces/ISafeBridgeRouter.sol new file mode 100644 index 000000000..de3cbd4e0 --- /dev/null +++ b/contracts/src/bridge/interfaces/ISafeBridgeRouter.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT + +/** + * @authors: [@jaybuidl, @shotaronowhere] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ + +pragma solidity ^0.8.0; + +import "./ISafeBridgeReceiver.sol"; +import "./ISafeBridgeSender.sol"; + +/** + * Abstract Router to forward messages between Safe Bridges. + */ +abstract contract ISafeBridgeRouter is ISafeBridgeReceiver, ISafeBridgeSender { + // ************************************* // + // * Events * // + // ************************************* // + + /** + * @dev Event emitted when a message is relayed to another Safe Bridge. + * @param _ticketID The unique identifier provided by the underlying canonical bridge. + */ + event SafeRelayed(bytes32 indexed _ticketID); +} diff --git a/contracts/src/bridge/interfaces/ISafeBridgeSender.sol b/contracts/src/bridge/interfaces/ISafeBridgeSender.sol index 97cb8f9b4..6cc063391 100644 --- a/contracts/src/bridge/interfaces/ISafeBridgeSender.sol +++ b/contracts/src/bridge/interfaces/ISafeBridgeSender.sol @@ -1,14 +1,22 @@ // SPDX-License-Identifier: MIT +/** + * @authors: [@jaybuidl, @shotaronowhere] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ + pragma solidity ^0.8.0; 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 contract address which will receive the calldata on the receiving chain. + * @param _calldata The encoded message data to send. * @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/ISenderGateway.sol b/contracts/src/bridge/interfaces/ISenderGateway.sol new file mode 100644 index 000000000..f3c6520b8 --- /dev/null +++ b/contracts/src/bridge/interfaces/ISenderGateway.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT + +/** + * @authors: [@jaybuidl, @shotaronowhere] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ + +pragma solidity ^0.8.0; + +import "../../bridge/interfaces/IFastBridgeSender.sol"; + +interface ISenderGateway { + function fastBridgeSender() external view returns (IFastBridgeSender); + + function receiverChainID() external view returns (uint256); + + function receiverGateway() external view returns (address); +} diff --git a/contracts/src/bridge/interfaces/gnosis-chain/IAMB.sol b/contracts/src/bridge/interfaces/gnosis-chain/IAMB.sol deleted file mode 100644 index 3f5ca3d5e..000000000 --- a/contracts/src/bridge/interfaces/gnosis-chain/IAMB.sol +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -interface IAMB { - function requireToPassMessage( - address _contract, - bytes memory _data, - uint256 _gas - ) external returns (bytes32); - - function maxGasPerTx() external view returns (uint256); - - function messageSender() external view returns (address); - - function messageSourceChainId() external view returns (bytes32); - - function messageId() external view returns (bytes32); -} diff --git a/contracts/src/bridge/merkle/MerkleProof.sol b/contracts/src/bridge/merkle/MerkleProof.sol index c01647408..a7a1ee369 100644 --- a/contracts/src/bridge/merkle/MerkleProof.sol +++ b/contracts/src/bridge/merkle/MerkleProof.sol @@ -21,34 +21,21 @@ contract MerkleProof { * @param leaf The leaf to validate membership in merkle tree. * @param merkleRoot The root of the merkle tree. */ - function validateProof( + function _validateProof( bytes32[] memory proof, bytes32 leaf, bytes32 merkleRoot ) internal pure returns (bool) { - 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, sha256(data), merkleRoot); + return (merkleRoot == _calculateRoot(proof, leaf)); } /** @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 <= 64, "Invalid Proof"); + require(proofLength <= 32, "Invalid Proof"); bytes32 h = leaf; for (uint256 i = 0; i < proofLength; i++) { bytes32 proofElement = proof[i]; diff --git a/contracts/src/bridge/merkle/MerkleTreeHistory.sol b/contracts/src/bridge/merkle/MerkleTree.sol similarity index 69% rename from contracts/src/bridge/merkle/MerkleTreeHistory.sol rename to contracts/src/bridge/merkle/MerkleTree.sol index 01f6260b0..67dde2ccf 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 + // merkle tree representation of a batch of messages // supports 2^64 messages. - bytes32[64] public branch; - uint256 public count; - - // block number => merkle root history - mapping(uint256 => bytes32) private history; + bytes32[64] private batch; + uint256 internal batchSize; // ************************************* // // * State Modifiers * // @@ -37,19 +34,18 @@ 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 { + 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/ - bytes32 leaf = sha256(data); - uint256 size = count + 1; - count = size; + uint256 size = batchSize + 1; + batchSize = size; uint256 hashBitField = (size ^ (size - 1)) & size; uint256 height; while ((hashBitField & 1) == 0) { - bytes32 node = branch[height]; + bytes32 node = batch[height]; if (node > leaf) assembly { // effecient hash @@ -67,46 +63,35 @@ contract MerkleTreeHistory { hashBitField /= 2; height = height + 1; } - branch[height] = leaf; + 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) { 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/single-message/FastBridgeReceiverOnEthereumSingleMessage.sol b/contracts/src/bridge/single-message/FastBridgeReceiverOnEthereumSingleMessage.sol new file mode 100644 index 000000000..001f6bf1d --- /dev/null +++ b/contracts/src/bridge/single-message/FastBridgeReceiverOnEthereumSingleMessage.sol @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: MIT + +/** + * @authors: [@jaybuidl, @shalzz, @hrishibhat, @shotaronowhere] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ + +pragma solidity ^0.8.0; + +import "./SafeBridgeReceiverOnEthereum.sol"; +import "./interfaces/IFastBridgeReceiver.sol"; + +/** + * Fast Bridge Receiver on Ethereum from Arbitrum + * Counterpart of `FastBridgeSenderToEthereum` + */ +contract FastBridgeReceiverOnEthereumSingleMessage 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; + } + + // ************************************* // + // * 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. + + /** + * @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. + */ + constructor( + address _governor, + address _safeBridgeSender, + address _inbox, + uint256 _claimDeposit, + uint256 _challengeDeposit, + uint256 _challengeDuration, + uint256 _alpha + ) SafeBridgeReceiverOnEthereum(_governor, _safeBridgeSender, _inbox) { + claimDeposit = _claimDeposit; + challengeDeposit = _challengeDeposit; + challengeDuration = _challengeDuration; + alpha = _alpha; + } + + // ************************************* // + // * State 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. + */ + 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); + } +} diff --git a/contracts/src/bridge/single-message/FastBridgeReceiverOnGnosisSingleMessage.sol b/contracts/src/bridge/single-message/FastBridgeReceiverOnGnosisSingleMessage.sol new file mode 100644 index 000000000..3d09f90a1 --- /dev/null +++ b/contracts/src/bridge/single-message/FastBridgeReceiverOnGnosisSingleMessage.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT + +/** + * @authors: [@jaybuidl] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ + +pragma solidity ^0.8.0; + +import "./interfaces/IFastBridgeReceiver.sol"; + +/** + * Fast Bridge Receiver on Gnosis from Arbitrum + * Counterpart of `FastBridgeSenderToGnosis` + */ +abstract contract FastBridgeReceiverOnGnosisSingleMessage is IFastBridgeReceiver { + // TODO in prealpha-3 +} diff --git a/contracts/src/bridge/FastBridgeSenderToEthereum.sol b/contracts/src/bridge/single-message/FastBridgeSenderToEthereum.sol similarity index 98% rename from contracts/src/bridge/FastBridgeSenderToEthereum.sol rename to contracts/src/bridge/single-message/FastBridgeSenderToEthereum.sol index 1f879268e..abcdc794d 100644 --- a/contracts/src/bridge/FastBridgeSenderToEthereum.sol +++ b/contracts/src/bridge/single-message/FastBridgeSenderToEthereum.sol @@ -35,7 +35,7 @@ contract FastBridgeSenderToEthereum is SafeBridgeSenderToEthereum, IFastBridgeSe 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. + address public fastBridgeSender; // The address of the Fast Bridge sender on Arbitrum, generally the Sender Gateway. uint256 public currentTicketID = 1; // Zero means not set, start at 1. mapping(uint256 => Ticket) public tickets; // The tickets by ticketID. @@ -52,7 +52,7 @@ contract FastBridgeSenderToEthereum is SafeBridgeSenderToEthereum, IFastBridgeSe * @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. + * @param _fastBridgeSender The address of the Fast Bridge sender on Arbitrum, generally the Sender Gateway. */ constructor( address _governor, diff --git a/contracts/src/bridge/FastBridgeSenderToGnosis.sol b/contracts/src/bridge/single-message/FastBridgeSenderToGnosis.sol similarity index 100% rename from contracts/src/bridge/FastBridgeSenderToGnosis.sol rename to contracts/src/bridge/single-message/FastBridgeSenderToGnosis.sol diff --git a/contracts/src/bridge/SafeBridgeReceiverOnEthereum.sol b/contracts/src/bridge/single-message/SafeBridgeReceiverOnEthereum.sol similarity index 96% rename from contracts/src/bridge/SafeBridgeReceiverOnEthereum.sol rename to contracts/src/bridge/single-message/SafeBridgeReceiverOnEthereum.sol index 07a43f833..29f589c5b 100644 --- a/contracts/src/bridge/SafeBridgeReceiverOnEthereum.sol +++ b/contracts/src/bridge/single-message/SafeBridgeReceiverOnEthereum.sol @@ -11,8 +11,8 @@ pragma solidity ^0.8.0; import "./interfaces/ISafeBridgeReceiver.sol"; -import "./interfaces/arbitrum/IInbox.sol"; -import "./interfaces/arbitrum/IOutbox.sol"; +import "../canonical/arbitrum/IInbox.sol"; +import "../canonical/arbitrum/IOutbox.sol"; /** * Safe Bridge Receiver on Ethereum from Arbitrum diff --git a/contracts/src/bridge/SafeBridgeSenderToArbitrumFromEthereum.sol b/contracts/src/bridge/single-message/SafeBridgeSenderToArbitrumFromEthereum.sol similarity index 95% rename from contracts/src/bridge/SafeBridgeSenderToArbitrumFromEthereum.sol rename to contracts/src/bridge/single-message/SafeBridgeSenderToArbitrumFromEthereum.sol index d10baf8d9..2e452dc9a 100644 --- a/contracts/src/bridge/SafeBridgeSenderToArbitrumFromEthereum.sol +++ b/contracts/src/bridge/single-message/SafeBridgeSenderToArbitrumFromEthereum.sol @@ -10,9 +10,9 @@ pragma solidity ^0.8.0; -import "./interfaces/arbitrum/IInbox.sol"; -import "./interfaces/arbitrum/IOutbox.sol"; -import "./interfaces/arbitrum/IArbRetryableTx.sol"; +import "../canonical/arbitrum/IInbox.sol"; +import "../canonical/arbitrum/IOutbox.sol"; +import "../canonical/arbitrum/IArbRetryableTx.sol"; import "./interfaces/ISafeBridgeSender.sol"; /** diff --git a/contracts/src/bridge/SafeBridgeSenderToEthereum.sol b/contracts/src/bridge/single-message/SafeBridgeSenderToEthereum.sol similarity index 92% rename from contracts/src/bridge/SafeBridgeSenderToEthereum.sol rename to contracts/src/bridge/single-message/SafeBridgeSenderToEthereum.sol index 46ae5e3dc..d03061e0a 100644 --- a/contracts/src/bridge/SafeBridgeSenderToEthereum.sol +++ b/contracts/src/bridge/single-message/SafeBridgeSenderToEthereum.sol @@ -10,8 +10,8 @@ pragma solidity ^0.8.0; -import "./interfaces/arbitrum/IArbSys.sol"; -import "./interfaces/arbitrum/AddressAliasHelper.sol"; +import "../canonical/arbitrum/IArbSys.sol"; +import "../canonical/arbitrum/AddressAliasHelper.sol"; import "./interfaces/ISafeBridgeSender.sol"; diff --git a/contracts/src/bridge/SafeBridgeSenderToGnosis.sol b/contracts/src/bridge/single-message/SafeBridgeSenderToGnosis.sol similarity index 93% rename from contracts/src/bridge/SafeBridgeSenderToGnosis.sol rename to contracts/src/bridge/single-message/SafeBridgeSenderToGnosis.sol index 41f9189d8..722852767 100644 --- a/contracts/src/bridge/SafeBridgeSenderToGnosis.sol +++ b/contracts/src/bridge/single-message/SafeBridgeSenderToGnosis.sol @@ -10,7 +10,7 @@ pragma solidity ^0.8.0; -import "./interfaces/gnosis-chain/IAMB.sol"; +import "../canonical/gnosis-chain/IAMB.sol"; import "./interfaces/ISafeBridgeSender.sol"; /** diff --git a/contracts/src/bridge/single-message/interfaces/IFastBridgeReceiver.sol b/contracts/src/bridge/single-message/interfaces/IFastBridgeReceiver.sol new file mode 100644 index 000000000..0b508f8d1 --- /dev/null +++ b/contracts/src/bridge/single-message/interfaces/IFastBridgeReceiver.sol @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: MIT + +/** + * @authors: [@jaybuidl, @shalzz, @hrishibhat, @shotaronowhere] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ + +pragma solidity ^0.8.0; + +interface IFastBridgeReceiver { + // ************************************* // + // * Events * // + // ************************************* // + + /** + * @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. + */ + event ClaimReceived(uint256 indexed ticketID, bytes32 indexed messageHash, uint256 claimedAt); + + /** + * @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. + */ + event ClaimChallenged(uint256 indexed ticketID, uint256 challengedAt); + + // ************************************* // + // * 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. + */ + function claim(uint256 _ticketID, bytes32 _messageHash) 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. + */ + function challenge(uint256 _ticketID) 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. + */ + function verifyAndRelay( + uint256 _ticketID, + uint256 _blockNumber, + bytes calldata _messageData + ) 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. + */ + 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; + + /** + * @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; + + // ************************************* // + // * 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 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); + + /** + * @return amount Basis point of claim or challenge deposit that are lost when dishonest. + */ + function alpha() external view returns (uint256 amount); +} diff --git a/contracts/src/bridge/single-message/interfaces/IFastBridgeSender.sol b/contracts/src/bridge/single-message/interfaces/IFastBridgeSender.sol new file mode 100644 index 000000000..c5ed12ab8 --- /dev/null +++ b/contracts/src/bridge/single-message/interfaces/IFastBridgeSender.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +interface IFastBridgeSender { + // ************************************* // + // * Events * // + // ************************************* // + + /** + * @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. + */ + event OutgoingMessage( + uint256 indexed ticketID, + uint256 blockNumber, + address target, + bytes32 indexed messageHash, + bytes message + ); + + // ************************************* // + // * Function Modifiers * // + // ************************************* // + + /** + * Note: Access must be restricted to the sending application. + * @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. + * @return ticketID The identifier to provide to sendSafeFallback(). + */ + function sendFast(address _receiver, bytes memory _calldata) external returns (uint256 ticketID); + + /** + * @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()`. + * @param _receiver The cross-domain contract address which receives the calldata. + * @param _calldata The receiving domain encoded message data. + */ + function sendSafeFallback( + uint256 _ticketID, + address _receiver, + bytes memory _calldata + ) external payable; +} diff --git a/contracts/src/bridge/single-message/interfaces/ISafeBridgeReceiver.sol b/contracts/src/bridge/single-message/interfaces/ISafeBridgeReceiver.sol new file mode 100644 index 000000000..bb3916563 --- /dev/null +++ b/contracts/src/bridge/single-message/interfaces/ISafeBridgeReceiver.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +abstract contract ISafeBridgeReceiver { + function isSentBySafeBridge() internal view virtual returns (bool); +} diff --git a/contracts/src/bridge/single-message/interfaces/ISafeBridgeSender.sol b/contracts/src/bridge/single-message/interfaces/ISafeBridgeSender.sol new file mode 100644 index 000000000..97cb8f9b4 --- /dev/null +++ b/contracts/src/bridge/single-message/interfaces/ISafeBridgeSender.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +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. + * @return Unique id to track the message request/transaction. + */ + function _sendSafe(address _receiver, bytes memory _calldata) internal virtual returns (uint256); +} diff --git a/contracts/src/bridge/mock/FastBridgeSenderToEthereumMock.sol b/contracts/src/bridge/single-message/mock/FastBridgeSenderToEthereumMock.sol similarity index 100% rename from contracts/src/bridge/mock/FastBridgeSenderToEthereumMock.sol rename to contracts/src/bridge/single-message/mock/FastBridgeSenderToEthereumMock.sol diff --git a/contracts/src/bridge/test/FastBridgeSenderMock.sol b/contracts/src/bridge/test/FastBridgeSenderMock.sol new file mode 100644 index 000000000..226c5f459 --- /dev/null +++ b/contracts/src/bridge/test/FastBridgeSenderMock.sol @@ -0,0 +1,264 @@ +// SPDX-License-Identifier: MIT + +/** + * @authors: [@jaybuidl, @shotaronowhere] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ + +pragma solidity ^0.8.0; + +import "../interfaces/IFastBridgeSender.sol"; +import "../interfaces/ISafeBridgeSender.sol"; +import "../interfaces/ISafeBridgeReceiver.sol"; +import "../canonical/arbitrum/IArbSys.sol"; // Arbiturm sender specific + +/** + * Fast Bridge Sender + * Counterpart of `FastReceiver` + */ +contract FastBridgeSenderMock is IFastBridgeSender, ISafeBridgeSender { + // **************************************** // + // * * // + // * Arbitrum Sender Specific * // + // * * // + // **************************************** // + + // ************************************* // + // * Events * // + // ************************************* // + + event L2ToL1TxCreated(uint256 indexed ticketID); + + // ************************************* // + // * Storage * // + // ************************************* // + + IArbSys public constant ARB_SYS = IArbSys(address(100)); + IArbSys public immutable 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.verifySafeBatch.selector; + bytes memory safeMessageData = abi.encodeWithSelector(methodSelector, _epoch, batchMerkleRoot); + + _sendSafe(safeBridgeReceiver, safeMessageData); + } + + function _sendSafe(address _receiver, bytes memory _calldata) internal override returns (bytes32) { + uint256 ticketID = arbsys.sendTxToL1(_receiver, _calldata); + + emit L2ToL1TxCreated(ticketID); + return bytes32(ticketID); + } + + /** + * @dev Constructor. + * @param _epochPeriod The duration between epochs. + * @param _safeBridgeReceiver The the Safe Bridge Router on Ethereum to the receiving chain. + * @param _arbsys The address of the mock ArbSys contract. + */ + constructor( + uint256 _epochPeriod, + address _safeBridgeReceiver, + address _arbsys + ) { + epochPeriod = _epochPeriod; + safeBridgeReceiver = _safeBridgeReceiver; + currentBatchID = block.timestamp / _epochPeriod; + arbsys = IArbSys(address(_arbsys)); + } + + // ************************************** // + // * * // + // * General Sender * // + // * * // + // ************************************** // + + // ************************************* // + // * Storage * // + // ************************************* // + + uint256 public immutable epochPeriod; // Epochs mark the period between potential batches of messages. + uint256 public currentBatchID; + mapping(uint256 => bytes32) public fastOutbox; // epoch count => merkle root of batched messages + address public immutable safeBridgeReceiver; + + // merkle tree representation of a batch of messages + // supports 2^64 messages. + bytes32[64] public batch; + uint256 public batchSize; + + // ************************************* // + // * 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 _calldata The receiving domain encoded message data / function arguments. + */ + function sendFast(address _receiver, bytes memory _calldata) external override { + (bytes32 fastMessageHash, bytes memory fastMessage) = _encode(_receiver, _calldata); + + emit MessageReceived(fastMessage, fastMessageHash); + + appendMessage(fastMessageHash); // add message to merkle tree + } + + /** + * Sends a batch of arbitrary message from one domain to another + * via the fast bridge mechanism. + */ + function sendBatch() external override { + uint256 epoch = block.timestamp / epochPeriod; + require(fastOutbox[epoch] == 0, "Batch already sent for the current epoch."); + require(batchSize > 0, "No messages to send."); + + // set merkle root in outbox and reset merkle tree + bytes32 batchMerkleRoot = getMerkleRoot(); + fastOutbox[epoch] = batchMerkleRoot; + emit BatchOutgoing(currentBatchID, batchSize, epoch, batchMerkleRoot); + + // reset + batchSize = 0; + currentBatchID = epoch; + } + + // ************************ // + // * Internal * // + // ************************ // + + function _encode(address _receiver, bytes memory _calldata) + internal + view + returns (bytes32 fastMessageHash, bytes memory fastMessage) + { + // Encode the receiver address with the function signature + arguments i.e calldata + bytes32 sender = bytes32(bytes20(msg.sender)); + bytes32 receiver = bytes32(bytes20(_receiver)); + uint256 nonce = batchSize; + // add sender and receiver with proper function selector formatting + // [length][receiver: 1 slot padded][offset][function selector: 4 bytes no padding][msg.sender: 1 slot padded][function arguments: 1 slot padded] + assembly { + fastMessage := mload(0x40) // free memory pointer + let lengthCalldata := mload(_calldata) // calldata length + let lengthFastMesssageCalldata := add(lengthCalldata, 0x20) // add msg.sender + let lengthEncodedMessage := add(lengthFastMesssageCalldata, 0x80) // 1 offsets, receiver, and lengthFastMesssageCalldata + mstore(fastMessage, lengthEncodedMessage) // bytes length + mstore(add(fastMessage, 0x20), nonce) // nonce + mstore(add(fastMessage, 0x4c), receiver) // receiver + mstore(add(fastMessage, 0x60), 0x60) // offset + mstore(add(fastMessage, 0x80), lengthFastMesssageCalldata) // fast message length + mstore( + add(fastMessage, 0xa0), + and(mload(add(_calldata, 0x20)), 0xFFFFFFFF00000000000000000000000000000000000000000000000000000000) + ) // function selector + mstore(add(fastMessage, 0xb0), sender) // sender + + let _cursor := add(fastMessage, 0xc4) // begin copying arguments of function call + let _cursorCalldata := add(_calldata, 0x24) // beginning of arguments + + // copy all arguments + for { + let j := 0x00 + } lt(j, lengthCalldata) { + j := add(j, 0x20) + } { + mstore(_cursor, mload(add(_cursorCalldata, j))) + _cursor := add(_cursor, 0x20) + } + // update free pointer + mstore(0x40, _cursor) + } + // Compute the hash over the message header (batchSize as nonce) and body (fastMessage). + fastMessageHash = sha256(fastMessage); + } + + // ********************************* // + // * Merkle Tree * // + // ********************************* // + + /** @dev Append data into merkle tree. + * `O(log(n))` where + * `n` is the number of leaves. + * Note: Although each insertion is O(log(n)), + * Complexity of n insertions is O(n). + * @param leaf The leaf (already hashed) to insert in the merkle tree. + */ + 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; + 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 Gets the current merkle root. + * `O(log(n))` where + * `n` is the number of leaves. + */ + function getMerkleRoot() internal view returns (bytes32) { + bytes32 node; + uint256 size = batchSize; + uint256 height = 0; + bool isFirstHash = true; + while (size > 0) { + if ((size & 1) == 1) { + // avoid redundant calculation + if (isFirstHash) { + node = batch[height]; + isFirstHash = false; + } else { + bytes32 hash = batch[height]; + // effecient hash + if (hash > node) + assembly { + mstore(0x00, node) + mstore(0x20, hash) + node := keccak256(0x00, 0x40) + } + else + assembly { + mstore(0x00, hash) + mstore(0x20, node) + node := keccak256(0x00, 0x40) + } + } + } + size /= 2; + height++; + } + return node; + } +} diff --git a/contracts/src/bridge/mock/ArbSysMock.sol b/contracts/src/bridge/test/arbitrum/ArbSysMock.sol similarity index 91% rename from contracts/src/bridge/mock/ArbSysMock.sol rename to contracts/src/bridge/test/arbitrum/ArbSysMock.sol index 1b657a6cc..7fd46627a 100644 --- a/contracts/src/bridge/mock/ArbSysMock.sol +++ b/contracts/src/bridge/test/arbitrum/ArbSysMock.sol @@ -10,7 +10,7 @@ pragma solidity ^0.8.0; -import "../interfaces/arbitrum/IArbSys.sol"; +import "../../canonical/arbitrum/IArbSys.sol"; contract ArbSysMock { function sendTxToL1(address destination, bytes calldata calldataForL1) diff --git a/contracts/src/bridge/mock/BridgeMock.sol b/contracts/src/bridge/test/arbitrum/BridgeMock.sol similarity index 96% rename from contracts/src/bridge/mock/BridgeMock.sol rename to contracts/src/bridge/test/arbitrum/BridgeMock.sol index 89cb34cc9..74c0187a1 100644 --- a/contracts/src/bridge/mock/BridgeMock.sol +++ b/contracts/src/bridge/test/arbitrum/BridgeMock.sol @@ -10,7 +10,7 @@ pragma solidity ^0.8.0; -import "../interfaces/arbitrum/IInbox.sol"; +import "../../canonical/arbitrum/IInbox.sol"; contract BridgeMock is IBridge { address public outbox; diff --git a/contracts/src/bridge/mock/InboxMock.sol b/contracts/src/bridge/test/arbitrum/InboxMock.sol similarity index 97% rename from contracts/src/bridge/mock/InboxMock.sol rename to contracts/src/bridge/test/arbitrum/InboxMock.sol index 5d7b91edc..8986ce891 100644 --- a/contracts/src/bridge/mock/InboxMock.sol +++ b/contracts/src/bridge/test/arbitrum/InboxMock.sol @@ -10,7 +10,7 @@ pragma solidity ^0.8.0; -import "../interfaces/arbitrum/IInbox.sol"; +import "../../canonical/arbitrum/IInbox.sol"; contract InboxMock is IInbox { IBridge public arbBridge; diff --git a/contracts/src/bridge/mock/OutboxMock.sol b/contracts/src/bridge/test/arbitrum/OutboxMock.sol similarity index 94% rename from contracts/src/bridge/mock/OutboxMock.sol rename to contracts/src/bridge/test/arbitrum/OutboxMock.sol index ed23d9709..601e99ac5 100644 --- a/contracts/src/bridge/mock/OutboxMock.sol +++ b/contracts/src/bridge/test/arbitrum/OutboxMock.sol @@ -10,7 +10,7 @@ pragma solidity ^0.8.0; -import "../interfaces/arbitrum/IOutbox.sol"; +import "../../canonical/arbitrum/IOutbox.sol"; contract OutboxMock is IOutbox { address public safeBridgeSender; diff --git a/contracts/src/bridge/test/gateways/IReceiverGatewayMock.sol b/contracts/src/bridge/test/gateways/IReceiverGatewayMock.sol new file mode 100644 index 000000000..6ab68a94d --- /dev/null +++ b/contracts/src/bridge/test/gateways/IReceiverGatewayMock.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT + +/** + * @authors: [@jaybuidl, @shotaronowhere] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ + +pragma solidity ^0.8.0; + +import "../../interfaces/IReceiverGateway.sol"; + +interface IReceiverGatewayMock is IReceiverGateway { + /** + * Receive the message from the home gateway. + */ + function receiveMessage(address _messageSender) external; +} diff --git a/contracts/src/bridge/test/gateways/ISenderGatewayMock.sol b/contracts/src/bridge/test/gateways/ISenderGatewayMock.sol new file mode 100644 index 000000000..815967273 --- /dev/null +++ b/contracts/src/bridge/test/gateways/ISenderGatewayMock.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../../interfaces/ISenderGateway.sol"; + +interface ISenderGatewayMock is ISenderGateway {} diff --git a/contracts/src/bridge/test/gateways/ReceiverGatewayMock.sol b/contracts/src/bridge/test/gateways/ReceiverGatewayMock.sol new file mode 100644 index 000000000..6caccfc00 --- /dev/null +++ b/contracts/src/bridge/test/gateways/ReceiverGatewayMock.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT + +/** + * @authors: [@shotaronowhere] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ + +pragma solidity ^0.8.0; + +import "./IReceiverGatewayMock.sol"; + +/** + * Receiver Gateway Mock + * Counterpart of `SenderGatewayMock` + */ +contract ReceiverGatewayMock is IReceiverGateway { + IFastBridgeReceiver public immutable fastBridgeReceiver; + address public immutable override senderGateway; + uint256 public immutable override senderChainID; + + uint256 public messageCount; + uint256 public data; + + constructor( + IFastBridgeReceiver _fastBridgeReceiver, + address _senderGateway, + uint256 _senderChainID + ) { + fastBridgeReceiver = _fastBridgeReceiver; + senderGateway = _senderGateway; + senderChainID = _senderChainID; + } + + modifier onlyFromFastBridge() { + require(address(fastBridgeReceiver) == msg.sender, "Fast Bridge only."); + _; + } + + /** + * Receive the message from the sender gateway. + */ + function receiveMessage(address _messageSender) external onlyFromFastBridge { + require(_messageSender == senderGateway, "Only the sender gateway is allowed."); + _receiveMessage(); + } + + /** + * Receive the message from the sender gateway. + */ + function receiveMessage(address _messageSender, uint256 _data) external onlyFromFastBridge { + require(_messageSender == senderGateway, "Only the sender gateway is allowed."); + _receiveMessage(_data); + } + + function _receiveMessage() internal { + messageCount++; + } + + function _receiveMessage(uint256 _data) internal { + messageCount++; + data = _data; + } +} diff --git a/contracts/src/bridge/test/gateways/SenderGatewayMock.sol b/contracts/src/bridge/test/gateways/SenderGatewayMock.sol new file mode 100644 index 000000000..becd043bd --- /dev/null +++ b/contracts/src/bridge/test/gateways/SenderGatewayMock.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT + +/** + * @authors: [@shotaronowhere] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ + +pragma solidity ^0.8.0; + +import "./IReceiverGatewayMock.sol"; +import "../../interfaces/ISenderGateway.sol"; + +/** + * Sender Gateway + * Counterpart of `ReceiverGatewayMock` + */ +contract SenderGatewayMock is ISenderGateway { + IFastBridgeSender public immutable fastBridgeSender; + address public override receiverGateway; + uint256 public immutable override receiverChainID; + + struct RelayedData { + uint256 arbitrationCost; + address relayer; + } + mapping(bytes32 => RelayedData) public disputeHashtoRelayedData; + + constructor( + IFastBridgeSender _fastBridgeSender, + address _receiverGateway, + uint256 _receiverChainID + ) { + fastBridgeSender = _fastBridgeSender; + receiverGateway = _receiverGateway; + receiverChainID = _receiverChainID; + } + + function sendFastMessage(uint256 _data) external { + bytes4 methodSelector = IReceiverGatewayMock.receiveMessage.selector; + bytes memory data = abi.encodeWithSelector(methodSelector, _data); + + fastBridgeSender.sendFast(receiverGateway, data); + } +} diff --git a/contracts/src/bridge/test/gnosis-chain/MockAMB.sol b/contracts/src/bridge/test/gnosis-chain/MockAMB.sol new file mode 100644 index 000000000..1f6a837c7 --- /dev/null +++ b/contracts/src/bridge/test/gnosis-chain/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 "../../canonical/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/bridge/test/merkle/MerkleProofExposed.sol b/contracts/src/bridge/test/merkle/MerkleProofExposed.sol new file mode 100644 index 000000000..0ac746c5c --- /dev/null +++ b/contracts/src/bridge/test/merkle/MerkleProofExposed.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT + +/** + * @authors: [@shotaronowhere] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ + +pragma solidity ^0.8.0; + +import "../../merkle/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/test/merkle/MerkleTreeExposed.sol b/contracts/src/bridge/test/merkle/MerkleTreeExposed.sol new file mode 100644 index 000000000..db297d6b9 --- /dev/null +++ b/contracts/src/bridge/test/merkle/MerkleTreeExposed.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT + +/** + * @authors: [@shotaronowhere] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ + +pragma solidity ^0.8.0; + +import "../../merkle/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/gateway/ForeignGateway.sol b/contracts/src/gateway/ForeignGateway.sol new file mode 100644 index 000000000..8d0f8ac62 --- /dev/null +++ b/contracts/src/gateway/ForeignGateway.sol @@ -0,0 +1,236 @@ +// SPDX-License-Identifier: MIT + +/** + * @authors: [@jaybuidl, @shotaronowhere, @shalzz] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ + +pragma solidity ^0.8.0; + +import "../arbitration/IArbitrable.sol"; +import "./interfaces/IForeignGateway.sol"; + +/** + * Foreign Gateway + * Counterpart of `HomeGateway` + */ +contract ForeignGateway is IForeignGateway { + // ************************************* // + // * Enums / Structs * // + // ************************************* // + + struct DisputeData { + uint248 id; + bool ruled; + address arbitrable; + uint256 paid; + address relayer; + } + + // ************************************* // + // * Events * // + // ************************************* // + + event OutgoingDispute( + bytes32 disputeHash, + bytes32 blockhash, + uint256 localDisputeID, + uint256 _choices, + bytes _extraData, + address arbitrable + ); + + // ************************************* // + // * Storage * // + // ************************************* // + + uint256 public constant MIN_JURORS = 3; // The global default minimum number of jurors in a dispute. + uint256 public immutable override senderChainID; + address public immutable override senderGateway; + uint256 internal localDisputeID = 1; // The disputeID must start from 1 as the KlerosV1 proxy governor depends on this implementation. We now also depend on localDisputeID not ever being zero. + uint256[] internal feeForJuror; // feeForJuror[subcourtID] + address public governor; + IFastBridgeReceiver public fastBridgeReceiver; + IFastBridgeReceiver public depreciatedFastbridge; + uint256 public depreciatedFastBridgeExpiration; + mapping(bytes32 => DisputeData) public disputeHashtoDisputeData; + + // ************************************* // + // * Function Modifiers * // + // ************************************* // + + modifier onlyFromFastBridge() { + require( + address(fastBridgeReceiver) == msg.sender || + ((block.timestamp < depreciatedFastBridgeExpiration) && address(depreciatedFastbridge) == msg.sender), + "Access not allowed: Fast Bridge only." + ); + _; + } + + modifier onlyByGovernor() { + require(governor == msg.sender, "Access not allowed: Governor only."); + _; + } + + constructor( + address _governor, + IFastBridgeReceiver _fastBridgeReceiver, + uint256[] memory _feeForJuror, + address _senderGateway, + uint256 _senderChainID + ) { + governor = _governor; + fastBridgeReceiver = _fastBridgeReceiver; + feeForJuror = _feeForJuror; + senderGateway = _senderGateway; + senderChainID = _senderChainID; + } + + // ************************************* // + // * Governance * // + // ************************************* // + + /** + * @dev Changes the fastBridge, useful to increase the claim deposit. + * @param _fastBridgeReceiver The address of the new fastBridge. + * @param _gracePeriod The duration to accept messages from the deprecated bridge (if at all). + */ + function changeFastbridge(IFastBridgeReceiver _fastBridgeReceiver, uint256 _gracePeriod) external onlyByGovernor { + // grace period to relay remaining messages in the relay / bridging process + depreciatedFastBridgeExpiration = block.timestamp + _fastBridgeReceiver.epochPeriod() + _gracePeriod; // 2 weeks + depreciatedFastbridge = fastBridgeReceiver; + fastBridgeReceiver = _fastBridgeReceiver; + } + + /** + * @dev Changes the `feeForJuror` property value of a specified subcourt. + * @param _subcourtID The ID of the subcourt. + * @param _feeForJuror The new value for the `feeForJuror` property value. + */ + function changeSubcourtJurorFee(uint96 _subcourtID, uint256 _feeForJuror) external onlyByGovernor { + feeForJuror[_subcourtID] = _feeForJuror; + } + + /** + * @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); + } + + // ************************************* // + // * State Modifiers * // + // ************************************* // + + 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++; + uint256 chainID; + assembly { + chainID := chainid() + } + bytes32 disputeHash = keccak256( + abi.encodePacked( + chainID, + blockhash(block.number - 1), + "createDispute", + disputeID, + _choices, + _extraData, + msg.sender + ) + ); + + disputeHashtoDisputeData[disputeHash] = DisputeData({ + id: uint248(disputeID), + arbitrable: msg.sender, + paid: msg.value, + relayer: address(0), + ruled: false + }); + + emit OutgoingDispute(disputeHash, blockhash(block.number - 1), disputeID, _choices, _extraData, msg.sender); + emit DisputeCreation(disputeID, IArbitrable(msg.sender)); + } + + function arbitrationCost(bytes calldata _extraData) public view override returns (uint256 cost) { + (uint96 subcourtID, uint256 minJurors) = extraDataToSubcourtIDMinJurors(_extraData); + + cost = feeForJuror[subcourtID] * minJurors; + } + + /** + * Relay the rule call from the home gateway to the arbitrable. + */ + function relayRule( + address _messageSender, + bytes32 _disputeHash, + uint256 _ruling, + address _relayer + ) external override onlyFromFastBridge { + require(_messageSender == senderGateway, "Only the homegateway is allowed."); + DisputeData storage dispute = disputeHashtoDisputeData[_disputeHash]; + + require(dispute.id != 0, "Dispute does not exist"); + require(!dispute.ruled, "Cannot rule twice"); + + dispute.ruled = true; + dispute.relayer = _relayer; + + IArbitrable arbitrable = IArbitrable(dispute.arbitrable); + arbitrable.rule(dispute.id, _ruling); + } + + 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"); + + uint256 amount = dispute.paid; + dispute.paid = 0; + payable(dispute.relayer).transfer(amount); + } + + // ************************************* // + // * Public Views * // + // ************************************* // + + function disputeHashToForeignID(bytes32 _disputeHash) external view override returns (uint256) { + return disputeHashtoDisputeData[_disputeHash].id; + } + + // ************************ // + // * Internal * // + // ************************ // + + function extraDataToSubcourtIDMinJurors(bytes memory _extraData) + internal + view + returns (uint96 subcourtID, uint256 minJurors) + { + // Note that here we ignore DisputeKitID + if (_extraData.length >= 64) { + assembly { + // solium-disable-line security/no-inline-assembly + subcourtID := mload(add(_extraData, 0x20)) + minJurors := mload(add(_extraData, 0x40)) + } + if (subcourtID >= feeForJuror.length) subcourtID = 0; + if (minJurors == 0) minJurors = MIN_JURORS; + } else { + subcourtID = 0; + minJurors = MIN_JURORS; + } + } +} 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/HomeGateway.sol b/contracts/src/gateway/HomeGateway.sol new file mode 100644 index 000000000..fb75ecb86 --- /dev/null +++ b/contracts/src/gateway/HomeGateway.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: MIT + +/** + * @authors: [@jaybuidl, @shotaronowhere, @shalzz] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ + +pragma solidity ^0.8.0; + +import "../arbitration/IArbitrator.sol"; +import "../bridge/interfaces/IFastBridgeSender.sol"; +import "./interfaces/IForeignGateway.sol"; +import "./interfaces/IHomeGateway.sol"; + +/** + * Home Gateway + * Counterpart of `ForeignGateway` + */ +contract HomeGateway is IHomeGateway { + // ************************************* // + // * Enums / Structs * // + // ************************************* // + + struct RelayedData { + uint256 arbitrationCost; + address relayer; + } + + // ************************************* // + // * Storage * // + // ************************************* // + + address public governor; + IArbitrator public immutable arbitrator; + IFastBridgeSender public fastBridgeSender; + address public override receiverGateway; + uint256 public immutable override receiverChainID; + mapping(uint256 => bytes32) public disputeIDtoHash; + mapping(bytes32 => uint256) public disputeHashtoID; + mapping(bytes32 => RelayedData) public disputeHashtoRelayedData; + + constructor( + address _governor, + IArbitrator _arbitrator, + IFastBridgeSender _fastBridgeSender, + address _receiverGateway, + uint256 _receiverChainID + ) { + governor = _governor; + arbitrator = _arbitrator; + fastBridgeSender = _fastBridgeSender; + receiverGateway = _receiverGateway; + receiverChainID = _receiverChainID; + + emit MetaEvidence(0, "BRIDGE"); + } + + // ************************************* // + // * Governance * // + // ************************************* // + + /** + * @dev Changes the fastBridge, useful to increase the claim deposit. + * @param _fastBridgeSender The address of the new fastBridge. + */ + function changeFastbridge(IFastBridgeSender _fastBridgeSender) external { + require(governor == msg.sender, "Access not allowed: Governor only."); + fastBridgeSender = _fastBridgeSender; + } + + // ************************************* // + // * State Modifiers * // + // ************************************* // + + /** + * @dev Provide the same parameters as on the originalChain while creating a dispute. Providing incorrect parameters will create a different hash than on the originalChain and will not affect the actual dispute/arbitrable's ruling. + * @param _originalChainID originalChainId + * @param _originalBlockHash originalBlockHash + * @param _originalDisputeID originalDisputeID + * @param _choices number of ruling choices + * @param _extraData extraData + * @param _arbitrable arbitrable + */ + function relayCreateDispute( + uint256 _originalChainID, + bytes32 _originalBlockHash, + uint256 _originalDisputeID, + uint256 _choices, + bytes calldata _extraData, + address _arbitrable + ) external payable override { + bytes32 disputeHash = keccak256( + abi.encodePacked( + _originalChainID, + _originalBlockHash, + "createDispute", + _originalDisputeID, + _choices, + _extraData, + _arbitrable + ) + ); + RelayedData storage relayedData = disputeHashtoRelayedData[disputeHash]; + require(relayedData.relayer == address(0), "Dispute already relayed"); + + // TODO: will mostly be replaced by the actual arbitrationCost paid on the foreignChain. + relayedData.arbitrationCost = arbitrator.arbitrationCost(_extraData); + require(msg.value >= relayedData.arbitrationCost, "Not enough arbitration cost paid"); + + uint256 disputeID = arbitrator.createDispute{value: msg.value}(_choices, _extraData); + disputeIDtoHash[disputeID] = disputeHash; + disputeHashtoID[disputeHash] = disputeID; + relayedData.relayer = msg.sender; + + emit Dispute(arbitrator, disputeID, 0, 0); + } + + 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); + + fastBridgeSender.sendFast(receiverGateway, data); + } + + 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..09700cd7a 100644 --- a/contracts/src/gateway/interfaces/IForeignGateway.sol +++ b/contracts/src/gateway/interfaces/IForeignGateway.sol @@ -1,16 +1,24 @@ // SPDX-License-Identifier: MIT +/** + * @authors: [@jaybuidl, @shotaronowhere, @shalzz] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ + pragma solidity ^0.8.0; import "../../arbitration/IArbitrator.sol"; +import "../../bridge/interfaces/IReceiverGateway.sol"; -interface IForeignGateway is IArbitrator { - function chainID() external view returns (uint256); - +interface IForeignGateway is IArbitrator, IReceiverGateway { /** * Relay the rule call from the home gateway to the arbitrable. */ function relayRule( + address _messageSender, bytes32 _disputeHash, uint256 _ruling, address _forwarder @@ -19,10 +27,5 @@ interface IForeignGateway is IArbitrator { function withdrawFees(bytes32 _disputeHash) external; // For cross-chain Evidence standard - function disputeHashToForeignID(bytes32 _disputeHash) external view returns (uint256); - - function homeChainID() external view returns (uint256); - - function homeGateway() external view returns (address); } diff --git a/contracts/src/gateway/interfaces/IHomeGateway.sol b/contracts/src/gateway/interfaces/IHomeGateway.sol index 99eae4d6e..03c4550e5 100644 --- a/contracts/src/gateway/interfaces/IHomeGateway.sol +++ b/contracts/src/gateway/interfaces/IHomeGateway.sol @@ -1,13 +1,20 @@ // SPDX-License-Identifier: MIT +/** + * @authors: [@jaybuidl, @shotaronowhere, @shalzz] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ + pragma solidity ^0.8.0; import "../../arbitration/IArbitrable.sol"; import "../../evidence/IMetaEvidence.sol"; +import "../../bridge/interfaces/ISenderGateway.sol"; -interface IHomeGateway is IArbitrable, IMetaEvidence { - function chainID() external view returns (uint256); - +interface IHomeGateway is IArbitrable, IMetaEvidence, ISenderGateway { function relayCreateDispute( uint256 _originalChainID, bytes32 _originalBlockHash, @@ -18,10 +25,5 @@ interface IHomeGateway is IArbitrable, IMetaEvidence { ) external payable; // For cross-chain Evidence standard - function disputeHashToHomeID(bytes32 _disputeHash) external view returns (uint256); - - function foreignChainID() external view returns (uint256); - - function foreignGateway() external view returns (address); } diff --git a/contracts/src/gateway/ForeignGatewayOnEthereum.sol b/contracts/src/gateway/single-message/ForeignGatewayOnEthereum.sol similarity index 95% rename from contracts/src/gateway/ForeignGatewayOnEthereum.sol rename to contracts/src/gateway/single-message/ForeignGatewayOnEthereum.sol index c0614bc72..fd570c0f2 100644 --- a/contracts/src/gateway/ForeignGatewayOnEthereum.sol +++ b/contracts/src/gateway/single-message/ForeignGatewayOnEthereum.sol @@ -10,16 +10,16 @@ pragma solidity ^0.8.0; -import "../arbitration/IArbitrable.sol"; -import "../bridge/interfaces/IFastBridgeReceiver.sol"; +import "../../arbitration/IArbitrable.sol"; +import "../../bridge/interfaces/IFastBridgeReceiver.sol"; -import "./interfaces/IForeignGateway.sol"; +import "./interfaces/IForeignGatewaySingleMessage.sol"; /** * Foreign Gateway on Ethereum * Counterpart of `HomeGatewayToEthereum` */ -contract ForeignGatewayOnEthereum is IForeignGateway { +contract ForeignGatewayOnEthereum is IForeignGatewaySingleMessage { // The global default minimum number of jurors in a dispute. uint256 public constant MIN_JURORS = 3; diff --git a/contracts/src/gateway/HomeGatewayToEthereum.sol b/contracts/src/gateway/single-message/HomeGatewayToEthereum.sol similarity index 90% rename from contracts/src/gateway/HomeGatewayToEthereum.sol rename to contracts/src/gateway/single-message/HomeGatewayToEthereum.sol index 6ce6cff60..ec337f1a9 100644 --- a/contracts/src/gateway/HomeGatewayToEthereum.sol +++ b/contracts/src/gateway/single-message/HomeGatewayToEthereum.sol @@ -10,17 +10,17 @@ pragma solidity ^0.8.0; -import "../arbitration/IArbitrator.sol"; -import "../bridge/interfaces/IFastBridgeSender.sol"; +import "../../arbitration/IArbitrator.sol"; +import "../../bridge/single-message/interfaces/IFastBridgeSender.sol"; -import "./interfaces/IForeignGateway.sol"; -import "./interfaces/IHomeGateway.sol"; +import "./interfaces/IForeignGatewaySingleMessage.sol"; +import "./interfaces/IHomeGatewaySingleMessage.sol"; /** * Home Gateway to Ethereum * Counterpart of `ForeignGatewayOnEthereum` */ -contract HomeGatewayToEthereum is IHomeGateway { +contract HomeGatewayToEthereum is IHomeGatewaySingleMessage { mapping(uint256 => bytes32) public disputeIDtoHash; mapping(bytes32 => uint256) public disputeHashtoID; @@ -107,7 +107,7 @@ contract HomeGatewayToEthereum is IHomeGateway { bytes32 disputeHash = disputeIDtoHash[_disputeID]; RelayedData memory relayedData = disputeHashtoRelayedData[disputeHash]; - bytes4 methodSelector = IForeignGateway.relayRule.selector; + bytes4 methodSelector = IForeignGatewaySingleMessage.relayRule.selector; bytes memory data = abi.encodeWithSelector(methodSelector, disputeHash, _ruling, relayedData.relayer); fastbridge.sendFast(foreignGateway, data); diff --git a/contracts/src/gateway/single-message/interfaces/IForeignGatewaySingleMessage.sol b/contracts/src/gateway/single-message/interfaces/IForeignGatewaySingleMessage.sol new file mode 100644 index 000000000..a1a80298b --- /dev/null +++ b/contracts/src/gateway/single-message/interfaces/IForeignGatewaySingleMessage.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../../../arbitration/IArbitrator.sol"; + +interface IForeignGatewaySingleMessage is IArbitrator { + function chainID() external view returns (uint256); + + /** + * Relay the rule call from the home gateway to the arbitrable. + */ + function relayRule( + bytes32 _disputeHash, + uint256 _ruling, + address _forwarder + ) external; + + function withdrawFees(bytes32 _disputeHash) external; + + // For cross-chain Evidence standard + + function disputeHashToForeignID(bytes32 _disputeHash) external view returns (uint256); + + function homeChainID() external view returns (uint256); + + function homeGateway() external view returns (address); +} diff --git a/contracts/src/gateway/single-message/interfaces/IHomeGatewaySingleMessage.sol b/contracts/src/gateway/single-message/interfaces/IHomeGatewaySingleMessage.sol new file mode 100644 index 000000000..eda279afa --- /dev/null +++ b/contracts/src/gateway/single-message/interfaces/IHomeGatewaySingleMessage.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../../../arbitration/IArbitrable.sol"; +import "../../../evidence/IMetaEvidence.sol"; + +interface IHomeGatewaySingleMessage is IArbitrable, IMetaEvidence { + function chainID() external view returns (uint256); + + function relayCreateDispute( + uint256 _originalChainID, + bytes32 _originalBlockHash, + uint256 _originalDisputeID, + uint256 _choices, + bytes calldata _extraData, + address _arbitrable + ) external payable; + + // For cross-chain Evidence standard + + function disputeHashToHomeID(bytes32 _disputeHash) external view returns (uint256); + + function foreignChainID() external view returns (uint256); + + function foreignGateway() external view returns (address); +} 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/index.ts b/contracts/test/bridge/merkle/index.ts index c0ed8d854..dda9d43c9 100644 --- a/contracts/test/bridge/merkle/index.ts +++ b/contracts/test/bridge/merkle/index.ts @@ -1,6 +1,5 @@ import { expect } from "chai"; import { ethers } from "hardhat"; -import { BigNumber } from "ethers"; import { toBuffer } from "ethereumjs-util"; import { soliditySha3 } from "web3-utils"; import { MerkleTree } from "./MerkleTree"; @@ -14,14 +13,14 @@ import { MerkleTree } from "./MerkleTree"; * @param leaf The leaf node. * @return valid Whether the proof is valid or not. */ - function verify(proof: string[], root: string, leaf: string) { +function verify(proof: string[], root: string, leaf: string) { return ( root === proof.reduce( (computedHash: string, proofElement: string, currentIndex: number): string => - Buffer.compare(toBuffer(computedHash), toBuffer(proofElement)) <= 0 - ? (soliditySha3(computedHash, proofElement) as string) - : (soliditySha3(proofElement, computedHash) as string), + Buffer.compare(toBuffer(computedHash), toBuffer(proofElement)) <= 0 + ? (soliditySha3(computedHash, proofElement) as string) + : (soliditySha3(proofElement, computedHash) as string), leaf ) ); @@ -29,44 +28,46 @@ import { MerkleTree } from "./MerkleTree"; describe("Merkle", async () => { describe("Sanity tests", async () => { - - let merkleTreeHistory, merkleProof; - let data,nodes,mt; - let rootOnChain,rootOffChain, proof; + let merkleTreeExposed; + let 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 () => { - data = [ - "0x00", - "0x01", - "0x03", - ]; + data = ["0x00", "0x01", "0x03"]; nodes = []; - for (var message of data) { - await merkleTreeHistory.append(message); + for (const message of data) { + await merkleTreeExposed.appendMessage(message); nodes.push(MerkleTree.makeLeafNode(message)); } mt = new MerkleTree(nodes); rootOffChain = mt.getHexRoot(); - rootOnChain = await merkleTreeHistory.getMerkleRoot(); - expect(rootOffChain == rootOnChain).equal(true); + rootOnChain = await merkleTreeExposed.getMerkleRoot(); + console.log("######"); + console.log(rootOffChain); + console.log(rootOnChain); + console.log("########################"); + + expect(rootOffChain).to.equal(rootOnChain); }); - it("Should correctly verify all nodes in the tree", async () => { - for (var message of data) { - const leaf = ethers.utils.sha256(message); - proof = mt.getHexProof(leaf); - const validation = await merkleProof.validateProof(proof, message,rootOnChain); - expect(validation).equal(true); - expect(verify(proof, rootOffChain, leaf)).equal(true); - } + + it("Should correctly verify all nodes in the tree", async () => { + for (const message of data) { + const leaf = ethers.utils.sha256(message); + proof = mt.getHexProof(leaf); + const validation = await merkleProofExposed.validateProof(proof, ethers.utils.sha256(message), rootOnChain); + expect(validation).to.equal(true); + expect(verify(proof, rootOffChain, leaf)).to.equal(true); + } }); }); -}); \ No newline at end of file +}); diff --git a/contracts/test/evidence/index.ts b/contracts/test/evidence/index.ts index 7f4ce5554..57d8b55de 100644 --- a/contracts/test/evidence/index.ts +++ b/contracts/test/evidence/index.ts @@ -142,7 +142,6 @@ describe("Home Evidence contract", async () => { expect(contributions[1]).to.equal(BigNumber.from("93")); expect(contributions[2]).to.equal(ZERO); expect(contributions.length).to.equal(3); - }); it("Should not allowed the same evidence twice for the same evidence group id.", async () => { @@ -185,16 +184,16 @@ describe("Home Evidence contract", async () => { await expect( evidenceModule.moderate(evidenceID, Party.Moderator, { value: totalCost, - gasLimit: 500000, + gasLimit: 500000, }) ).to.be.revertedWith("Moderation market is closed."); - await evidenceModule.resolveModerationMarket(evidenceID, {gasLimit: 500000}); + await evidenceModule.resolveModerationMarket(evidenceID, { gasLimit: 500000 }); // After market has been closed, moderation can re-open. await evidenceModule.moderate(evidenceID, Party.Submitter, { value: totalCost, - gasLimit: 500000, + gasLimit: 500000, }); }); diff --git a/contracts/test/integration/index.ts b/contracts/test/integration/index.ts index 084e141a5..402168a3a 100644 --- a/contracts/test/integration/index.ts +++ b/contracts/test/integration/index.ts @@ -8,7 +8,7 @@ import { FastBridgeReceiverOnEthereum, ForeignGatewayOnEthereum, ArbitrableExample, - FastBridgeSenderToEthereum, + FastBridgeSenderToEthereumMock, HomeGatewayToEthereum, DisputeKitClassic, InboxMock, @@ -17,7 +17,7 @@ import { /* eslint-disable no-unused-vars */ /* eslint-disable no-unused-expressions */ // https://github.com/standard/standard/issues/690#issuecomment-278533482 -describe("Demo pre-alpha1", async () => { +describe("Integration tests", async () => { const ONE_TENTH_ETH = BigNumber.from(10).pow(17); const ONE_ETH = BigNumber.from(10).pow(18); const ONE_HUNDRED_PNK = BigNumber.from(10).pow(20); @@ -42,12 +42,11 @@ describe("Demo pre-alpha1", async () => { drawing, // Jurors can be drawn. } - let deployer, relayer, bridger, challenger, innocentBystander; + let deployer; let ng, disputeKit, pnk, core, fastBridgeReceiver, foreignGateway, arbitrable, fastBridgeSender, homeGateway, inbox; beforeEach("Setup", async () => { - deployer = (await getNamedAccounts()).deployer; - relayer = (await getNamedAccounts()).relayer; + ({ deployer } = await getNamedAccounts()); console.log("deployer:%s", deployer); console.log("named accounts: %O", await getNamedAccounts()); @@ -63,7 +62,7 @@ describe("Demo pre-alpha1", async () => { fastBridgeReceiver = (await ethers.getContract("FastBridgeReceiverOnEthereum")) as FastBridgeReceiverOnEthereum; foreignGateway = (await ethers.getContract("ForeignGatewayOnEthereum")) as ForeignGatewayOnEthereum; arbitrable = (await ethers.getContract("ArbitrableExample")) as ArbitrableExample; - fastBridgeSender = (await ethers.getContract("FastBridgeSenderToEthereumMock")) as FastBridgeSenderToEthereum; + fastBridgeSender = (await ethers.getContract("FastBridgeSenderToEthereumMock")) as FastBridgeSenderToEthereumMock; homeGateway = (await ethers.getContract("HomeGatewayToEthereum")) as HomeGatewayToEthereum; inbox = (await ethers.getContract("InboxMock")) as InboxMock; }); @@ -81,9 +80,9 @@ describe("Demo pre-alpha1", async () => { expect(rn).to.equal(rnOld.add(1)); }); - it("Demo - Honest Claim - No Challenge - Bridger paid", async () => { + it("Honest Claim - No Challenge - Bridger paid", async () => { const arbitrationCost = ONE_TENTH_ETH.mul(3); - const [bridger, challenger] = await ethers.getSigners(); + const [bridger, challenger, relayer] = await ethers.getSigners(); await pnk.approve(core.address, ONE_THOUSAND_PNK.mul(100)); @@ -132,7 +131,7 @@ describe("Demo pre-alpha1", async () => { // Relayer tx const tx2 = await homeGateway - .connect(await ethers.getSigner(relayer)) + .connect(relayer) .relayCreateDispute(31337, lastBlock.hash, disputeId, 2, "0x00", arbitrable.address, { value: arbitrationCost, }); @@ -141,6 +140,7 @@ describe("Demo pre-alpha1", async () => { 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); @@ -153,7 +153,7 @@ describe("Demo pre-alpha1", async () => { 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 mineBlocks(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()); @@ -179,45 +179,50 @@ describe("Demo pre-alpha1", async () => { 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); + expect(await core.execute(0, 0, 1000)).to.emit(core, "TokenAndETHShift"); + const tx4 = await core.executeRuling(0); - expect(tx4).to.emit(fastBridgeSender, "OutgoingMessage"); + expect(tx4).to.emit(fastBridgeSender, "MessageReceived"); + const MessageReceived = fastBridgeSender.filters.MessageReceived(); + const event5 = await fastBridgeSender.queryFilter(MessageReceived); + const fastMessage = event5[0].args.fastMessage; - 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); + // relayer tx - send batch + const tx4a = await fastBridgeSender.connect(bridger).sendBatch(); + expect(tx4a).to.emit(fastBridgeSender, "BatchOutgoing"); + // expect(tx4a).to.emit(fastBridgeSender, "SentSafe"); // does not work because FastBridgeSender is just a (bad) mock. - 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 BatchOutgoing = fastBridgeSender.filters.BatchOutgoing(); + const event5a = await fastBridgeSender.queryFilter(BatchOutgoing); + const batchID = event5a[0].args.batchID; + const batchMerkleRoot = event5a[0].args.batchMerkleRoot; - 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]); + const tx5 = await fastBridgeReceiver.connect(bridger).claim(batchID, batchMerkleRoot, { value: ONE_TENTH_ETH }); + expect(tx5).to.emit(fastBridgeReceiver, "ClaimReceived").withArgs(batchID, batchMerkleRoot); + + // wait for challenge period (and epoch) to pass + await network.provider.send("evm_increaseTime", [86400]); await network.provider.send("evm_mine"); - const tx7 = await fastBridgeReceiver.connect(bridger).verifyAndRelay(ticketID, blockNumber, messageData); + const tx7a = await fastBridgeReceiver.connect(bridger).verifyBatch(batchID); + expect(tx7a).to.emit(fastBridgeReceiver, "BatchVerified").withArgs(batchID); + + const tx7 = await fastBridgeReceiver.connect(relayer).verifyAndRelayMessage(batchID, [], fastMessage); + expect(tx7).to.emit(fastBridgeReceiver, "MessageRelayed").withArgs(batchID, 0); expect(tx7).to.emit(arbitrable, "Ruling"); - const tx8 = await fastBridgeReceiver.withdrawClaimDeposit(ticketID); + const tx8 = await fastBridgeReceiver.withdrawClaimDeposit(batchID); + expect(tx8).to.emit(fastBridgeReceiver, "ClaimDepositWithdrawn").withArgs(batchID, bridger.address); + + expect(fastBridgeReceiver.withdrawChallengeDeposit(batchID)).to.be.revertedWith("Challenge does not exist"); }); - it("Demo - Honest Claim - Challenged - Bridger Paid, Challenger deposit forfeited", async () => { + it("Honest Claim - Dishonest Challenge - Bridger paid, Challenger deposit forfeited", async () => { const arbitrationCost = ONE_TENTH_ETH.mul(3); - const [bridger, challenger] = await ethers.getSigners(); + const [bridger, challenger, relayer] = await ethers.getSigners(); await pnk.approve(core.address, ONE_THOUSAND_PNK.mul(100)); @@ -282,7 +287,7 @@ describe("Demo pre-alpha1", async () => { expect(events2[0].args._disputeID).to.equal(disputeId); // Relayer tx const tx2 = await homeGateway - .connect(await ethers.getSigner(relayer)) + .connect(relayer) .relayCreateDispute(31337, lastBlock.hash, disputeId, 2, "0x00", arbitrable.address, { value: arbitrationCost, }); @@ -304,7 +309,7 @@ describe("Demo pre-alpha1", async () => { 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 mineBlocks(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()); @@ -345,75 +350,65 @@ describe("Demo pre-alpha1", async () => { 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); + console.log("Executed ruling"); - expect(tx4).to.emit(fastBridgeSender, "OutgoingMessage"); + expect(tx4).to.emit(fastBridgeSender, "MessageReceived"); - console.log("Executed ruling"); + const MessageReceived = fastBridgeSender.filters.MessageReceived(); + const event4 = await fastBridgeSender.queryFilter(MessageReceived); + const fastMessage = event4[0].args.fastMessage; - 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 tx4a = await fastBridgeSender.connect(bridger).sendBatch(); + expect(tx4a).to.emit(fastBridgeSender, "BatchOutgoing"); - const currentID = await fastBridgeSender.currentTicketID(); - expect(currentID).to.equal(2); + const BatchOutgoing = fastBridgeSender.filters.BatchOutgoing(); + const event4a = await fastBridgeSender.queryFilter(BatchOutgoing); + const batchID = event4a[0].args.batchID; + const batchMerkleRoot = event4a[0].args.batchMerkleRoot; - // 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); + console.log("Executed ruling"); + + // bridger tx starts - Honest Bridger + const tx5 = await fastBridgeReceiver.connect(bridger).claim(batchID, batchMerkleRoot, { value: ONE_TENTH_ETH }); + expect(tx5).to.emit(fastBridgeReceiver, "ClaimReceived").withArgs(batchID, batchMerkleRoot); // 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]); + const tx6 = await fastBridgeReceiver.connect(challenger).challenge(batchID, { value: ONE_TENTH_ETH }); + expect(tx6).to.emit(fastBridgeReceiver, "ClaimChallenged").withArgs(batchID); + + // wait for challenge period (and epoch) to pass + await network.provider.send("evm_increaseTime", [86400]); await network.provider.send("evm_mine"); - await expect( - fastBridgeReceiver.connect(bridger).verifyAndRelay(ticketID, blockNumber, messageData) - ).to.be.revertedWith("Claim is challenged"); + await expect(fastBridgeReceiver.connect(relayer).verifyBatch(batchID)).to.not.emit( + fastBridgeReceiver, + "BatchVerified" + ); - 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"); + const tx7 = await fastBridgeSender.connect(bridger).sendSafeFallback(batchID, { gasLimit: 1000000 }); + expect(tx7).to.emit(fastBridgeSender, "L2ToL1TxCreated").withArgs(0); + // expect(tx7).to.emit(fastBridgeSender, "SentSafe"); // does not work because FastBridgeSender is just a (bad) mock. + + const tx8 = await fastBridgeReceiver.connect(bridger).verifySafeBatch(batchID, batchMerkleRoot); + expect(tx8).to.emit(fastBridgeReceiver, "BatchSafeVerified").withArgs(batchID, true, false); + + const tx9 = await fastBridgeReceiver.connect(relayer).verifyAndRelayMessage(batchID, [], fastMessage); + expect(tx9).to.emit(fastBridgeReceiver, "MessageRelayed").withArgs(batchID, 0); + expect(tx9).to.emit(arbitrable, "Ruling"); + + const tx10 = await fastBridgeReceiver.connect(relayer).withdrawClaimDeposit(batchID); + expect(tx10).to.emit(fastBridgeReceiver, "ClaimDepositWithdrawn").withArgs(batchID, bridger.address); - await expect(fastBridgeReceiver.withdrawChallengeDeposit(ticketID)).to.be.revertedWith( - "Claim verified: deposit forfeited" + await expect(fastBridgeReceiver.connect(relayer).withdrawChallengeDeposit(batchID)).to.be.revertedWith( + "Challenge failed." ); }); - it("Demo - Dishonest Claim - Challenged - Bridger deposit forfeited, Challenger paid", async () => { + it("Dishonest Claim - Honest Challenge - Bridger deposit forfeited, Challenger paid", async () => { const arbitrationCost = ONE_TENTH_ETH.mul(3); - const [bridger, challenger] = await ethers.getSigners(); + const [bridger, challenger, relayer] = await ethers.getSigners(); await pnk.approve(core.address, ONE_THOUSAND_PNK.mul(100)); @@ -461,7 +456,7 @@ describe("Demo pre-alpha1", async () => { // Relayer tx const tx2 = await homeGateway - .connect(await ethers.getSigner(relayer)) + .connect(relayer) .relayCreateDispute(31337, lastBlock.hash, disputeId, 2, "0x00", arbitrable.address, { value: arbitrationCost, }); @@ -482,7 +477,7 @@ describe("Demo pre-alpha1", async () => { 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 mineBlocks(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()); @@ -509,88 +504,62 @@ describe("Demo pre-alpha1", async () => { 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); + expect(tx4).to.emit(fastBridgeSender, "MessageReceived"); + + const MessageReceived = fastBridgeSender.filters.MessageReceived(); + const event4 = await fastBridgeSender.queryFilter(MessageReceived); + const fastMessage = event4[0].args.fastMessage; - const currentID = await fastBridgeSender.currentTicketID(); - expect(currentID).to.equal(2); + const tx4a = await fastBridgeSender.connect(bridger).sendBatch(); + expect(tx4a).to.emit(fastBridgeSender, "BatchOutgoing"); + + const BatchOutgoing = fastBridgeSender.filters.BatchOutgoing(); + const event4a = await fastBridgeSender.queryFilter(BatchOutgoing); + const batchID = event4a[0].args.batchID; + const batchMerkleRoot = event4a[0].args.batchMerkleRoot; // 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); + const fakeData = "KlerosToTheMoon"; + const fakeHash = utils.keccak256(utils.defaultAbiCoder.encode(["string"], [fakeData])); + const tx5 = await fastBridgeReceiver.connect(bridger).claim(batchID, fakeHash, { value: ONE_TENTH_ETH }); // 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]); + const tx6 = await fastBridgeReceiver.connect(challenger).challenge(batchID, { value: ONE_TENTH_ETH }); + expect(tx6).to.emit(fastBridgeReceiver, "ClaimChallenged").withArgs(batchID); + + // wait for challenge period (and epoch) to pass + await network.provider.send("evm_increaseTime", [86400]); await network.provider.send("evm_mine"); - await expect( - fastBridgeReceiver.connect(bridger).verifyAndRelay(ticketID, blockNumber, fakeData) - ).to.be.revertedWith("Claim is challenged"); + await expect(fastBridgeReceiver.connect(relayer).verifyBatch(batchID)).to.not.emit( + fastBridgeReceiver, + "BatchVerified" + ); - let data = await ethers.utils.defaultAbiCoder.decode(["address", "bytes"], fakeData); + const tx7 = await fastBridgeSender.connect(bridger).sendSafeFallback(batchID, { gasLimit: 1000000 }); + expect(tx7).to.emit(fastBridgeSender, "L2ToL1TxCreated").withArgs(0); + // expect(tx7).to.emit(fastBridgeSender, "SentSafe"); // does not work because FastBridgeSender is just a (bad) mock. - await expect( - fastBridgeSender - .connect(bridger) - .sendSafeFallbackMock(ticketID, foreignGateway.address, data[1], { gasLimit: 1000000 }) - ).to.be.revertedWith("Invalid message for ticketID."); + const tx8 = await fastBridgeReceiver.connect(bridger).verifySafeBatch(batchID, batchMerkleRoot); + expect(tx8).to.emit(fastBridgeReceiver, "BatchSafeVerified").withArgs(batchID, false, true); - 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"); + const tx9 = await fastBridgeReceiver.connect(relayer).verifyAndRelayMessage(batchID, [], fastMessage); + expect(tx9).to.emit(fastBridgeReceiver, "MessageRelayed").withArgs(batchID, 0); + expect(tx9).to.emit(arbitrable, "Ruling"); - await expect(fastBridgeReceiver.withdrawClaimDeposit(ticketID)).to.be.revertedWith( - "Claim not verified: deposit forfeited" - ); - await fastBridgeReceiver.withdrawChallengeDeposit(ticketID); + expect(fastBridgeReceiver.connect(relayer).withdrawClaimDeposit(batchID)).to.be.revertedWith("Claim failed."); + + const tx10 = await fastBridgeReceiver.connect(relayer).withdrawChallengeDeposit(batchID); + expect(tx10).to.emit(fastBridgeReceiver, "ChallengeDepositWithdrawn").withArgs(batchID, challenger.address); }); - async function mineNBlocks(n) { + async function mineBlocks(n) { for (let index = 0; index < n; index++) { await network.provider.send("evm_mine"); }