// SPDX-License-Identifier: MIT /// @custom:authors: [@shotaronowhere, @jaybuidl] /// @custom:reviewers: [] /// @custom:auditors: [] /// @custom:bounties: [] /// @custom:deployments: [] pragma solidity 0.8.24; import "../canonical/gnosis-chain/IAMB.sol"; import "../canonical/arbitrum/IBridge.sol"; import "../canonical/arbitrum/IOutbox.sol"; import "../canonical/arbitrum/ISequencerInbox.sol"; import "../interfaces/routers/IRouterToGnosis.sol"; import "../interfaces/outboxes/IVeaOutboxOnL1.sol"; import "../interfaces/updaters/ISequencerDelayUpdatable.sol"; /// @dev Router from Arbitrum to Gnosis Chain. /// Note: This contract is deployed on Ethereum. contract RouterArbToGnosis is IRouterToGnosis { // ************************************* // // * Storage * // // ************************************* // IBridge public immutable bridge; // The address of the Arbitrum bridge contract. IAMB public immutable amb; // The address of the AMB contract on Ethereum. address public immutable veaInboxArbToGnosis; // The address of the veaInbox on Arbitrum. address public immutable veaOutboxArbToGnosis; // The address of the veaOutbox on Gnosis Chain. uint256 public sequencerDelayLimit; // This is MaxTimeVariation.delaySeconds from the arbitrum sequencer inbox, it is the maximum seconds the sequencer can backdate L2 txns relative to the L1 clock. SequencerLimitDecreaseRequest public sequencerDelayLimitDecreaseRequest; // Decreasing the sequencerDelayLimit requires a delay to avoid griefing by sequencer, so we keep track of the request here. struct SequencerLimitDecreaseRequest { uint256 requestedSequencerLimit; uint256 timestamp; } // ************************************* // // * Events * // // ************************************* // /// @dev Event emitted when a message is relayed to another Safe Bridge. /// @param _epoch The epoch of the batch requested to send. /// @param _ticketID The unique identifier provided by the underlying canonical bridge. event Routed(uint256 indexed _epoch, bytes32 _ticketID); /// @dev This event indicates a cross-chain message was sent to inform the veaOutbox of the sequencer limit value /// @param _ticketID The ticketID from the AMB of the cross-chain message. event SequencerDelayLimitSent(bytes32 _ticketID); /// @dev This event indicates the sequencer limit updated. /// @param _newSequencerDelayLimit The new sequencer delay limit. event SequencerDelayLimitUpdated(uint256 _newSequencerDelayLimit); /// @dev This event indicates that a request to decrease the sequencer limit has been made. /// @param _requestedSequencerDelayLimit The new sequencer limit requested. event SequencerDelayLimitDecreaseRequested(uint256 _requestedSequencerDelayLimit); /// @dev Constructor. /// @param _bridge The address of the arbitrum bridge contract on Ethereum. /// @param _amb The address of the AMB contract on Ethereum. /// @param _veaInboxArbToGnosis The vea inbox on Arbitrum. /// @param _veaOutboxArbToGnosis The vea outbox on Gnosis Chain. constructor(IBridge _bridge, IAMB _amb, address _veaInboxArbToGnosis, address _veaOutboxArbToGnosis) { bridge = _bridge; amb = _amb; veaInboxArbToGnosis = _veaInboxArbToGnosis; veaOutboxArbToGnosis = _veaOutboxArbToGnosis; (, , sequencerDelayLimit, ) = ISequencerInbox(bridge.sequencerInbox()).maxTimeVariation(); } // ************************************* // // * Parameter Updates * // // ************************************* // /// @dev Update the sequencerDelayLimit. If decreasing, a delayed request is created for later execution. function updateSequencerDelayLimit() public { // the maximum asynchronous lag between the L2 and L1 clocks (, , uint256 newSequencerDelayLimit, ) = ISequencerInbox(bridge.sequencerInbox()).maxTimeVariation(); if (newSequencerDelayLimit > sequencerDelayLimit) { // For sequencerDelayLimit / epochPeriod > timeoutEpochs, claims cannot be verified by the timeout period and the bridge will shutdown. sequencerDelayLimit = newSequencerDelayLimit; sendSequencerDelayLimit(); emit SequencerDelayLimitUpdated(newSequencerDelayLimit); } else if (newSequencerDelayLimit < sequencerDelayLimit) { require( sequencerDelayLimitDecreaseRequest.timestamp == 0, "Sequencer limit decrease request already pending." ); sequencerDelayLimitDecreaseRequest = SequencerLimitDecreaseRequest({ requestedSequencerLimit: newSequencerDelayLimit, timestamp: block.timestamp }); emit SequencerDelayLimitDecreaseRequested(newSequencerDelayLimit); } } /// @dev execute sequencerDelayLimitDecreaseRequest function executeSequencerDelayLimitDecreaseRequest() external { require(sequencerDelayLimitDecreaseRequest.timestamp != 0, "No pending sequencer limit decrease request."); require( block.timestamp > sequencerDelayLimitDecreaseRequest.timestamp + sequencerDelayLimit, "Sequencer limit decrease request is still pending." ); uint256 requestedSequencerDelayLimit = sequencerDelayLimitDecreaseRequest.requestedSequencerLimit; delete sequencerDelayLimitDecreaseRequest; (, , uint256 currentSequencerDelayLimit, ) = ISequencerInbox(bridge.sequencerInbox()).maxTimeVariation(); // check the request is still consistent with the arbitrum bridge if (currentSequencerDelayLimit == requestedSequencerDelayLimit) { sequencerDelayLimit = requestedSequencerDelayLimit; sendSequencerDelayLimit(); emit SequencerDelayLimitUpdated(requestedSequencerDelayLimit); } } /// @dev Send the sequencer delay limit. function sendSequencerDelayLimit() internal { bytes memory data = abi.encodeCall( ISequencerDelayUpdatable.updateSequencerDelayLimit, (sequencerDelayLimit, block.timestamp) ); // Note: using maxGasPerTx here means the relaying txn on Gnosis will need to pass that (large) amount of gas, though almost all will be unused and refunded. This is preferred over hardcoding a gas limit. bytes32 ticketID = amb.requireToPassMessage(veaOutboxArbToGnosis, data, amb.maxGasPerTx()); emit SequencerDelayLimitSent(ticketID); } // ************************************* // // * State Modifiers * // // ************************************* // /// Note: Access restricted to arbitrum canonical bridge. /// @dev Resolves any challenge of the optimistic claim for '_epoch'. /// @param _epoch The epoch to verify. /// @param _stateroot The true batch merkle root for the epoch. /// @param _gasLimit The true batch gas limit for the epoch. /// @param _claim The claim associated with the epoch. function route(uint256 _epoch, bytes32 _stateroot, uint256 _gasLimit, Claim calldata _claim) external { // Arbitrum -> Ethereum message sender authentication // docs: https://developer.arbitrum.io/arbos/l2-to-l1-messaging/ // example: https://github.com/OffchainLabs/arbitrum-tutorials/blob/2c1b7d2db8f36efa496e35b561864c0f94123a5f/packages/greeter/contracts/ethereum/GreeterL1.sol#L50 // example: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/dfef6a68ee18dbd2e1f5a099061a3b8a0e404485/contracts/crosschain/arbitrum/LibArbitrumL1.sol#L34 // note: we use the bridge address as a source of truth for the activeOutbox address require(msg.sender == address(bridge), "Not from bridge."); require(IOutbox(bridge.activeOutbox()).l2ToL1Sender() == veaInboxArbToGnosis, "veaInbox only."); // Ethereum -> Gnosis message passing with the AMB, the canonical Ethereum <-> Gnosis bridge. // https://docs.tokenbridge.net/amb-bridge/development-of-a-cross-chain-application/how-to-develop-xchain-apps-by-amb#receive-a-method-call-from-the-amb-bridge bytes memory data = abi.encodeCall(IVeaOutboxOnL1.resolveDisputedClaim, (_epoch, _stateroot, _claim)); uint256 maxGasPerTx = amb.maxGasPerTx(); uint256 gasLimitCapped = _gasLimit > maxGasPerTx ? maxGasPerTx : _gasLimit; bytes32 ticketID = amb.requireToPassMessage(veaOutboxArbToGnosis, data, gasLimitCapped); emit Routed(_epoch, ticketID); } }