diff --git a/.github/workflows/build-test-deploy.yml b/.github/workflows/build-test-deploy.yml index 6da92fe852..7577880482 100644 --- a/.github/workflows/build-test-deploy.yml +++ b/.github/workflows/build-test-deploy.yml @@ -84,13 +84,18 @@ jobs: - name: Migrate Database run: yarn workspace @connext/nxtp-adapters-database dbmate up + # TODO: remove before merging - name: Yarn test - run: yarn test:all + run: yarn test:contracts - - name: Yarn lint - env: - NODE_OPTIONS: "--max-old-space-size=8192" - run: yarn lint:all + # TODO: uncomment both before merging + # - name: Yarn test + # run: yarn test:all + + # - name: Yarn lint + # env: + # NODE_OPTIONS: "--max-old-space-size=8192" + # run: yarn lint:all - name: Install jq run: sudo apt-get install -y jq diff --git a/package.json b/package.json index a229fc3c88..b78888ddc7 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "scripts": { "lint:all": "yarn workspaces foreach --parallel --exclude @connext/nxtp-subgraph --exclude @connext/bridge-reference --exclude @connext/nxtp-integration --exclude @connext/smart-contracts run lint", "test:all": "yarn workspaces foreach --parallel --exclude @connext/nxtp-relayer --exclude @connext/nxtp-subgraph --exclude @connext/nxtp-integration --exclude @connext/sdk run test", + "test:contracts": "yarn workspace @connext/smart-contracts run test", "clean:all": "yarn workspaces foreach --parallel --exclude @connext/nxtp-integration --exclude @connext/nxtp-subgraph run clean", "build:all": "yarn workspaces foreach --parallel -p --topological-dev --exclude @connext/nxtp-subgraph --exclude @connext/bridge-reference run build", "verify:all": "yarn test:all && yarn clean:all && yarn build:all && yarn lint:all --max-warnings 0", diff --git a/packages/deployments/contracts/contracts/messaging/RootManager.sol b/packages/deployments/contracts/contracts/messaging/RootManager.sol index 9278244e1b..e0ae9fc816 100644 --- a/packages/deployments/contracts/contracts/messaging/RootManager.sol +++ b/packages/deployments/contracts/contracts/messaging/RootManager.sol @@ -14,6 +14,8 @@ import {SnapshotId} from "./libraries/SnapshotId.sol"; import {MerkleTreeManager} from "./MerkleTreeManager.sol"; import {WatcherClient} from "./WatcherClient.sol"; +import {IHubSpokeConnector} from "./interfaces/IHubSpokeConnector.sol"; + /** * @notice This contract exists at cluster hubs, and aggregates all transfer roots from messaging * spokes into a single merkle tree. Regularly broadcasts the root of the aggregator tree back out @@ -34,10 +36,6 @@ contract RootManager is ProposedOwnable, IRootManager, WatcherClient, DomainInde event RootReceived(uint32 domain, bytes32 receivedRoot, uint256 queueIndex); - event RootsAggregated(bytes32 aggregateRoot, uint256 count, bytes32[] aggregatedMessageRoots); - - event RootPropagated(bytes32 aggregateRoot, uint256 count, bytes32 domainsHash); - event RootDiscarded(bytes32 fraudulentRoot); /** @@ -70,11 +68,12 @@ contract RootManager is ProposedOwnable, IRootManager, WatcherClient, DomainInde event OptimisticModeActivated(); /** - * @notice Emitted when an optimistic root is propagated + * @notice Emitted when a root is propagated + * @dev It doesnt matter if the root was generated optimistically or on-chain. * @param aggregateRoot The aggregate root propagated * @param domainsHash The current domain hash */ - event OptimisticRootPropagated(bytes32 indexed aggregateRoot, bytes32 domainsHash); + event AggregateRootPropagated(bytes32 indexed aggregateRoot, bytes32 domainsHash); /** * @notice Emitted when a new aggregate root is proposed @@ -95,10 +94,36 @@ contract RootManager is ProposedOwnable, IRootManager, WatcherClient, DomainInde ); /** - * @notice Emitted when the current proposed root is finalized - * @param aggregateRoot The aggregate root finalized + * @notice Emitted when an aggregate root is added to the validAggregateRoots map during optimistic mode. + * @param aggregateRoot The saved aggregate root + * @param rootTimestamp The timestamp at which the aggregate root was saved. + */ + event AggregateRootSavedOptimistic(bytes32 aggregateRoot, uint256 rootTimestamp); + + /** + * @notice Emitted when an aggregate root is added to the validAggregateRoots map during slow mode. + * @param aggregateRoot The saved aggregate root + * @param leafCount The new number of leaves in the tree. + * @param aggregatedRoots The verified inbound roots inserted in the tree. + * @param rootTimestamp The timestamp at which the aggregate root was saved. + */ + event AggregateRootSavedSlow( + bytes32 aggregateRoot, + uint256 leafCount, + bytes32[] aggregatedRoots, + uint256 rootTimestamp + ); + + /** + * @notice Emitted when a domain is set as the hub domain. + * @param domain The domain set as hub domain. */ - event ProposedRootFinalized(bytes32 aggregateRoot); + event HubDomainSet(uint32 domain); + + /** + * @notice Emitted when the previously set hub domain is cleared. + */ + event HubDomainCleared(); // ============ Errors ============ @@ -120,10 +145,6 @@ contract RootManager is ProposedOwnable, IRootManager, WatcherClient, DomainInde error RootManager_onlyProposer__NotWhitelistedProposer(address caller); - error RootManager_optimisticPropagate__ForbiddenOptimisticRoot(); - - error RootManager_slowPropagate__OldAggregateRoot(); - error RootManager_sendRootToHub__NoMessageSent(); error RootManager_finalize__InvalidInputHash(); @@ -138,6 +159,10 @@ contract RootManager is ProposedOwnable, IRootManager, WatcherClient, DomainInde error RootManager__renounceOwnership_prohibited(); + error RootManager_propagate__AggregateRootIsZero(); + + error RootManager_setHubDomain__InvalidDomain(); + // ============ Properties ============ /** @@ -147,7 +172,7 @@ contract RootManager is ProposedOwnable, IRootManager, WatcherClient, DomainInde uint128 public constant DEQUEUE_MAX = 100; /** - * @notice Root used to keep the slots of proposedAggregateRootHash and finalizedOptimisticAggregateRoot warm. + * @notice Root used to keep the slots of proposedAggregateRootHash warm. */ bytes32 public constant FINALIZED_HASH = 0x0000000000000000000000000000000000000000000000000000000000000001; /** @@ -166,19 +191,6 @@ contract RootManager is ProposedOwnable, IRootManager, WatcherClient, DomainInde */ uint256 public minDisputeBlocks; - /** - * @notice The amount of inserted leaves prior to switching to optimistic mode - * @dev Used to prevent the propagation of an old aggregate root right after switching to Slow mode. - * After switching back to slow mode, if the count didnt change from previous slow mode, we consider the root as an old one since - * probably lot of optimistic roots have been propagated. - * Example: Propagation in slow mode happens, switch to Op Mode, multiple propagations happen, switch back to Slow mode. - * If at this point someone calls propagate and the queue is empty or non of elemenets are ready, _slowPropagate will - * call the dequeue function which will return the old root and count before Op mode was activated. And since multiple - * propagations happened while in optimistic mode, the lastPropagatedRoot will be different than the old but current MERKLE.root - * which will lead to propagating a deprecated root. - */ - uint256 public lastCountBeforeOpMode; - /** * @notice True if the system is working in optimistic mode. Otherwise is working in slow mode */ @@ -190,12 +202,6 @@ contract RootManager is ProposedOwnable, IRootManager, WatcherClient, DomainInde */ mapping(uint32 => bytes32) public lastPropagatedRoot; - /** - * @notice The last finalized aggregate root in optimistic mode. - * @dev Set to 0x1 to prevent someone from calling finalize() the moment the contract is deployed - */ - bytes32 public finalizedOptimisticAggregateRoot = 0x0000000000000000000000000000000000000000000000000000000000000001; - /** * @notice Queue used for management of verification for inbound roots from spoke chains. Once * the verification period elapses, the inbound messages can be aggregated into the merkle tree @@ -223,6 +229,27 @@ contract RootManager is ProposedOwnable, IRootManager, WatcherClient, DomainInde */ mapping(address => bool) public allowlistedProposers; + /** + * @notice The list of valid aggregate roots for a given timestamp. + * @dev Each time a new aggregate root is generated or + * finalized, it will be added to this mapping using the block.timestamp as key. + * @dev This is only used as Data-Availability for off-chain agents. Especially for the Watchers that fetch the + * correct aggregate root from this contract in order to verify the data proposed on the Spoke Connectors. + * @dev rootTimestamp => aggregateRoot + */ + mapping(uint256 => bytes32) public validAggregateRoots; + + /** + * @notice Timestamp of the last aggregate root saved. + * @dev Used to ensure that the propagate function will send the latest aggregate root available. + */ + uint256 public lastSavedAggregateRootTimestamp; + + /** + * @notice Domain id of the current network + */ + uint32 public hubDomain; + // ============ Modifiers ============ modifier onlyConnector(uint32 _domain) { @@ -319,10 +346,9 @@ contract RootManager is ProposedOwnable, IRootManager, WatcherClient, DomainInde } /** - * @notice Set the `disputeBlocks`, the duration, in blocks, of the dispute process for - * a given proposed root + * @notice Set the `minDisputeBlocks` variable to the provided parameter. */ - function setMinDisputeBlocks(uint256 _minDisputeBlocks) public onlyOwner { + function setMinDisputeBlocks(uint256 _minDisputeBlocks) external onlyOwner { if (_minDisputeBlocks == minDisputeBlocks) revert RootManager_setMinDisputeBlocks__SameMinDisputeBlocksAsBefore(); emit MinDisputeBlocksUpdated(minDisputeBlocks, _minDisputeBlocks); minDisputeBlocks = _minDisputeBlocks; @@ -332,7 +358,7 @@ contract RootManager is ProposedOwnable, IRootManager, WatcherClient, DomainInde * @notice Set the `disputeBlocks`, the duration, in blocks, of the dispute process for * a given proposed root */ - function setDisputeBlocks(uint256 _disputeBlocks) public onlyOwner { + function setDisputeBlocks(uint256 _disputeBlocks) external onlyOwner { if (_disputeBlocks < minDisputeBlocks) revert RootManager_setDisputeBlocks__DisputeBlocksLowerThanMin(); if (_disputeBlocks == disputeBlocks) revert RootManager_setDisputeBlocks__SameDisputeBlocksAsBefore(); emit DisputeBlocksUpdated(disputeBlocks, _disputeBlocks); @@ -343,7 +369,7 @@ contract RootManager is ProposedOwnable, IRootManager, WatcherClient, DomainInde * @notice Set the `delayBlocks`, the period in blocks over which an incoming message * is verified. */ - function setDelayBlocks(uint256 _delayBlocks) public onlyOwner { + function setDelayBlocks(uint256 _delayBlocks) external onlyOwner { require(_delayBlocks != delayBlocks, "!delayBlocks"); emit DelayBlocksUpdated(delayBlocks, _delayBlocks); delayBlocks = _delayBlocks; @@ -367,13 +393,12 @@ contract RootManager is ProposedOwnable, IRootManager, WatcherClient, DomainInde * @notice Remove support for a connector and respective domain. That connector/domain will no longer * receive updates for the latest aggregate root. * @dev Only watcher can remove a connector. - * @dev The proposedAggregateRootHash will be set to the FINALIZED_HASH while the finalizedOptimisticAggregateRoot - * will continue existing as it is verified. + * @dev The proposedAggregateRootHash will be set to the FINALIZED_HASH. * TODO: Could add a metatx-able `removeConnectorWithSig` if we want to use relayers? * * @param _domain The spoke domain of the target connector we want to remove. */ - function removeConnector(uint32 _domain) public onlyWatcher { + function removeConnector(uint32 _domain) external onlyWatcher { address _connector = removeDomain(_domain); proposedAggregateRootHash = FINALIZED_HASH; emit ConnectorRemoved(_domain, _connector, domains, connectors, msg.sender); @@ -388,7 +413,7 @@ contract RootManager is ProposedOwnable, IRootManager, WatcherClient, DomainInde * * @param _root The root to be discarded. */ - function discardRoot(bytes32 _root) public onlyOwner whenPaused { + function discardRoot(bytes32 _root) external onlyOwner whenPaused { pendingInboundRoots.remove(_root); emit RootDiscarded(_root); } @@ -402,6 +427,25 @@ contract RootManager is ProposedOwnable, IRootManager, WatcherClient, DomainInde revert RootManager__renounceOwnership_prohibited(); } + /** + * @notice Sets domain corresponding to the hub domain. + * + * @param _domain The domain to be set as hub domain. + */ + function setHubDomain(uint32 _domain) external onlyOwner { + if (!isDomainSupported(_domain)) revert RootManager_setHubDomain__InvalidDomain(); + hubDomain = _domain; + emit HubDomainSet(_domain); + } + + /** + * @notice Removes the domain associated with the hub domain. + */ + function clearHubDomain() external onlyOwner { + delete hubDomain; + emit HubDomainCleared(); + } + // ============ Public Functions ============ /** @@ -423,7 +467,7 @@ contract RootManager is ProposedOwnable, IRootManager, WatcherClient, DomainInde bytes32 _aggregateRoot, bytes32[] calldata _snapshotsRoots, uint32[] calldata _domains - ) external onlyProposer onlyOptimisticMode checkDomains(_domains) { + ) external onlyProposer onlyOptimisticMode checkDomains(_domains) whenNotPaused { if (_snapshotId != SnapshotId.getLastCompletedSnapshotId()) revert RootManager_proposeAggregateRoot__InvalidSnapshotId(_snapshotId); if (proposedAggregateRootHash != FINALIZED_HASH) revert RootManager_proposeAggregateRoot__ProposeInProgress(); @@ -443,17 +487,21 @@ contract RootManager is ProposedOwnable, IRootManager, WatcherClient, DomainInde * @param _proposedAggregateRoot The aggregate root currently proposed * @param _endOfDispute The block in which the dispute period for proposedAggregateRootHash finalizes */ - function finalize(bytes32 _proposedAggregateRoot, uint256 _endOfDispute) public onlyOptimisticMode { + function finalize(bytes32 _proposedAggregateRoot, uint256 _endOfDispute) public onlyOptimisticMode whenNotPaused { bytes32 _proposedAggregateRootHash = proposedAggregateRootHash; if (_proposedAggregateRootHash == FINALIZED_HASH) revert RootManager_finalize__InvalidAggregateRoot(); bytes32 _userInputHash = keccak256(abi.encode(_proposedAggregateRoot, _endOfDispute)); if (_userInputHash != _proposedAggregateRootHash) revert RootManager_finalize__InvalidInputHash(); if (_endOfDispute > block.number) revert RootManager_finalize__ProposeInProgress(); - finalizedOptimisticAggregateRoot = _proposedAggregateRoot; + // Save data + validAggregateRoots[block.timestamp] = _proposedAggregateRoot; + lastSavedAggregateRootTimestamp = block.timestamp; + + // Clear the propose slot proposedAggregateRootHash = FINALIZED_HASH; - emit ProposedRootFinalized(_proposedAggregateRoot); + emit AggregateRootSavedOptimistic(_proposedAggregateRoot, block.timestamp); } /** @@ -473,7 +521,7 @@ contract RootManager is ProposedOwnable, IRootManager, WatcherClient, DomainInde bytes[] memory _encodedData, bytes32 _proposedAggregateRoot, uint256 _endOfDispute - ) external payable whenNotPaused { + ) external payable { finalize(_proposedAggregateRoot, _endOfDispute); propagate(_connectors, _fees, _encodedData); } @@ -482,7 +530,7 @@ contract RootManager is ProposedOwnable, IRootManager, WatcherClient, DomainInde * @notice This is called by relayers to take the current aggregate tree root and propagate it to all * spoke domains (via their respective hub connectors). * @dev Should be called by relayers at a regular interval. - * Workflow is different depending on the mode the system is in. + * Workflow is slightly different depending on the mode the system is in. * * @param _connectors Array of connectors: should match exactly the array of `connectors` in storage; * used here to reduce gas costs, and keep them static regardless of number of supported domains. @@ -500,65 +548,16 @@ contract RootManager is ProposedOwnable, IRootManager, WatcherClient, DomainInde // Sanity check: fees and encodedData lengths matches connectors length. require(_fees.length == _numDomains && _encodedData.length == _numDomains, "invalid lengths"); - optimisticMode - ? _optimisticPropagate(_connectors, _fees, _encodedData) - : _slowPropagate(_connectors, _fees, _encodedData); - } + // If in slow mode, we dequeue to ensure that we add the inboundRoots that are ready. + if (!optimisticMode) dequeue(); - /** - * @notice Function used to propagate the aggregate root when the system is in optimistic mode. - * @dev The root used is the already finalized optimistic root, this function does not use the MERKLE tree nor the - * pendingInboundRoots queue. - * Will set finalizedOptimisticAggregateRoot to FINALIZED_HASH. - * Emits a unique event. - * CRITICAL: This function does NOT check if _connectors sent to it are correct or not. - * Can always be called internally, since it doesn't check if the system is in optimistic mode or not. - * All the needed checks must be done before calling this function. - * - * @param _connectors Array of connectors: should match exactly the array of `connectors` in storage; - * used here to reduce gas costs, and keep them static regardless of number of supported domains. - * @param _fees Array of fees in native token for an AMB if required - * @param _encodedData Array of encodedData: extra params for each AMB if required - */ - function _optimisticPropagate( - address[] calldata _connectors, - uint256[] calldata _fees, - bytes[] memory _encodedData - ) internal { - bytes32 _aggregateRoot = finalizedOptimisticAggregateRoot; - if (_aggregateRoot == FINALIZED_HASH) revert RootManager_optimisticPropagate__ForbiddenOptimisticRoot(); + bytes32 _aggregateRoot = validAggregateRoots[lastSavedAggregateRootTimestamp]; - finalizedOptimisticAggregateRoot = FINALIZED_HASH; + if (_aggregateRoot == 0) revert RootManager_propagate__AggregateRootIsZero(); - _sendRootToHubs(_aggregateRoot, _connectors, _fees, _encodedData); - emit OptimisticRootPropagated(_aggregateRoot, domainsHash); - } + emit AggregateRootPropagated(_aggregateRoot, domainsHash); - /** - * @notice Function used to propagate the aggregate root when the system is in slow mode. - * @dev Will dequeue the elements that are ready on pendingInboundRoots queue and insert them in MERKLE tree to generate - * the new aggregate root that needs to be propagated. - * Will set the finalizedOptimisticAggregateRoot to FINALIZE_HASH in order to discard an old root with old inboundRoots. - * Emits a unique event. - * CRITICAL: This function does NOT check if _connectors sent to it are correct or not. - * Can always be called internally, since it doesn't check if the system is in slow mode or not. - * All the needed checks must be done before calling this function. - * - * @param _connectors Array of connectors: should match exactly the array of `connectors` in storage; - * used here to reduce gas costs, and keep them static regardless of number of supported domains. - * @param _fees Array of fees in native token for an AMB if required - * @param _encodedData Array of encodedData: extra params for each AMB if required - */ - function _slowPropagate( - address[] calldata _connectors, - uint256[] calldata _fees, - bytes[] memory _encodedData - ) internal { - finalizedOptimisticAggregateRoot = FINALIZED_HASH; - (bytes32 _aggregateRoot, uint256 _currentCount) = dequeue(); - if (_currentCount <= lastCountBeforeOpMode) revert RootManager_slowPropagate__OldAggregateRoot(); _sendRootToHubs(_aggregateRoot, _connectors, _fees, _encodedData); - emit RootPropagated(_aggregateRoot, _currentCount, domainsHash); } /** @@ -623,6 +622,16 @@ contract RootManager is ProposedOwnable, IRootManager, WatcherClient, DomainInde } } + /** + * @notice Sends the latest valid aggregate root to the hub domain's spoke connector. + * @dev This has no guards as the guards should be in the spoke connector. For example, the spoke connector should + * guard against receiving the root through this function if the spoke connector is not in optimistic mode. + */ + function sendRootToHubSpoke() external whenNotPaused { + bytes32 _aggregateRoot = validAggregateRoots[lastSavedAggregateRootTimestamp]; + IHubSpokeConnector(getConnectorForDomain(hubDomain)).saveAggregateRoot(_aggregateRoot); + } + /** * @notice Accept an inbound root coming from a given domain's hub connector, enqueuing this incoming * root into the current queue as it awaits the verification period. @@ -643,7 +652,6 @@ contract RootManager is ProposedOwnable, IRootManager, WatcherClient, DomainInde * @notice Dequeue verified inbound roots and insert them into the aggregator tree. * @dev Will dequeue a fixed maximum amount of roots to prevent out of gas errors. As such, this * method is public and separate from `propagate` so we can curtail an overloaded queue as needed. - * @dev Reverts if no verified inbound roots are found. * * @return bytes32 The new aggregate root. * @return uint256 The updated count (number of leaves). @@ -661,7 +669,10 @@ contract RootManager is ProposedOwnable, IRootManager, WatcherClient, DomainInde // aggregate root and count). (bytes32 _aggregateRoot, uint256 _count) = MERKLE.insert(_verifiedInboundRoots); - emit RootsAggregated(_aggregateRoot, _count, _verifiedInboundRoots); + validAggregateRoots[block.timestamp] = _aggregateRoot; + lastSavedAggregateRootTimestamp = block.timestamp; + + emit AggregateRootSavedSlow(_aggregateRoot, _count, _verifiedInboundRoots, block.timestamp); return (_aggregateRoot, _count); } @@ -688,7 +699,6 @@ contract RootManager is ProposedOwnable, IRootManager, WatcherClient, DomainInde if (optimisticMode) revert RootManager_activateOptimisticMode__OptimisticModeOn(); pendingInboundRoots.last = pendingInboundRoots.first - 1; - lastCountBeforeOpMode = MERKLE.count(); optimisticMode = true; emit OptimisticModeActivated(); diff --git a/packages/deployments/contracts/contracts/messaging/connectors/SpokeConnector.sol b/packages/deployments/contracts/contracts/messaging/connectors/SpokeConnector.sol index 0a07f9cbd3..ff38e3a89a 100644 --- a/packages/deployments/contracts/contracts/messaging/connectors/SpokeConnector.sol +++ b/packages/deployments/contracts/contracts/messaging/connectors/SpokeConnector.sol @@ -50,6 +50,18 @@ abstract contract SpokeConnector is Connector, ConnectorManager, WatcherClient, */ event SenderRemoved(address indexed sender); + /** + * @notice Emitted when a new proposer is added + * @param proposer The address of the proposer + */ + event ProposerAdded(address indexed proposer); + + /** + * @notice Emitted when a proposer is removed + * @param proposer The address of the proposer + */ + event ProposerRemoved(address indexed proposer); + /** * @notice Emitted when a new aggregate root is delivered from the hub * @param root Delivered root @@ -104,6 +116,72 @@ abstract contract SpokeConnector is Connector, ConnectorManager, WatcherClient, */ event MessageProven(bytes32 indexed leaf, bytes32 indexed aggregateRoot, uint256 aggregateIndex); + /** + * @notice Emitted when slow mode is activated + * @param watcher The address of the watcher who called the function + */ + event SlowModeActivated(address indexed watcher); + + /** + * @notice Emitted when optimistic mode is activated + */ + event OptimisticModeActivated(); + + /** + * @notice Emitted when a new aggregate root is proposed + * @param aggregateRoot The new aggregate root proposed + * @param endOfDispute The block at which this root can't be disputed anymore and therefore it's deemed valid. + * @param rootTimestamp The timestamp at which the root was finalized in the root manager contract. + * @param domain The domain where this root was proposed. + */ + event AggregateRootProposed( + bytes32 indexed aggregateRoot, + uint256 indexed rootTimestamp, + uint256 indexed endOfDispute, + uint32 domain + ); + + /** + * @notice Emitted when a pending aggregate root is deleted from the pendingAggregateRoots mapping + * @param aggregateRoot The deleted aggregate root + */ + event PendingAggregateRootDeleted(bytes32 indexed aggregateRoot); + + /** + * @notice Emitted when the current proposed root is finalized + * @param aggregateRoot The aggregate root finalized + */ + event ProposedRootFinalized(bytes32 aggregateRoot); + + /** + * @notice Emitted when the number of dispute blocks is updated + * @param previous The previous number of blocks off-chain agents had to dispute a proposed root + * @param updated The new number of blocks off-chain agents have to dispute a proposed root + */ + event DisputeBlocksUpdated(uint256 previous, uint256 updated); + + /** + * @notice Emitted whem the number of minimum dispute blocks is updated + * @param previous The previous minimum number of dispute blocks to set + * @param updated The new minimum number of dispute blocks to set + */ + event MinDisputeBlocksUpdated(uint256 previous, uint256 updated); + + // ============ Errors ============ + + error SpokeConnector_onlyOptimisticMode__SlowModeOn(); + error SpokeConnector_activateOptimisticMode__OptimisticModeOn(); + error SpokeConnector_onlyProposer__NotAllowlistedProposer(); + error SpokeConnector_proposeAggregateRoot__ProposeInProgress(); + error SpokeConnector_finalize__ProposeInProgress(); + error SpokeConnector_finalize__InvalidInputHash(); + error SpokeConnector_finalize__ProposedHashIsFinalizedHash(); + error SpokeConnector_setMinDisputeBlocks__SameMinDisputeBlocksAsBefore(); + error SpokeConnector_setDisputeBlocks__DisputeBlocksLowerThanMin(); + error SpokeConnector_setDisputeBlocks__SameDisputeBlocksAsBefore(); + error SpokeConnector_receiveAggregateRoot__OptimisticModeOn(); + error SpokeConnector_constructor__DisputeBlocksLowerThanMin(); + // ============ Structs ============ /** @@ -118,6 +196,37 @@ abstract contract SpokeConnector is Connector, ConnectorManager, WatcherClient, uint256 index; } + /** + * Struct containing the base construstor arguments of a SpokeConnector + * @param domain The domain this connector lives on. + * @param mirrorDomain The hub domain. + * @param amb The address of the AMB on the spoke domain this connector lives on. + * @param rootManager The address of the RootManager on the hub. + * @param mirrorConnector The address of the spoke connector. + * @param processGas The gas costs used in `handle` to ensure meaningful state changes can occur (minimum gas needed + * to handle transaction). + * @param reserveGas The gas costs reserved when `handle` is called to ensure failures are handled. + * @param delayBlocks The delay for the validation period for incoming messages in blocks. + * @param merkle The address of the MerkleTreeManager on this spoke domain. + * @param watcherManager The address of the WatcherManager to whom this connector is a client. + * @param minDisputeBlocks The minimum number of dispute blocks that can be set. + * @param disputeBlocks The number of dispute blocks off-chain agents will have to dispute proposed roots. + */ + struct ConstructorParams { + uint32 domain; + uint32 mirrorDomain; + address amb; + address rootManager; + address mirrorConnector; + uint256 processGas; + uint256 reserveGas; + uint256 delayBlocks; + address merkle; + address watcherManager; + uint256 minDisputeBlocks; + uint256 disputeBlocks; + } + // ============ Public Storage ============ /** @@ -187,6 +296,40 @@ abstract contract SpokeConnector is Connector, ConnectorManager, WatcherClient, */ mapping(uint256 => bytes32) public snapshotRoots; + /** + * @notice The resulting hash of keccaking the proposed aggregate root, the timestamp at which it was finalized in the root manager + * and the block at which the time to dispute it ends. + * @dev Set to 0x1 to prevent someone from calling finalize() the moment the contract is deployed. + */ + bytes32 public proposedAggregateRootHash = 0x0000000000000000000000000000000000000000000000000000000000000001; + + /* + @notice The number of blocks off-chain agents have to dispute a given proposal. + */ + uint256 public disputeBlocks; + + /** + * @notice The minimum number of blocks disputeBlocks can be set to. + */ + uint256 public minDisputeBlocks; + + /** + * @notice Hash used to keep the proposal slot warm once a given proposal has been finalized. + * @dev It also represents the empty state. This means if a proposal holds this hash, it's deemed empty. + */ + bytes32 public constant FINALIZED_HASH = 0x0000000000000000000000000000000000000000000000000000000000000001; + + /** + * @notice True if the system is working in optimistic mode. Otherwise is working in slow mode + */ + bool public optimisticMode; + + /** + * @notice This is used for the `onlyProposers` modifier, which gates who + * can propose new roots using `proposeAggregateRoot`. + */ + mapping(address => bool) public allowlistedProposers; + // ============ Modifiers ============ /** @@ -197,48 +340,50 @@ abstract contract SpokeConnector is Connector, ConnectorManager, WatcherClient, _; } + /** + * @notice Ensures the msg.sender is an allowlisted proposer + */ + modifier onlyAllowlistedProposer() { + if (!allowlistedProposers[msg.sender]) revert SpokeConnector_onlyProposer__NotAllowlistedProposer(); + _; + } + + /** + * @notice Checks if this spoke connector is working in optimistic mode + */ + modifier onlyOptimisticMode() { + if (!optimisticMode) revert SpokeConnector_onlyOptimisticMode__SlowModeOn(); + _; + } + // ============ Constructor ============ /** * @notice Creates a new SpokeConnector instance. - * @param _domain The domain this connector lives on. - * @param _mirrorDomain The hub domain. - * @param _amb The address of the AMB on the spoke domain this connector lives on. - * @param _rootManager The address of the RootManager on the hub. - * @param _mirrorConnector The address of the spoke connector. - * @param _processGas The gas costs used in `handle` to ensure meaningful state changes can occur (minimum gas needed - * to handle transaction). - * @param _reserveGas The gas costs reserved when `handle` is called to ensure failures are handled. - * @param _delayBlocks The delay for the validation period for incoming messages in blocks. - * @param _merkle The address of the MerkleTreeManager on this spoke domain. - * @param _watcherManager The address of the WatcherManager to whom this connector is a client. + * @param _params The constructor parameters. */ constructor( - uint32 _domain, - uint32 _mirrorDomain, - address _amb, - address _rootManager, - address _mirrorConnector, - uint256 _processGas, - uint256 _reserveGas, - uint256 _delayBlocks, - address _merkle, - address _watcherManager + ConstructorParams memory _params ) ConnectorManager() - Connector(_domain, _mirrorDomain, _amb, _rootManager, _mirrorConnector) - WatcherClient(_watcherManager) + Connector(_params.domain, _params.mirrorDomain, _params.amb, _params.rootManager, _params.mirrorConnector) + WatcherClient(_params.watcherManager) { + uint256 _disputeBlocks = _params.disputeBlocks; + uint256 _minDisputeBlocks = _params.minDisputeBlocks; + if (_disputeBlocks < _minDisputeBlocks) revert SpokeConnector_constructor__DisputeBlocksLowerThanMin(); // Sanity check: constants are reasonable. - require(_processGas > 850_000 - 1, "!process gas"); - require(_reserveGas > 15_000 - 1, "!reserve gas"); - PROCESS_GAS = _processGas; - RESERVE_GAS = _reserveGas; + require(_params.processGas > 850_000 - 1, "!process gas"); + require(_params.reserveGas > 15_000 - 1, "!reserve gas"); + PROCESS_GAS = _params.processGas; + RESERVE_GAS = _params.reserveGas; - require(_merkle != address(0), "!zero merkle"); - MERKLE = MerkleTreeManager(_merkle); + require(_params.merkle != address(0), "!zero merkle"); + MERKLE = MerkleTreeManager(_params.merkle); - delayBlocks = _delayBlocks; + delayBlocks = _params.delayBlocks; + minDisputeBlocks = _minDisputeBlocks; + disputeBlocks = _disputeBlocks; } // ============ Admin Functions ============ @@ -248,7 +393,7 @@ abstract contract SpokeConnector is Connector, ConnectorManager, WatcherClient, * @dev Only allowlisted routers (senders) can call `dispatch`. * @param _sender Sender to whitelist */ - function addSender(address _sender) public onlyOwner { + function addSender(address _sender) external onlyOwner { require(!allowlistedSenders[_sender], "allowed"); allowlistedSenders[_sender] = true; emit SenderAdded(_sender); @@ -259,18 +404,57 @@ abstract contract SpokeConnector is Connector, ConnectorManager, WatcherClient, * @dev Only allowlisted routers (senders) can call `dispatch`. * @param _sender Sender to remove from whitelist */ - function removeSender(address _sender) public onlyOwner { + function removeSender(address _sender) external onlyOwner { require(allowlistedSenders[_sender], "!allowed"); delete allowlistedSenders[_sender]; emit SenderRemoved(_sender); } + /** + * @notice Adds a proposer to the allowlist. + * @dev Only allowlisted proposers can call `proposeAggregateRoot`. + */ + function addProposer(address _proposer) external onlyOwner { + allowlistedProposers[_proposer] = true; + emit ProposerAdded(_proposer); + } + + /** + * @notice Removes a proposer from the allowlist. + * @dev Only allowlisted proposers can call `proposeAggregateRoot`. + */ + function removeProposer(address _proposer) external onlyOwner { + delete allowlistedProposers[_proposer]; + emit ProposerRemoved(_proposer); + } + + /** + * @notice Set the `minDisputeBlocks` variable to the provided parameter. + */ + function setMinDisputeBlocks(uint256 _minDisputeBlocks) external onlyOwner { + if (_minDisputeBlocks == minDisputeBlocks) + revert SpokeConnector_setMinDisputeBlocks__SameMinDisputeBlocksAsBefore(); + emit MinDisputeBlocksUpdated(minDisputeBlocks, _minDisputeBlocks); + minDisputeBlocks = _minDisputeBlocks; + } + + /** + * @notice Set the `disputeBlocks`, the duration, in blocks, of the dispute process for + * a given proposed root + */ + function setDisputeBlocks(uint256 _disputeBlocks) external onlyOwner { + if (_disputeBlocks < minDisputeBlocks) revert SpokeConnector_setDisputeBlocks__DisputeBlocksLowerThanMin(); + if (_disputeBlocks == disputeBlocks) revert SpokeConnector_setDisputeBlocks__SameDisputeBlocksAsBefore(); + emit DisputeBlocksUpdated(disputeBlocks, _disputeBlocks); + disputeBlocks = _disputeBlocks; + } + /** * @notice Set the `delayBlocks`, the period in blocks over which an incoming message * is verified. * @param _delayBlocks Updated delay block value */ - function setDelayBlocks(uint256 _delayBlocks) public onlyOwner { + function setDelayBlocks(uint256 _delayBlocks) external onlyOwner { require(_delayBlocks != delayBlocks, "!delayBlocks"); emit DelayBlocksUpdated(_delayBlocks, msg.sender); delayBlocks = _delayBlocks; @@ -283,7 +467,7 @@ abstract contract SpokeConnector is Connector, ConnectorManager, WatcherClient, * @param _rateLimit The number of blocks require between sending messages. If set to * 0, rate limiting for this spoke connector will be disabled. */ - function setRateLimitBlocks(uint256 _rateLimit) public onlyOwner { + function setRateLimitBlocks(uint256 _rateLimit) external onlyOwner { _setRateLimitBlocks(_rateLimit); } @@ -296,7 +480,7 @@ abstract contract SpokeConnector is Connector, ConnectorManager, WatcherClient, * @param _fraudulentRoot Target fraudulent root that should be erased from the * `pendingAggregateRoots` mapping. */ - function removePendingAggregateRoot(bytes32 _fraudulentRoot) public onlyOwner whenPaused { + function removePendingAggregateRoot(bytes32 _fraudulentRoot) external onlyOwner whenPaused { // Sanity check: pending aggregate root exists. require(pendingAggregateRoots[_fraudulentRoot] != 0, "aggregateRoot !exists"); delete pendingAggregateRoots[_fraudulentRoot]; @@ -313,6 +497,25 @@ abstract contract SpokeConnector is Connector, ConnectorManager, WatcherClient, require(false, "prohibited"); } + /** + * @notice Watcher can set the system in slow mode. + * @dev Sets the proposed aggregate root hash to FINALIZED_HASH, invalidating it. + */ + function activateSlowMode() external onlyWatcher onlyOptimisticMode { + optimisticMode = false; + proposedAggregateRootHash = FINALIZED_HASH; + emit SlowModeActivated(msg.sender); + } + + /** + * @notice Owner can set the system to optimistic mode. + */ + function activateOptimisticMode() external onlyOwner { + if (optimisticMode) revert SpokeConnector_activateOptimisticMode__OptimisticModeOn(); + optimisticMode = true; + emit OptimisticModeActivated(); + } + // ============ Public Functions ============ /** @@ -400,6 +603,56 @@ abstract contract SpokeConnector is Connector, ConnectorManager, WatcherClient, return (_messageHash, _message); } + /** + * @notice Propose a new aggregate root + * @dev _rootTimestamp is required for off-chain agents to be able to know which root they should fetch from the root manager contract + * in order to compare it with the one being proposed. The off-chain agents should also ensure the proposed root is + * not an old one. + * @param _aggregateRoot The aggregate root to propose. + * @param _rootTimestamp Block.timestamp at which the root was finalized in the root manager contract. + */ + function proposeAggregateRoot( + bytes32 _aggregateRoot, + uint256 _rootTimestamp + ) external virtual whenNotPaused onlyAllowlistedProposer onlyOptimisticMode { + if (proposedAggregateRootHash != FINALIZED_HASH) revert SpokeConnector_proposeAggregateRoot__ProposeInProgress(); + if (pendingAggregateRoots[_aggregateRoot] != 0) { + delete pendingAggregateRoots[_aggregateRoot]; + emit PendingAggregateRootDeleted(_aggregateRoot); + } + + uint256 _endOfDispute = block.number + disputeBlocks; + proposedAggregateRootHash = keccak256(abi.encode(_aggregateRoot, _rootTimestamp, _endOfDispute)); + + emit AggregateRootProposed(_aggregateRoot, _rootTimestamp, _endOfDispute, DOMAIN); + } + + /** + * @notice Finalizes the proposed aggregate root. This confirms the root validity. Therefore, it can be proved and processed. + * @dev Finalized roots won't be monitored by off-chain agents as they are deemed valid. + * + * @param _proposedAggregateRoot The aggregate root currently proposed + * @param _endOfDispute The block in which the dispute period for proposedAggregateRootHash concludes + */ + function finalize( + bytes32 _proposedAggregateRoot, + uint256 _rootTimestamp, + uint256 _endOfDispute + ) external virtual whenNotPaused onlyOptimisticMode { + if (_endOfDispute > block.number) revert SpokeConnector_finalize__ProposeInProgress(); + + bytes32 _proposedAggregateRootHash = proposedAggregateRootHash; + if (_proposedAggregateRootHash == FINALIZED_HASH) revert SpokeConnector_finalize__ProposedHashIsFinalizedHash(); + + bytes32 _userInputHash = keccak256(abi.encode(_proposedAggregateRoot, _rootTimestamp, _endOfDispute)); + if (_userInputHash != _proposedAggregateRootHash) revert SpokeConnector_finalize__InvalidInputHash(); + + provenAggregateRoots[_proposedAggregateRoot] = true; + proposedAggregateRootHash = FINALIZED_HASH; + + emit ProposedRootFinalized(_proposedAggregateRoot); + } + /** * @notice Must be able to call the `handle` function on the BridgeRouter contract. This is called * on the destination domain to handle incoming messages. @@ -502,6 +755,7 @@ abstract contract SpokeConnector is Connector, ConnectorManager, WatcherClient, * @param _newRoot Received aggregate */ function receiveAggregateRoot(bytes32 _newRoot) internal { + if (optimisticMode) revert SpokeConnector_receiveAggregateRoot__OptimisticModeOn(); require(_newRoot != bytes32(""), "new root empty"); require(pendingAggregateRoots[_newRoot] == 0, "root already pending"); require(!provenAggregateRoots[_newRoot], "root already proven"); diff --git a/packages/deployments/contracts/contracts/messaging/connectors/arbitrum/ArbitrumSpokeConnector.sol b/packages/deployments/contracts/contracts/messaging/connectors/arbitrum/ArbitrumSpokeConnector.sol index b7259734ae..add262ff15 100644 --- a/packages/deployments/contracts/contracts/messaging/connectors/arbitrum/ArbitrumSpokeConnector.sol +++ b/packages/deployments/contracts/contracts/messaging/connectors/arbitrum/ArbitrumSpokeConnector.sol @@ -35,32 +35,8 @@ contract ArbitrumSpokeConnector is SpokeConnector { // ============ Constructor ============ - constructor( - uint32 _domain, - uint32 _mirrorDomain, - address _amb, - address _rootManager, - address _mirrorConnector, - uint256 _processGas, - uint256 _reserveGas, - uint256 _delayBlocks, - address _merkle, - address _watcherManager - ) - SpokeConnector( - _domain, - _mirrorDomain, - _amb, - _rootManager, - _mirrorConnector, - _processGas, - _reserveGas, - _delayBlocks, - _merkle, - _watcherManager - ) - { - _setAliasedSender(_mirrorConnector); + constructor(ConstructorParams memory _baseSpokeParams) SpokeConnector(_baseSpokeParams) { + _setAliasedSender(_baseSpokeParams.mirrorConnector); } // ============ Public Functions ============ diff --git a/packages/deployments/contracts/contracts/messaging/connectors/consensys/ConsensysSpokeConnector.sol b/packages/deployments/contracts/contracts/messaging/connectors/consensys/ConsensysSpokeConnector.sol index 312973f589..4f51eec5d9 100644 --- a/packages/deployments/contracts/contracts/messaging/connectors/consensys/ConsensysSpokeConnector.sol +++ b/packages/deployments/contracts/contracts/messaging/connectors/consensys/ConsensysSpokeConnector.sol @@ -9,32 +9,7 @@ import {ConsensysBase} from "./ConsensysBase.sol"; contract ConsensysSpokeConnector is SpokeConnector, ConsensysBase { // ============ Constructor ============ - constructor( - uint32 _domain, - uint32 _mirrorDomain, - address _amb, - address _rootManager, - address _mirrorConnector, - uint256 _processGas, - uint256 _reserveGas, - uint256 _delayBlocks, - address _merkle, - address _watcherManager - ) - SpokeConnector( - _domain, - _mirrorDomain, - _amb, - _rootManager, - _mirrorConnector, - _processGas, - _reserveGas, - _delayBlocks, - _merkle, - _watcherManager - ) - ConsensysBase() - {} + constructor(ConstructorParams memory _baseSpokeParams) SpokeConnector(_baseSpokeParams) ConsensysBase() {} // ============ Override Fns ============ function _verifySender(address _expected) internal view override returns (bool) { diff --git a/packages/deployments/contracts/contracts/messaging/connectors/gnosis/GnosisSpokeConnector.sol b/packages/deployments/contracts/contracts/messaging/connectors/gnosis/GnosisSpokeConnector.sol index 9ebec3912c..fbad7081ce 100644 --- a/packages/deployments/contracts/contracts/messaging/connectors/gnosis/GnosisSpokeConnector.sol +++ b/packages/deployments/contracts/contracts/messaging/connectors/gnosis/GnosisSpokeConnector.sol @@ -11,33 +11,10 @@ import {GnosisBase} from "./GnosisBase.sol"; contract GnosisSpokeConnector is SpokeConnector, GnosisBase { // ============ Constructor ============ constructor( - uint32 _domain, - uint32 _mirrorDomain, - address _amb, - address _rootManager, - address _mirrorConnector, - uint256 _processGas, - uint256 _reserveGas, - uint256 _delayBlocks, - address _merkle, - address _watcherManager, + ConstructorParams memory _baseSpokeParams, uint256 _gasCap, // gas to be provided on L1 execution uint256 _mirrorChainId - ) - SpokeConnector( - _domain, - _mirrorDomain, - _amb, - _rootManager, - _mirrorConnector, - _processGas, - _reserveGas, - _delayBlocks, - _merkle, - _watcherManager - ) - GnosisBase(_gasCap, _mirrorChainId) - {} + ) SpokeConnector(_baseSpokeParams) GnosisBase(_gasCap, _mirrorChainId) {} /** * @notice Should not be able to renounce ownership diff --git a/packages/deployments/contracts/contracts/messaging/connectors/mainnet/MainnetSpokeConnector.sol b/packages/deployments/contracts/contracts/messaging/connectors/mainnet/MainnetSpokeConnector.sol index ff90775cda..2255578ad6 100644 --- a/packages/deployments/contracts/contracts/messaging/connectors/mainnet/MainnetSpokeConnector.sol +++ b/packages/deployments/contracts/contracts/messaging/connectors/mainnet/MainnetSpokeConnector.sol @@ -3,36 +3,21 @@ pragma solidity 0.8.17; import {IRootManager} from "../../interfaces/IRootManager.sol"; import {IHubConnector} from "../../interfaces/IHubConnector.sol"; +import {IHubSpokeConnector} from "../../interfaces/IHubSpokeConnector.sol"; import {SpokeConnector} from "../SpokeConnector.sol"; -contract MainnetSpokeConnector is SpokeConnector, IHubConnector { +contract MainnetSpokeConnector is SpokeConnector, IHubConnector, IHubSpokeConnector { + // ============ Errors ============ + error MainnetSpokeConnector_proposeAggregateRoot__DeprecatedInHubDomain(); + error MainnetSpokeConnector_finalize__DeprecatedInHubDomain(); + error MainnetSpokeConnector_saveAggregateRoot__OnlyOptimisticMode(); + error MainnetSpokeConnector_saveAggregateRoot__CallerIsNotRootManager(); + error MainnetSpokeConnector_saveAggregateRoot__RootAlreadyProven(); + error MainnetSpokeConnector_saveAggregateRoot__EmptyRoot(); + // ============ Constructor ============ - constructor( - uint32 _domain, - uint32 _mirrorDomain, - address _amb, - address _rootManager, - address _mirrorConnector, - uint256 _processGas, - uint256 _reserveGas, - uint256 _delayBlocks, - address _merkle, - address _watcherManager - ) - SpokeConnector( - _domain, - _mirrorDomain, - _amb, - _rootManager, - _mirrorConnector, - _processGas, - _reserveGas, - _delayBlocks, - _merkle, - _watcherManager - ) - {} + constructor(ConstructorParams memory _baseSpokeParams) SpokeConnector(_baseSpokeParams) {} // ============ Public fns ============ /** @@ -73,4 +58,46 @@ contract MainnetSpokeConnector is SpokeConnector, IHubConnector { // otherwise is relayer, update the outbound root on the root manager IRootManager(ROOT_MANAGER).aggregate(DOMAIN, bytes32(_data)); } + + /** + * @notice Saves a aggregateRoot after it has been deemed valid by the RootManager. + * @dev This function is used when optimistic mode is on. This function exists only on the hub domain's spoke connector given that + * it resides on the same chain as the RootManager, meaning it can take advantage of the RootManager performing the propose + * and finalize flow and simply recieve the finalized root directly. + * @param _aggregateRoot The aggregateRoot to store as proven. + */ + function saveAggregateRoot(bytes32 _aggregateRoot) external { + if (_aggregateRoot == 0) revert MainnetSpokeConnector_saveAggregateRoot__EmptyRoot(); + if (!optimisticMode) revert MainnetSpokeConnector_saveAggregateRoot__OnlyOptimisticMode(); + if (msg.sender != ROOT_MANAGER) revert MainnetSpokeConnector_saveAggregateRoot__CallerIsNotRootManager(); + if (provenAggregateRoots[_aggregateRoot]) revert MainnetSpokeConnector_saveAggregateRoot__RootAlreadyProven(); + if (pendingAggregateRoots[_aggregateRoot] != 0) { + delete pendingAggregateRoots[_aggregateRoot]; + emit PendingAggregateRootDeleted(_aggregateRoot); + } + + provenAggregateRoots[_aggregateRoot] = true; + emit ProposedRootFinalized(_aggregateRoot); + } + + /** + * @notice Proposes a new aggregate root. + * @dev Reverts in the hub domain as there's no need to propose nor finalize. + * @param _aggregateRoot The aggregate root to propose. + * @param _rootTimestamp Block.timestamp at which the root was finalized in the root manager contract. + */ + function proposeAggregateRoot(bytes32 _aggregateRoot, uint256 _rootTimestamp) external override { + revert MainnetSpokeConnector_proposeAggregateRoot__DeprecatedInHubDomain(); + } + + /** + * @notice Finalizes the proposed aggregate root. This confirms the root validity. Therefore, it can be proved and processed. + * @dev Reverts in the hub domain as there's no need to propose nor finalize. + * + * @param _proposedAggregateRoot The aggregate root currently proposed + * @param _endOfDispute The block in which the dispute period for proposedAggregateRootHash concludes + */ + function finalize(bytes32 _proposedAggregateRoot, uint256 _rootTimestamp, uint256 _endOfDispute) external override { + revert MainnetSpokeConnector_finalize__DeprecatedInHubDomain(); + } } diff --git a/packages/deployments/contracts/contracts/messaging/connectors/multichain/MultichainSpokeConnector.sol b/packages/deployments/contracts/contracts/messaging/connectors/multichain/MultichainSpokeConnector.sol index 816c8cf5b5..d289c766a0 100644 --- a/packages/deployments/contracts/contracts/messaging/connectors/multichain/MultichainSpokeConnector.sol +++ b/packages/deployments/contracts/contracts/messaging/connectors/multichain/MultichainSpokeConnector.sol @@ -9,33 +9,10 @@ import {BaseMultichain} from "./BaseMultichain.sol"; contract MultichainSpokeConnector is SpokeConnector, BaseMultichain { // ============ Constructor ============ constructor( - uint32 _domain, - uint32 _mirrorDomain, - address _amb, - address _rootManager, - address _mirrorConnector, - uint256 _processGas, - uint256 _reserveGas, - uint256 _delayBlocks, - address _merkle, - address _watcherManager, + ConstructorParams memory _baseSpokeParams, uint256 _mirrorChainId, uint256 _gasCap - ) - SpokeConnector( - _domain, - _mirrorDomain, - _amb, - _rootManager, - _mirrorConnector, - _processGas, - _reserveGas, - _delayBlocks, - _merkle, - _watcherManager - ) - BaseMultichain(_amb, _mirrorChainId, _gasCap) - {} + ) SpokeConnector(_baseSpokeParams) BaseMultichain(_baseSpokeParams.amb, _mirrorChainId, _gasCap) {} // ============ Admin fns ============ diff --git a/packages/deployments/contracts/contracts/messaging/connectors/optimism/OptimismSpokeConnector.sol b/packages/deployments/contracts/contracts/messaging/connectors/optimism/OptimismSpokeConnector.sol index 00553e1804..436701de67 100644 --- a/packages/deployments/contracts/contracts/messaging/connectors/optimism/OptimismSpokeConnector.sol +++ b/packages/deployments/contracts/contracts/messaging/connectors/optimism/OptimismSpokeConnector.sol @@ -11,32 +11,9 @@ import {BaseOptimism} from "./BaseOptimism.sol"; contract OptimismSpokeConnector is SpokeConnector, BaseOptimism { // ============ Constructor ============ constructor( - uint32 _domain, - uint32 _mirrorDomain, - address _amb, - address _rootManager, - address _mirrorConnector, - uint256 _processGas, - uint256 _reserveGas, - uint256 _delayBlocks, - address _merkle, - address _watcherManager, + ConstructorParams memory _baseSpokeParams, uint256 _gasCap // gasLimit of message call on L1 - ) - SpokeConnector( - _domain, - _mirrorDomain, - _amb, - _rootManager, - _mirrorConnector, - _processGas, - _reserveGas, - _delayBlocks, - _merkle, - _watcherManager - ) - BaseOptimism(_gasCap) - {} + ) SpokeConnector(_baseSpokeParams) BaseOptimism(_gasCap) {} // ============ Override Fns ============ function _verifySender(address _expected) internal view override returns (bool) { diff --git a/packages/deployments/contracts/contracts/messaging/connectors/polygon/PolygonSpokeConnector.sol b/packages/deployments/contracts/contracts/messaging/connectors/polygon/PolygonSpokeConnector.sol index c87d6d600f..1ffca8cc7c 100644 --- a/packages/deployments/contracts/contracts/messaging/connectors/polygon/PolygonSpokeConnector.sol +++ b/packages/deployments/contracts/contracts/messaging/connectors/polygon/PolygonSpokeConnector.sol @@ -16,31 +16,8 @@ import {SpokeConnector} from "../SpokeConnector.sol"; contract PolygonSpokeConnector is SpokeConnector, FxBaseChildTunnel { // ============ Constructor ============ constructor( - uint32 _domain, - uint32 _mirrorDomain, - address _amb, - address _rootManager, - address _mirrorConnector, - uint256 _processGas, - uint256 _reserveGas, - uint256 _delayBlocks, - address _merkle, - address _watcherManager - ) - SpokeConnector( - _domain, - _mirrorDomain, - _amb, - _rootManager, - _mirrorConnector, - _processGas, - _reserveGas, - _delayBlocks, - _merkle, - _watcherManager - ) - FxBaseChildTunnel(_amb) - {} + ConstructorParams memory _baseSpokeParams + ) SpokeConnector(_baseSpokeParams) FxBaseChildTunnel(_baseSpokeParams.amb) {} // ============ Private fns ============ @@ -57,7 +34,7 @@ contract PolygonSpokeConnector is SpokeConnector, FxBaseChildTunnel { } function _processMessageFromRoot( - uint256, /* stateId */ + uint256 /* stateId */, address sender, bytes memory data ) internal override validateSender(sender) { diff --git a/packages/deployments/contracts/contracts/messaging/connectors/polygonzk/PolygonZkSpokeConnector.sol b/packages/deployments/contracts/contracts/messaging/connectors/polygonzk/PolygonZkSpokeConnector.sol index a8e9b2a913..2ab63beb0e 100644 --- a/packages/deployments/contracts/contracts/messaging/connectors/polygonzk/PolygonZkSpokeConnector.sol +++ b/packages/deployments/contracts/contracts/messaging/connectors/polygonzk/PolygonZkSpokeConnector.sol @@ -9,32 +9,9 @@ import {BasePolygonZk} from "./BasePolygonZk.sol"; contract PolygonZkSpokeConnector is SpokeConnector, BasePolygonZk { // ============ Constructor ============ constructor( - uint32 _domain, - uint32 _mirrorDomain, - address _amb, - address _rootManager, - address _mirrorConnector, - uint256 _processGas, - uint256 _reserveGas, - uint256 _delayBlocks, - address _merkle, - address _watcherManager, + ConstructorParams memory _baseSpokeParams, uint32 _mirrorNetworkId - ) - SpokeConnector( - _domain, - _mirrorDomain, - _amb, - _rootManager, - _mirrorConnector, - _processGas, - _reserveGas, - _delayBlocks, - _merkle, - _watcherManager - ) - BasePolygonZk(_mirrorNetworkId) - {} + ) SpokeConnector(_baseSpokeParams) BasePolygonZk(_mirrorNetworkId) {} // ============ Admin fns ============ diff --git a/packages/deployments/contracts/contracts/messaging/connectors/wormhole/WormholeSpokeConnector.sol b/packages/deployments/contracts/contracts/messaging/connectors/wormhole/WormholeSpokeConnector.sol index 8692182214..40cd81628b 100644 --- a/packages/deployments/contracts/contracts/messaging/connectors/wormhole/WormholeSpokeConnector.sol +++ b/packages/deployments/contracts/contracts/messaging/connectors/wormhole/WormholeSpokeConnector.sol @@ -10,33 +10,10 @@ import {BaseWormhole} from "./BaseWormhole.sol"; contract WormholeSpokeConnector is SpokeConnector, BaseWormhole, IWormholeReceiver { // ============ Constructor ============ constructor( - uint32 _domain, - uint32 _mirrorDomain, - address _amb, - address _rootManager, - address _mirrorConnector, - uint256 _processGas, - uint256 _reserveGas, - uint256 _delayBlocks, - address _merkle, - address _watcherManager, + ConstructorParams memory _baseSpokeParams, uint256 _gasCap, uint16 _mirrorWormholeChainId - ) - SpokeConnector( - _domain, - _mirrorDomain, - _amb, - _rootManager, - _mirrorConnector, - _processGas, - _reserveGas, - _delayBlocks, - _merkle, - _watcherManager - ) - BaseWormhole(_gasCap, _mirrorWormholeChainId) - {} + ) SpokeConnector(_baseSpokeParams) BaseWormhole(_gasCap, _mirrorWormholeChainId) {} // ============ Admin fns ============ diff --git a/packages/deployments/contracts/contracts/messaging/connectors/zksync/ZkSyncSpokeConnector.sol b/packages/deployments/contracts/contracts/messaging/connectors/zksync/ZkSyncSpokeConnector.sol index 8678c25ca4..a97287122c 100644 --- a/packages/deployments/contracts/contracts/messaging/connectors/zksync/ZkSyncSpokeConnector.sol +++ b/packages/deployments/contracts/contracts/messaging/connectors/zksync/ZkSyncSpokeConnector.sol @@ -11,31 +11,7 @@ contract ZkSyncSpokeConnector is SpokeConnector { uint160 constant L1_TO_L2_ALIAS_OFFSET = uint160(0x1111000000000000000000000000000000001111); // ============ Constructor ============ - constructor( - uint32 _domain, - uint32 _mirrorDomain, - address _amb, - address _rootManager, - address _mirrorConnector, - uint256 _processGas, - uint256 _reserveGas, - uint256 _delayBlocks, - address _merkle, - address _watcherManager - ) - SpokeConnector( - _domain, - _mirrorDomain, - _amb, - _rootManager, - _mirrorConnector, - _processGas, - _reserveGas, - _delayBlocks, - _merkle, - _watcherManager - ) - {} + constructor(ConstructorParams memory _baseSpokeParams) SpokeConnector(_baseSpokeParams) {} // ============ Public Functions ============ diff --git a/packages/deployments/contracts/contracts/messaging/interfaces/IHubSpokeConnector.sol b/packages/deployments/contracts/contracts/messaging/interfaces/IHubSpokeConnector.sol new file mode 100644 index 0000000000..7249ffebee --- /dev/null +++ b/packages/deployments/contracts/contracts/messaging/interfaces/IHubSpokeConnector.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity 0.8.17; + +interface IHubSpokeConnector { + function saveAggregateRoot(bytes32 _aggregateRoot) external; +} diff --git a/packages/deployments/contracts/contracts_forge/messaging/PingPong.t.sol b/packages/deployments/contracts/contracts_forge/messaging/PingPong.t.sol index 30813b0efa..762e5f4282 100644 --- a/packages/deployments/contracts/contracts_forge/messaging/PingPong.t.sol +++ b/packages/deployments/contracts/contracts_forge/messaging/PingPong.t.sol @@ -50,8 +50,6 @@ contract PingPong is ConnectorHelper { MerkleTreeManager aggregateTree; uint256 _delayBlocks = 40; - uint256 _minDisputeBlocks = 125; - uint256 _disputeBlocks = 150; address _watcherManager; // ============ connectors @@ -86,21 +84,23 @@ contract PingPong is ConnectorHelper { ); aggregateTree.setArborist(_rootManager); + SpokeConnector.ConstructorParams memory _originParams = SpokeConnector.ConstructorParams({ + domain: _originDomain, + mirrorDomain: _mainnetDomain, + amb: _originAMB, + rootManager: _rootManager, + mirrorConnector: address(0), + processGas: PROCESS_GAS, + reserveGas: RESERVE_GAS, + delayBlocks: 0, + merkle: address(originSpokeTree), + watcherManager: address(1), + minDisputeBlocks: _minDisputeBlocks, + disputeBlocks: _disputeBlocks + }); + // Mock sourceconnector on l2 - _originConnectors.spoke = address( - new MockSpokeConnector( - _originDomain, // uint32 _domain, - _mainnetDomain, // uint32 _mirrorDomain - _originAMB, // address _amb, - _rootManager, // address _rootManager, - address(originSpokeTree), // address merkle root manager - address(0), // address _mirrorConnector - PROCESS_GAS, // uint256 _processGas, - RESERVE_GAS, // uint256 _reserveGas - 0, // uint256 _delayBlocks - _watcherManager - ) - ); + _originConnectors.spoke = address(new MockSpokeConnector(_originParams)); originSpokeTree.setArborist(_originConnectors.spoke); MockSpokeConnector(payable(_originConnectors.spoke)).setUpdatesAggregate(true); @@ -116,21 +116,23 @@ contract PingPong is ConnectorHelper { ); MockHubConnector(payable(_originConnectors.hub)).setUpdatesAggregate(true); + SpokeConnector.ConstructorParams memory _destinationParams = SpokeConnector.ConstructorParams({ + domain: _destinationDomain, + mirrorDomain: _mainnetDomain, + amb: _destinationAMB, + rootManager: _rootManager, + mirrorConnector: address(0), + processGas: PROCESS_GAS, + reserveGas: RESERVE_GAS, + delayBlocks: 0, + merkle: address(destinationSpokeTree), + watcherManager: address(1), + minDisputeBlocks: _minDisputeBlocks, + disputeBlocks: _disputeBlocks + }); + // Mock dest connector on l2 - _destinationConnectors.spoke = address( - new MockSpokeConnector( - _destinationDomain, // uint32 _domain, - _mainnetDomain, // uint32 _mirrorDomain, - _destinationAMB, // address _amb, - _rootManager, // address _rootManager, - address(destinationSpokeTree), // address merkle root manager - address(0), // address _mirrorConnector, - PROCESS_GAS, // uint256 _processGas, - RESERVE_GAS, // uint256 _reserveGas - 0, // uint256 _delayBlocks - _watcherManager - ) - ); + _destinationConnectors.spoke = address(new MockSpokeConnector(_destinationParams)); destinationSpokeTree.setArborist(_destinationConnectors.spoke); MockSpokeConnector(payable(_destinationConnectors.spoke)).setUpdatesAggregate(true); diff --git a/packages/deployments/contracts/contracts_forge/messaging/ProposeFinalizePropagate.t.sol b/packages/deployments/contracts/contracts_forge/messaging/ProposeFinalizePropagate.t.sol index fb1103e646..0ccb5410ea 100644 --- a/packages/deployments/contracts/contracts_forge/messaging/ProposeFinalizePropagate.t.sol +++ b/packages/deployments/contracts/contracts_forge/messaging/ProposeFinalizePropagate.t.sol @@ -58,58 +58,6 @@ contract ProposeFinalizePropagate is ForgeHelper { aggregateRoot = bytes32(abi.encode(5)); } - function test_RootManager__RevertIfPropagateOldData() external { - // Start in slowMode - vm.mockCall( - watcherManager, - abi.encodeWithSelector(WatcherManager(watcherManager).isWatcher.selector), - abi.encode(true) - ); - vm.prank(watcher); - rootManager.activateSlowMode(); - - // Checks that we are in slow mode - isSlow(); - - // Aggregate elements to the queue - vm.prank(connectors[0]); - rootManager.aggregate(domains[0], bytes32("test0")); - vm.prank(connectors[1]); - rootManager.aggregate(domains[1], bytes32("test1")); - - // Count has to be 2 - assertEq(rootManager.getPendingInboundRootsCount(), 2); - - // Switch again to optimistic mode - vm.prank(owner); - rootManager.activateOptimisticMode(); - - // Count has to be 0 - assertEq(rootManager.getPendingInboundRootsCount(), 0); - - // Warp time - vm.warp(block.timestamp + 2 hours); - - // Checks - isOptimistic(); - - // Again slow mode - vm.prank(watcher); - rootManager.activateSlowMode(); - - // Checks that we are in slow mode - isSlow(); - - // Should revert because no new messages have arrived - vm.expectRevert(abi.encodeWithSelector(RootManager.RootManager_slowPropagate__OldAggregateRoot.selector)); - - // Propagate - rootManager.propagate(connectors, fees, encodedData); - - // Count has to be 0 - assertEq(rootManager.getPendingInboundRootsCount(), 0); - } - function test_RootManager__QueuedSwitchAndPropagate() external { // Start in slowMode vm.mockCall( @@ -180,13 +128,13 @@ contract ProposeFinalizePropagate is ForgeHelper { } function test_RootManager__QueuedDequeueQueuedSwitch() external { - // Start in slowMode vm.mockCall( watcherManager, abi.encodeWithSelector(WatcherManager(watcherManager).isWatcher.selector), abi.encode(true) ); vm.prank(watcher); + // Set in slowMode rootManager.activateSlowMode(); // Checks that we are in slow mode @@ -211,9 +159,6 @@ contract ProposeFinalizePropagate is ForgeHelper { vm.prank(owner); rootManager.activateOptimisticMode(); - // Last count should be equals to count - assertEq(_countBefore, rootManager.lastCountBeforeOpMode()); - // Checks isOptimistic(); diff --git a/packages/deployments/contracts/contracts_forge/messaging/RootManager.t.sol b/packages/deployments/contracts/contracts_forge/messaging/RootManager.t.sol index 5cc969590f..4f99da0900 100644 --- a/packages/deployments/contracts/contracts_forge/messaging/RootManager.t.sol +++ b/packages/deployments/contracts/contracts_forge/messaging/RootManager.t.sol @@ -10,6 +10,7 @@ import {IHubConnector} from "../../contracts/messaging/interfaces/IHubConnector. import {MerkleTreeManager} from "../../contracts/messaging/MerkleTreeManager.sol"; import {WatcherManager} from "../../contracts/messaging/WatcherManager.sol"; import {SnapshotId} from "../../contracts/messaging/libraries/SnapshotId.sol"; +import {IHubSpokeConnector} from "../../contracts/messaging/interfaces/IHubSpokeConnector.sol"; import "../utils/ConnectorHelper.sol"; @@ -49,10 +50,6 @@ contract RootManagerForTest is DomainIndexer, RootManager { uint256 _disputeBlocks ) RootManager(_delayBlocks, _merkle, _watcherManager, _minDisputeBlocks, _disputeBlocks) {} - function forTest_setLastCountBeforeOpMode(uint256 _lastCountBeforeOpMode) public { - lastCountBeforeOpMode = _lastCountBeforeOpMode; - } - function forTest_setProposer(address _proposer, bool _isProposer) public { allowlistedProposers[_proposer] = _isProposer; } @@ -79,24 +76,12 @@ contract RootManagerForTest is DomainIndexer, RootManager { proposedAggregateRootHash = FINALIZED_HASH; } - function forTest_setFinalizedOptimisticRoot(bytes32 _aggregateRoot) public { - finalizedOptimisticAggregateRoot = _aggregateRoot; - } - - function forTest_optimisticPropagate( - address[] calldata _connectors, - uint256[] calldata _fees, - bytes[] memory _encodedData - ) public { - _optimisticPropagate(_connectors, _fees, _encodedData); + function forTest_setValidAggregateRoot(bytes32 _aggregateRoot, uint256 _timestamp) public { + validAggregateRoots[_timestamp] = _aggregateRoot; } - function forTest_slowPropagate( - address[] calldata _connectors, - uint256[] calldata _fees, - bytes[] memory _encodedData - ) public { - _slowPropagate(_connectors, _fees, _encodedData); + function forTest_setLastSavedAggregateRootTimestamp(uint256 _timestamp) public { + lastSavedAggregateRootTimestamp = _timestamp; } function forTest_sendRootToHubs( @@ -119,6 +104,10 @@ contract RootManagerForTest is DomainIndexer, RootManager { function forTest_pause() public { _pause(); } + + function forTest_setHubDomain(uint32 _domain) public { + hubDomain = _domain; + } } contract Base is ForgeHelper { @@ -128,8 +117,6 @@ contract Base is ForgeHelper { // ============ Events ============ event RootReceived(uint32 domain, bytes32 receivedRoot, uint256 queueIndex); - event RootsAggregated(bytes32 aggregateRoot, uint256 count, bytes32[] aggregatedMessageRoots); - event RootPropagated(bytes32 aggregate, uint32[] domains, uint256 count); event ConnectorAdded(uint32 domain, address connector, uint32[] domains, address[] connectors); @@ -138,6 +125,21 @@ contract Base is ForgeHelper { event PropagateFailed(uint32 domain, address connector); + event AggregateRootSavedSlow( + bytes32 aggregateRoot, + uint256 leafCount, + bytes32[] aggregatedRoots, + uint256 rootTimestamp + ); + + event AggregateRootSavedOptimistic(bytes32 aggregateRoot, uint256 rootTimestamp); + + event AggregateRootPropagated(bytes32 indexed aggregateRoot, bytes32 domainsHash); + + event HubDomainSet(uint32 _domain); + + event HubDomainCleared(); + // ============ Storage ============ RootManagerForTest _rootManager; uint256 _delayBlocks = 40; @@ -577,6 +579,16 @@ contract RootManager_ProposeAggregateRoot is Base { _rootManager.proposeAggregateRoot(snapshotId, aggregateRoot, snapshotsRoots, _domains); } + function test_revertIfPaused(bytes32 aggregateRoot, bytes32[] memory snapshotsRoots) public { + uint256 snapshotId = SnapshotId.getLastCompletedSnapshotId(); + _rootManager.forTest_generateAndAddDomains(_domains, _connectors); + _rootManager.forTest_pause(); + + vm.expectRevert(bytes("Pausable: paused")); + vm.prank(proposer); + _rootManager.proposeAggregateRoot(snapshotId, aggregateRoot, snapshotsRoots, _domains); + } + function test_emitProposeAggregateRoot( bytes32 aggregateRoot, bytes32 baseRoot, @@ -603,8 +615,6 @@ contract RootManager_ProposeAggregateRoot is Base { } contract RootManager_Finalize is Base { - event ProposedRootFinalized(bytes32 aggregateRoot); - function test_revertIfSlowModeOn() public { _rootManager.forTest_setOptimisticMode(false); @@ -646,7 +656,14 @@ contract RootManager_Finalize is Base { _rootManager.finalize(_differentRoot, block.number + _disputeBlocks); } - function test_setFinalizedAggregateRoot(bytes32 aggregateRoot) public { + function test_revertIfPaused(bytes32 aggregateRoot) public { + _rootManager.forTest_pause(); + + vm.expectRevert(bytes("Pausable: paused")); + _rootManager.finalize(aggregateRoot, block.number + _disputeBlocks); + } + + function test_saveAggregateRoot(bytes32 aggregateRoot) public { vm.assume(aggregateRoot != _finalizedHash); _rootManager.forTest_setProposeHash(aggregateRoot, block.number + _disputeBlocks); vm.roll(block.number + _disputeBlocks); @@ -656,19 +673,47 @@ contract RootManager_Finalize is Base { _rootManager.finalize(aggregateRoot, block.number); bytes32 afterAggregateRootHash = _rootManager.proposedAggregateRootHash(); - bytes32 finalizedAggregateRoot = _rootManager.finalizedOptimisticAggregateRoot(); + uint256 _lastSavedRootTimestamp = _rootManager.lastSavedAggregateRootTimestamp(); + + bytes32 finalizedAggregateRoot = _rootManager.validAggregateRoots(_lastSavedRootTimestamp); assertEq(_previousAggregateRoot, finalizedAggregateRoot); assertEq(afterAggregateRootHash, _finalizedHash); } - function test_emitIfProposedRootHasFinalized(bytes32 aggregateRoot) public { + function test_setLastSavedAggregateRootTimestamp(bytes32 aggregateRoot) public { + vm.assume(aggregateRoot != _finalizedHash); + _rootManager.forTest_setProposeHash(aggregateRoot, block.number + _disputeBlocks); + vm.roll(block.number + _disputeBlocks); + + uint256 _expectedRootTimestamp = block.timestamp; + + _rootManager.finalize(aggregateRoot, block.number); + + uint256 _lastSavedRootTimestamp = _rootManager.lastSavedAggregateRootTimestamp(); + + assertEq(_lastSavedRootTimestamp, _expectedRootTimestamp); + } + + function test_clearProposeHash(bytes32 aggregateRoot) public { + vm.assume(aggregateRoot != _finalizedHash); + _rootManager.forTest_setProposeHash(aggregateRoot, block.number + _disputeBlocks); + vm.roll(block.number + _disputeBlocks); + + _rootManager.finalize(aggregateRoot, block.number); + + bytes32 _currentProposeHash = _rootManager.proposedAggregateRootHash(); + + assertEq(_currentProposeHash, _rootManager.FINALIZED_HASH()); + } + + function test_emitIfAggregateRootSaved(bytes32 aggregateRoot) public { vm.assume(aggregateRoot != _finalizedHash); _rootManager.forTest_setProposeHash(aggregateRoot, block.number + _disputeBlocks); vm.roll(block.number + _disputeBlocks); vm.expectEmit(true, true, true, true); - emit ProposedRootFinalized(aggregateRoot); + emit AggregateRootSavedOptimistic(aggregateRoot, block.timestamp); _rootManager.finalize(aggregateRoot, block.number); } @@ -858,10 +903,6 @@ contract RootManager_ActivateOptimisticMode is Base { event OptimisticModeActivated(); using QueueLib for QueueLib.Queue; - function setUp() public virtual override { - super.setUp(); - } - function test_revertIfCallerIsNotOwner() public { vm.prank(stranger); vm.expectRevert(abi.encodeWithSelector(ProposedOwnable.ProposedOwnable__onlyOwner_notOwner.selector)); @@ -902,17 +943,6 @@ contract RootManager_ActivateOptimisticMode is Base { assertEq(pendingInboundsRoots, 0); } - function test_merkleCountIsSet() public { - _rootManager.forTest_setOptimisticMode(false); - - uint256 beforeCount = _rootManager.MERKLE().count(); - - vm.prank(owner); - _rootManager.activateOptimisticMode(); - - assertEq(beforeCount, _rootManager.lastCountBeforeOpMode()); - } - function test_emitIfOptimisticModeIsActivated() public { _rootManager.forTest_setOptimisticMode(false); @@ -925,10 +955,6 @@ contract RootManager_ActivateOptimisticMode is Base { } contract RootManager_RemoveConnector is Base { - function setUp() public virtual override { - super.setUp(); - } - function test_deleteProposedAggregateRoot(bytes32 aggregateRoot) public { _rootManager.forTest_generateAndAddDomains(_domains, _connectors); uint256 endOfDispute = block.number + _disputeBlocks; @@ -955,10 +981,6 @@ contract RootManager_RemoveConnector is Base { contract RootManager_Aggregate is Base { using QueueLib for QueueLib.Queue; - function setUp() public virtual override { - super.setUp(); - } - function test_revertIfNotValidConnector(uint8 index, address invalidConnector, bytes32 inbound) public { vm.assume(index < _domains.length); vm.assume(invalidConnector != _connectors[index]); @@ -1023,10 +1045,6 @@ contract RootManager_AddProposer is Base { contract RootManager_RemoveProposer is Base { event ProposerRemoved(address indexed proposer); - function setUp() public virtual override { - super.setUp(); - } - function test_revertIfCallerIsNotOwner() public { vm.prank(stranger); vm.expectRevert(abi.encodeWithSelector(ProposedOwnable.ProposedOwnable__onlyOwner_notOwner.selector)); @@ -1051,11 +1069,10 @@ contract RootManager_RemoveProposer is Base { } contract RootManager_Propagate is Base { - event OptimisticRootPropagated(bytes32 indexed aggregateRoot, bytes32 domainsHash); - event RootPropagated(bytes32 aggregateRoot, uint256 count, bytes32 domainsHash); - - function setUp() public virtual override { - super.setUp(); + function test_revertIfContractPaused() public { + _rootManager.forTest_pause(); + vm.expectRevert(bytes("Pausable: paused")); + _rootManager.propagate(_connectors, _fees, _encodedData); } function test_revertIfInvalidLengthsIfDifferentFeesAmounts(uint256[] calldata randomFees) public { @@ -1074,164 +1091,37 @@ contract RootManager_Propagate is Base { _rootManager.propagate(_connectors, _fees, randomEncodedData); } - function test_callOptimisticPropagateFunction(bytes32 aggregateRoot) public { - vm.assume(aggregateRoot > _finalizedHash); + function test_revertIfAggregateRootIsZero() public { _rootManager.forTest_setOptimisticMode(true); - _rootManager.forTest_setFinalizedOptimisticRoot(aggregateRoot); - - utils_generateAndAddConnectors(_connectors.length, false, true); - - bytes32 _domainsHash = _rootManager.domainsHash(); - - vm.expectEmit(true, true, true, true); - emit OptimisticRootPropagated(aggregateRoot, _domainsHash); - - _rootManager.propagate(_connectors, _fees, _encodedData); - } - - function test_callSlowPropagateFunction(bytes32 aggregateRoot, uint256 count) public { - vm.assume(aggregateRoot > 0 && count > _rootManager.lastCountBeforeOpMode()); - _rootManager.forTest_setOptimisticMode(false); - - utils_generateAndAddConnectors(_connectors.length, false, true); - - bytes32 _domainsHash = _rootManager.domainsHash(); - - vm.expectEmit(true, true, true, true); - emit RootPropagated(aggregateRoot, count, _domainsHash); + _rootManager.forTest_generateAndAddDomains(_domains, _connectors); - vm.mockCall( - _merkle, - abi.encodeWithSelector(MerkleTreeManager.rootAndCount.selector), - abi.encode(aggregateRoot, count) - ); + // set to zero to guaranty that is an invalid timestamp that will return a zero root. + _rootManager.forTest_setLastSavedAggregateRootTimestamp(0); + vm.expectRevert(abi.encodeWithSelector(RootManager.RootManager_propagate__AggregateRootIsZero.selector)); _rootManager.propagate(_connectors, _fees, _encodedData); } -} - -contract RootManager_OptimisticPropagate is Base { - event OptimisticRootPropagated(bytes32 indexed aggregateRoot, bytes32 domainsHash); - - function setUp() public virtual override { - super.setUp(); - } - - function test_revertIfImmediatePropagate() public { - vm.expectRevert( - abi.encodeWithSelector(RootManager.RootManager_optimisticPropagate__ForbiddenOptimisticRoot.selector) - ); - _rootManager.forTest_optimisticPropagate(_connectors, _fees, _encodedData); - } - - function test_revertIfFinalizedRootIsFinalizedHash() public { - _rootManager.forTest_setFinalizedOptimisticRoot(_finalizedHash); - - vm.expectRevert( - abi.encodeWithSelector(RootManager.RootManager_optimisticPropagate__ForbiddenOptimisticRoot.selector) - ); - _rootManager.forTest_optimisticPropagate(_connectors, _fees, _encodedData); - } - function test_deleteFinalizedData(bytes32 aggregateRoot) public { + function test_emitIfAggregateRootPropagated(bytes32 aggregateRoot) public { vm.assume(aggregateRoot > _finalizedHash); - _rootManager.forTest_setFinalizedOptimisticRoot(aggregateRoot); - - utils_generateAndAddConnectors(_connectors.length, false, true); - _rootManager.forTest_optimisticPropagate(_connectors, _fees, _encodedData); - - bytes32 _afterFinalizedRoot = _rootManager.finalizedOptimisticAggregateRoot(); - assertEq(_afterFinalizedRoot, _finalizedHash); - } - - function test_emitEventOptimisticRootPropagated(bytes32 aggregateRoot) public { - vm.assume(aggregateRoot > _finalizedHash); - _rootManager.forTest_setFinalizedOptimisticRoot(aggregateRoot); - - utils_generateAndAddConnectors(_connectors.length, false, true); - - bytes32 _domainsHash = _rootManager.domainsHash(); - - vm.expectEmit(true, true, true, true); - emit OptimisticRootPropagated(aggregateRoot, _domainsHash); - - _rootManager.forTest_optimisticPropagate(_connectors, _fees, _encodedData); - } -} - -contract RootManager_SlowPropagate is Base { - event RootPropagated(bytes32 aggregateRoot, uint256 count, bytes32 domainsHash); - - function setUp() public virtual override { - super.setUp(); - } - - function test_revertIfLastCountIsGreaterThanCount(bytes32 aggregateRoot, uint256 lastCountBeforeOpMode) public { - // MERKLE.count will be zero for this example since the tree is new. - vm.assume(aggregateRoot > _finalizedHash && lastCountBeforeOpMode > 0); - _rootManager.forTest_setLastCountBeforeOpMode(lastCountBeforeOpMode); - - vm.expectRevert(abi.encodeWithSelector(RootManager.RootManager_slowPropagate__OldAggregateRoot.selector)); - _rootManager.forTest_slowPropagate(_connectors, _fees, _encodedData); - } - - function test_revertIfLastCountIsEqualToCount(bytes32 aggregateRoot, uint256 lastCountBeforeOpMode) public { - vm.assume(aggregateRoot > _finalizedHash && lastCountBeforeOpMode > 0); - - vm.mockCall( - _merkle, - abi.encodeWithSelector(MerkleTreeManager.rootAndCount.selector), - abi.encode(aggregateRoot, lastCountBeforeOpMode) - ); - - _rootManager.forTest_setLastCountBeforeOpMode(lastCountBeforeOpMode); - - vm.expectRevert(abi.encodeWithSelector(RootManager.RootManager_slowPropagate__OldAggregateRoot.selector)); - _rootManager.forTest_slowPropagate(_connectors, _fees, _encodedData); - } - - function test_deleteFinalizedOptimisticAggregateRoot(bytes32 aggregateRoot, uint256 count) public { - vm.assume(aggregateRoot > _finalizedHash && count > _rootManager.lastCountBeforeOpMode()); - utils_generateAndAddConnectors(_connectors.length, false, true); - vm.mockCall( - _merkle, - abi.encodeWithSelector(MerkleTreeManager.rootAndCount.selector), - abi.encode(aggregateRoot, count) - ); - - _rootManager.forTest_setFinalizedOptimisticRoot(aggregateRoot); - - _rootManager.forTest_slowPropagate(_connectors, _fees, _encodedData); - - bytes32 _finalizedOptimisticRoot = _rootManager.finalizedOptimisticAggregateRoot(); - - assertEq(_finalizedOptimisticRoot, _finalizedHash); - } + _rootManager.forTest_setOptimisticMode(true); - function test_emitEventRootPropagated(bytes32 aggregateRoot, uint256 count) public { - vm.assume(aggregateRoot > _finalizedHash && count > _rootManager.lastCountBeforeOpMode()); + uint256 _rootTimestamp = block.timestamp; + _rootManager.forTest_setLastSavedAggregateRootTimestamp(_rootTimestamp); + _rootManager.forTest_setValidAggregateRoot(aggregateRoot, _rootTimestamp); utils_generateAndAddConnectors(_connectors.length, false, true); bytes32 _domainsHash = _rootManager.domainsHash(); vm.expectEmit(true, true, true, true); - emit RootPropagated(aggregateRoot, count, _domainsHash); + emit AggregateRootPropagated(aggregateRoot, _domainsHash); - vm.mockCall( - _merkle, - abi.encodeWithSelector(MerkleTreeManager.rootAndCount.selector), - abi.encode(aggregateRoot, count) - ); - _rootManager.forTest_slowPropagate(_connectors, _fees, _encodedData); + _rootManager.propagate(_connectors, _fees, _encodedData); } } contract RootManager_SendRootToHubs is Base { - function setUp() public virtual override { - super.setUp(); - } - function test_revertIfRedundantRoot(bytes32 aggregateRoot) public { _rootManager.forTest_setDomains(_domains); for (uint256 i = 0; i < _connectors.length; i++) { @@ -1279,14 +1169,11 @@ contract RootManager_SendRootToHubs is Base { _rootManager.forTest_sendRootToHubs(aggregateRoot, _connectors, _fees, _encodedData); } - function test_revertIfSendingIncorrectAmounOfEth( - bytes32 aggregateRoot, - uint32 newDomain, - address newConnector - ) public { - vm.assume(newDomain > 0); + function test_revertIfSendingIncorrectAmounOfEth(bytes32 aggregateRoot) public { vm.assume(aggregateRoot > _finalizedHash); - vm.assume(newConnector != address(0)); + + uint32 newDomain = uint32(1002); + address newConnector = address(1002); // Ensure that the fuzzed reverterDomain is never equal to one of the valid domains. for (uint256 i = 0; i < _domains.length; i++) { @@ -1409,21 +1296,6 @@ contract RootManager_SendRootToHubs is Base { } contract RootManager_FinalizeAndPropagate is Base { - function setUp() public virtual override { - super.setUp(); - } - - function test_revertIfContractPaused( - address[] memory connectors, - uint256[] memory fees, - bytes[] memory encodedData - ) public { - _rootManager.forTest_pause(); - vm.expectRevert(bytes("Pausable: paused")); - uint256 _endOfDispute = block.number + _disputeBlocks; - _rootManager.finalizeAndPropagate(connectors, fees, encodedData, _randomRoot, _endOfDispute); - } - function test_finalizeAndPropagate(bytes32 aggregateRoot) public { vm.assume(aggregateRoot > _finalizedHash); uint256 _endOfDispute = block.number - 1; @@ -1431,7 +1303,6 @@ contract RootManager_FinalizeAndPropagate is Base { _rootManager.forTest_setOptimisticMode(true); utils_generateAndAddConnectors(_connectors.length, false, true); _rootManager.forTest_setProposeHash(aggregateRoot, _endOfDispute); - _rootManager.forTest_setFinalizedOptimisticRoot(aggregateRoot); _rootManager.finalizeAndPropagate(_connectors, _fees, _encodedData, aggregateRoot, _endOfDispute); } @@ -1442,3 +1313,232 @@ contract RootManager_GetSnapshotDuration is Base { assertEq(SnapshotId.SNAPSHOT_DURATION, _rootManager.getSnapshotDuration()); } } + +contract RootManager_Dequeue is Base { + bytes32 RANDOM_INBOUND_ROOT = bytes32("random inbound root"); + + bytes4 insertSelector = bytes4(keccak256("insert(bytes32[])")); + + uint256 mockedCount = 1; + + function test_nothingToDequeueReturnsSameRoot() public { + bytes32 _beforeRoot = MerkleTreeManager(_merkle).root(); + + (bytes32 _afterRoot, ) = _rootManager.dequeue(); + + assertEq(_beforeRoot, _afterRoot); + } + + function test_callMerkleManagerInsert() public { + _rootManager.forTest_addInboundRootToQueue(RANDOM_INBOUND_ROOT); + + // fastforward blocks to make the element in the queue ready. + vm.roll(block.number + _rootManager.delayBlocks()); + + bytes32[] memory _verifiedInboundRoots = new bytes32[](1); + _verifiedInboundRoots[0] = RANDOM_INBOUND_ROOT; + + vm.expectCall(_merkle, abi.encodeWithSelector(insertSelector, _verifiedInboundRoots)); + + uint256 _expectedRootTimestamp = block.timestamp; + bytes32 _beforeRoot = _rootManager.validAggregateRoots(_expectedRootTimestamp); + + _rootManager.dequeue(); + + bytes32 _afterRoot = _rootManager.validAggregateRoots(_expectedRootTimestamp); + + assertNotEq(_beforeRoot, _afterRoot); + } + + function test_saveNewAggregateRoot(bytes32 aggregateRoot) public { + _rootManager.forTest_addInboundRootToQueue(RANDOM_INBOUND_ROOT); + + // fastforward blocks to make the element in the queue ready. + vm.roll(block.number + _rootManager.delayBlocks()); + + // Mock the call over `insert` + vm.mockCall(_merkle, abi.encodeWithSelector(insertSelector), abi.encode(aggregateRoot, mockedCount)); + + uint256 _lastSavedRootTimestamp = block.timestamp; + + _rootManager.dequeue(); + + bytes32 _root = _rootManager.validAggregateRoots(_lastSavedRootTimestamp); + + assertEq(_root, aggregateRoot); + } + + function test_updateLastSavedRootTimestamp(bytes32 aggregateRoot) public { + _rootManager.forTest_addInboundRootToQueue(RANDOM_INBOUND_ROOT); + + // fastforward blocks to make the element in the queue ready. + vm.roll(block.number + _rootManager.delayBlocks()); + + // Mock the call over `insert` + vm.mockCall(_merkle, abi.encodeWithSelector(insertSelector), abi.encode(aggregateRoot, mockedCount)); + + uint256 _expectedRootTimestamp = block.timestamp; + + _rootManager.dequeue(); + + uint256 _lastSavedRootTimestamp = _rootManager.lastSavedAggregateRootTimestamp(); + + assertEq(_lastSavedRootTimestamp, _expectedRootTimestamp); + } + + function test_emitIfAggregateRootSaved(bytes32 aggregateRoot) public { + _rootManager.forTest_addInboundRootToQueue(RANDOM_INBOUND_ROOT); + + // fastforward blocks to make the element in the queue ready. + vm.roll(block.number + _rootManager.delayBlocks()); + + // Mock the call over `insert` + vm.mockCall(_merkle, abi.encodeWithSelector(insertSelector), abi.encode(aggregateRoot, mockedCount)); + + uint256 _expectedSavedTimestamp = block.timestamp; + + bytes32[] memory _verifiedInboundRoots = new bytes32[](1); + _verifiedInboundRoots[0] = RANDOM_INBOUND_ROOT; + + vm.expectEmit(true, true, true, true); + emit AggregateRootSavedSlow(aggregateRoot, mockedCount, _verifiedInboundRoots, _expectedSavedTimestamp); + + _rootManager.dequeue(); + } +} + +contract RootManager_setHubDomain is Base { + function test_revertIfNotOwner(address stranger, uint32 hubDomain) public { + vm.assume(stranger != owner); + + vm.expectRevert(ProposedOwnable.ProposedOwnable__onlyOwner_notOwner.selector); + vm.prank(stranger); + _rootManager.setHubDomain(hubDomain); + } + + function test_revertIfDomainIsNotSupported(uint32 hubDomain) public { + // At this point no domains have been added. + vm.expectRevert(RootManager.RootManager_setHubDomain__InvalidDomain.selector); + vm.prank(owner); + _rootManager.setHubDomain(hubDomain); + } + + function test_setHubDomainCorrectly(uint32 hubDomain) public { + vm.assume(hubDomain != 0); + + uint32[] memory domains = new uint32[](1); + domains[0] = hubDomain; + + address[] memory connectors = new address[](1); + connectors[0] = makeAddr("connector 1"); + + _rootManager.forTest_generateAndAddDomains(domains, connectors); + + uint32 _beforeHubDomain = _rootManager.hubDomain(); + + vm.prank(owner); + _rootManager.setHubDomain(hubDomain); + + uint32 _afterHubDomain = _rootManager.hubDomain(); + + assertEq(_beforeHubDomain != hubDomain, true); + assertEq(hubDomain, _afterHubDomain); + } + + function test_emitIfHubDomainSet(uint32 hubDomain) public { + vm.assume(hubDomain != 0); + + uint32[] memory domains = new uint32[](1); + domains[0] = hubDomain; + + address[] memory connectors = new address[](1); + connectors[0] = makeAddr("connector 1"); + + _rootManager.forTest_generateAndAddDomains(domains, connectors); + + vm.expectEmit(true, true, true, true); + emit HubDomainSet(hubDomain); + + vm.prank(owner); + _rootManager.setHubDomain(hubDomain); + } +} + +contract RootManager_clearHubDomain is Base { + function test_revertIfNotOwner(address stranger) public { + vm.assume(stranger != owner); + + vm.expectRevert(ProposedOwnable.ProposedOwnable__onlyOwner_notOwner.selector); + vm.prank(stranger); + _rootManager.clearHubDomain(); + } + + function test_clearHubDomain(uint32 hubDomain) public { + vm.assume(hubDomain != 0); + _rootManager.forTest_setHubDomain(hubDomain); + + vm.prank(owner); + _rootManager.clearHubDomain(); + + assertEq(_rootManager.hubDomain(), 0); + } + + function test_emitIfHubDomainCleared(uint32 hubDomain) public { + vm.assume(hubDomain != 0); + _rootManager.forTest_setHubDomain(hubDomain); + + vm.expectEmit(true, true, true, true); + emit HubDomainCleared(); + + vm.prank(owner); + _rootManager.clearHubDomain(); + } +} + +contract RootManager_sendRootToHubSpoke is Base { + function test_revertWhenPaused() public { + _rootManager.forTest_pause(); + + vm.expectRevert("Pausable: paused"); + _rootManager.sendRootToHubSpoke(); + } + + function test_sendRoot(bytes32 aggregateRoot, uint256 timestamp, uint32 hubDomain) public { + vm.assume(aggregateRoot != 0); + vm.assume(timestamp != 0); + vm.assume(hubDomain != 0); + + _rootManager.forTest_setLastSavedAggregateRootTimestamp(timestamp); + _rootManager.forTest_setValidAggregateRoot(aggregateRoot, timestamp); + + // set the fuzzed domain as the hub domain + _rootManager.forTest_setHubDomain(hubDomain); + + // create and populate the domains and connectors arrays + uint32[] memory domains = new uint32[](1); + domains[0] = hubDomain; + address[] memory connectors = new address[](1); + connectors[0] = makeAddr("connector 1"); + + // use the first and only connector as the hub spoke connector + address hubSpokeConnector = connectors[0]; + + // add the fuzzed domain and the connector to the supported domains + _rootManager.forTest_generateAndAddDomains(domains, connectors); + + // mock call to the hub spoke connector + vm.mockCall( + hubSpokeConnector, + abi.encodeWithSelector(IHubSpokeConnector.saveAggregateRoot.selector, aggregateRoot), + abi.encode() + ); + + // expect the call to the hub spoke connector with the correct aggregate root + vm.expectCall( + hubSpokeConnector, + abi.encodeWithSelector(IHubSpokeConnector.saveAggregateRoot.selector, aggregateRoot) + ); + + _rootManager.sendRootToHubSpoke(); + } +} diff --git a/packages/deployments/contracts/contracts_forge/messaging/connectors/SpokeConnector.t.sol b/packages/deployments/contracts/contracts_forge/messaging/connectors/SpokeConnector.t.sol index 15ce87711e..abffe7d618 100644 --- a/packages/deployments/contracts/contracts_forge/messaging/connectors/SpokeConnector.t.sol +++ b/packages/deployments/contracts/contracts_forge/messaging/connectors/SpokeConnector.t.sol @@ -4,17 +4,21 @@ pragma solidity 0.8.17; import {MockSpokeConnector} from "../../utils/Mock.sol"; import {SpokeConnector} from "../../../contracts/messaging/connectors/SpokeConnector.sol"; import {WatcherManager} from "../../../contracts/messaging/WatcherManager.sol"; +import {RootManager} from "../../../contracts/messaging/RootManager.sol"; import {MerkleTreeManager} from "../../../contracts/messaging/MerkleTreeManager.sol"; import {Message} from "../../../contracts/messaging/libraries/Message.sol"; import {RateLimited} from "../../../contracts/messaging/libraries/RateLimited.sol"; import {TypeCasts} from "../../../contracts/shared/libraries/TypeCasts.sol"; import {MerkleLib} from "../../../contracts/messaging/libraries/MerkleLib.sol"; import {SnapshotId} from "../../../contracts/messaging/libraries/SnapshotId.sol"; +import {ProposedOwnable} from "../../../contracts/shared/ProposedOwnable.sol"; import "../../utils/ForgeHelper.sol"; contract Base is ForgeHelper { event MessageSent(bytes data, bytes encodedData, address caller); + event ProposerAdded(address indexed _proposer); + event ProposerRemoved(address indexed _proposer); using stdStorage for StdStorage; @@ -34,12 +38,16 @@ contract Base is ForgeHelper { address _destinationMainnetAMB = address(456456); address _originMainnetAMB = address(123123); address _rootManager = address(121212); + address _proposer = makeAddr("proposer"); + WatcherManager _watcherManager; MerkleTreeManager _merkle; uint256 PROCESS_GAS = 850_000; uint256 RESERVE_GAS = 15_000; uint256 SNAPSHOT_DURATION = 30 minutes; + uint256 _minDisputeBlocks = 100; + uint256 _disputeBlocks = 120; // ============ Setup ============ function setUp() public virtual { @@ -53,18 +61,22 @@ contract Base is ForgeHelper { _watcherManager = new WatcherManager(); _merkle = new MerkleTreeManager(); - spokeConnector = new MockSpokeConnector( - _originDomain, // uint32 _domain, - _mainnetDomain, // uint32 _mirrorDomain - _originAMB, // address _amb, - _rootManager, // address _rootManager, - address(_merkle), // address _merkle - address(0), // address _mirrorConnector - PROCESS_GAS, // uint256 _processGas, - RESERVE_GAS, // uint256 _reserveGas - 0, // uint256 _delayBlocks - address(_watcherManager) - ); + SpokeConnector.ConstructorParams memory _baseParams = SpokeConnector.ConstructorParams({ + domain: _originDomain, + mirrorDomain: _mainnetDomain, + amb: _originAMB, + rootManager: _rootManager, + mirrorConnector: address(0), + processGas: PROCESS_GAS, + reserveGas: RESERVE_GAS, + delayBlocks: 0, + merkle: address(_merkle), + watcherManager: address(_watcherManager), + minDisputeBlocks: _minDisputeBlocks, + disputeBlocks: _disputeBlocks + }); + + spokeConnector = new MockSpokeConnector(_baseParams); vm.stopPrank(); } @@ -179,6 +191,44 @@ contract SpokeConnector_General is Base { } } +contract SpokeConnector_Constructor is Base { + function test_shouldInitializeValuesCorrectly() public { + assertEq(spokeConnector.DOMAIN(), _originDomain); + assertEq(spokeConnector.MIRROR_DOMAIN(), _mainnetDomain); + assertEq(spokeConnector.AMB(), _originAMB); + assertEq(spokeConnector.ROOT_MANAGER(), _rootManager); + assertEq(spokeConnector.mirrorConnector(), address(0)); + assertEq(spokeConnector.PROCESS_GAS(), PROCESS_GAS); + assertEq(spokeConnector.RESERVE_GAS(), RESERVE_GAS); + assertEq(spokeConnector.delayBlocks(), 0); + assertEq(address(spokeConnector.MERKLE()), address(_merkle)); + assertEq(address(spokeConnector.watcherManager()), address(_watcherManager)); + assertEq(spokeConnector.minDisputeBlocks(), _minDisputeBlocks); + assertEq(spokeConnector.disputeBlocks(), _disputeBlocks); + } + + function test_shouldRevertIfDisputeBlocksLessThanMinDisputeBlocks() public { + uint256 _failingDisputeBlocks = _minDisputeBlocks - 1; + SpokeConnector.ConstructorParams memory _constructorParams = SpokeConnector.ConstructorParams({ + domain: _originDomain, + mirrorDomain: _mainnetDomain, + amb: _originAMB, + rootManager: _rootManager, + mirrorConnector: address(0), + processGas: PROCESS_GAS, + reserveGas: RESERVE_GAS, + delayBlocks: 0, + merkle: address(_merkle), + watcherManager: address(_watcherManager), + minDisputeBlocks: _minDisputeBlocks, + disputeBlocks: _failingDisputeBlocks + }); + + vm.expectRevert(SpokeConnector.SpokeConnector_constructor__DisputeBlocksLowerThanMin.selector); + new MockSpokeConnector(_constructorParams); + } +} + contract SpokeConnector_Dispatch is Base { event SnapshotRootSaved(uint256 indexed snapshotId, bytes32 indexed root, uint256 indexed count); event Dispatch(bytes32 indexed leaf, uint256 indexed index, bytes32 indexed root, bytes message); @@ -314,12 +364,478 @@ contract SpokeConnector_Dispatch is Base { } } -contract SpokeConnector_GetLastCompletedSnapshotId is Base { - function test_getLastCompletedSnapshotIdExternal(uint128 _snapshotId) public { - vm.assume(_snapshotId > 0); +contract SpokeConnector_AddProposer is Base { + function test_revertIfCallerIsNotOwner(address _stranger, address _proposer) public { + vm.assume(_proposer != address(0)); + vm.assume(_stranger != owner); + + vm.prank(_stranger); + vm.expectRevert(ProposedOwnable.ProposedOwnable__onlyOwner_notOwner.selector); + spokeConnector.addProposer(_proposer); + } + + function test_addProposer(address _proposer) public { + vm.assume(_proposer != address(0)); + + vm.expectEmit(true, true, true, true); + emit ProposerAdded(_proposer); + + vm.prank(owner); + spokeConnector.addProposer(_proposer); + + assertEq(spokeConnector.allowlistedProposers(_proposer), true); + } +} + +contract SpokeConnector_RemoveProposer is Base { + function test_revertIfCallerIsNotOwner(address _stranger, address _proposer) public { + vm.assume(_proposer != address(0)); + vm.assume(_stranger != owner); + + vm.prank(_stranger); + vm.expectRevert(ProposedOwnable.ProposedOwnable__onlyOwner_notOwner.selector); + spokeConnector.removeProposer(_proposer); + } + + function test_removeProposer(address _proposer) public { + vm.assume(_proposer != address(0)); + vm.startPrank(owner); + + spokeConnector.addProposer(_proposer); + + vm.expectEmit(true, true, true, true); + emit ProposerRemoved(_proposer); + + spokeConnector.removeProposer(_proposer); + assertEq(spokeConnector.allowlistedProposers(_proposer), false); + } +} + +contract SpokeConnector_activateSlowMode is Base { + event SlowModeActivated(address indexed watcher); + + function setUp() public virtual override { + super.setUp(); + MockSpokeConnector(payable(address(spokeConnector))).setOptimisticMode(true); + + vm.mockCall( + address(_watcherManager), + abi.encodeWithSelector(WatcherManager(_watcherManager).isWatcher.selector), + abi.encode(true) + ); + } + + function test_revertIfCallerIsNotWatcher(address caller) public { + vm.mockCall( + address(_watcherManager), + abi.encodeWithSelector(WatcherManager(_watcherManager).isWatcher.selector), + abi.encode(false) + ); + vm.expectRevert(bytes("!watcher")); + vm.prank(caller); + spokeConnector.activateSlowMode(); + } + + function test_revertIfSlowModeOn() public { + MockSpokeConnector(payable(address(spokeConnector))).setOptimisticMode(false); + + vm.expectRevert(abi.encodeWithSelector(SpokeConnector.SpokeConnector_onlyOptimisticMode__SlowModeOn.selector)); + vm.prank(owner); + spokeConnector.activateSlowMode(); + } + + function test_cleanProposedAggregateRoot(bytes32 _proposedAggregateRootHash) public { + vm.assume(_proposedAggregateRootHash != spokeConnector.FINALIZED_HASH()); + MockSpokeConnector(payable(address(spokeConnector))).setProposedAggregateRootHash(_proposedAggregateRootHash); + spokeConnector.activateSlowMode(); + assertEq(spokeConnector.proposedAggregateRootHash(), spokeConnector.FINALIZED_HASH()); + } + + function test_emitSlowModeActivated() public { + vm.expectEmit(true, true, true, true); + emit SlowModeActivated(owner); + + vm.prank(owner); + spokeConnector.activateSlowMode(); + } +} + +contract SpokeConnector_activateOptimisticMode is Base { + event OptimisticModeActivated(); + + function test_revertIfCallerIsNotOwner(address stranger) public { + vm.assume(stranger != owner); + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(ProposedOwnable.ProposedOwnable__onlyOwner_notOwner.selector)); + spokeConnector.activateOptimisticMode(); + } + + function test_revertIfOptimisticModeOn() public { + MockSpokeConnector(payable(address(spokeConnector))).setOptimisticMode(true); + vm.expectRevert( + abi.encodeWithSelector(SpokeConnector.SpokeConnector_activateOptimisticMode__OptimisticModeOn.selector) + ); + + vm.prank(owner); + spokeConnector.activateOptimisticMode(); + } + + function test_switchToOptimisticMode() public { + MockSpokeConnector(payable(address(spokeConnector))).setOptimisticMode(false); + bool beforeMode = spokeConnector.optimisticMode(); + + vm.prank(owner); + spokeConnector.activateOptimisticMode(); + bool afterMode = spokeConnector.optimisticMode(); + + assertEq(beforeMode, false); + assertEq(afterMode, true); + } + + function test_emitIfOptimisticModeIsActivated() public { + MockSpokeConnector(payable(address(spokeConnector))).setOptimisticMode(false); + + vm.expectEmit(true, true, true, true); + emit OptimisticModeActivated(); + + vm.prank(owner); + spokeConnector.activateOptimisticMode(); + } +} + +contract SpokeConnector_ProposeAggregateRoot is Base { + event AggregateRootProposed( + bytes32 indexed aggregateRoot, + uint256 indexed rootTimestamp, + uint256 indexed endOfDispute, + uint32 domain + ); + + event PendingAggregateRootDeleted(bytes32 indexed aggregateRoot); + + function setUp() public virtual override { + super.setUp(); + MockSpokeConnector(payable(address(spokeConnector))).setAllowlistedProposer(_proposer, true); + MockSpokeConnector(payable(address(spokeConnector))).setOptimisticMode(true); + } + + function test_revertIfCallerIsNotProposer(address stranger, bytes32 aggregateRoot, uint256 rootTimestamp) public { + vm.assume(stranger != _proposer); + + vm.expectRevert( + abi.encodeWithSelector(SpokeConnector.SpokeConnector_onlyProposer__NotAllowlistedProposer.selector) + ); + vm.prank(stranger); + spokeConnector.proposeAggregateRoot(aggregateRoot, rootTimestamp); + } + + function test_revertIfSlowModeOn(bytes32 aggregateRoot, uint256 rootTimestamp) public { + MockSpokeConnector(payable(address(spokeConnector))).setOptimisticMode(false); + + vm.expectRevert(abi.encodeWithSelector(SpokeConnector.SpokeConnector_onlyOptimisticMode__SlowModeOn.selector)); + vm.prank(_proposer); + spokeConnector.proposeAggregateRoot(aggregateRoot, rootTimestamp); + } + + function test_revertIfSystemIsPaused(bytes32 aggregateRoot, uint256 rootTimestamp, address watcher) public { + utils_mockIsWatcher_true(watcher); + vm.prank(watcher); + spokeConnector.pause(); + vm.expectRevert("Pausable: paused"); + vm.prank(_proposer); + spokeConnector.proposeAggregateRoot(aggregateRoot, rootTimestamp); + } + + function test_revertIfProposeInProgress(bytes32 aggregateRoot, uint256 rootTimestamp) public { + vm.assume(aggregateRoot != spokeConnector.FINALIZED_HASH()); + MockSpokeConnector(payable(address(spokeConnector))).setProposedAggregateRootHash(bytes32("random hash")); + + vm.expectRevert( + abi.encodeWithSelector(SpokeConnector.SpokeConnector_proposeAggregateRoot__ProposeInProgress.selector) + ); + vm.prank(_proposer); + spokeConnector.proposeAggregateRoot(aggregateRoot, rootTimestamp); + } + + function test_deletePendingAggregateRoot(bytes32 aggregateRoot, uint256 rootTimestamp) public { + vm.assume(aggregateRoot != spokeConnector.FINALIZED_HASH()); + MockSpokeConnector(payable(address(spokeConnector))).setPendingAggregateRoot(aggregateRoot, block.number); + vm.prank(_proposer); + spokeConnector.proposeAggregateRoot(aggregateRoot, rootTimestamp); + assertEq(spokeConnector.pendingAggregateRoots(aggregateRoot), 0); + } + + function test_emitPendingAggregateRootDeleted(bytes32 aggregateRoot, uint256 rootTimestamp) public { + vm.assume(aggregateRoot != spokeConnector.FINALIZED_HASH()); + MockSpokeConnector(payable(address(spokeConnector))).setPendingAggregateRoot(aggregateRoot, block.number); + vm.expectEmit(true, true, true, true); + emit PendingAggregateRootDeleted(aggregateRoot); + vm.prank(_proposer); + spokeConnector.proposeAggregateRoot(aggregateRoot, rootTimestamp); + } + + function test_aggregateRootCorrectlyProposed(bytes32 aggregateRoot, uint256 rootTimestamp) public { + vm.assume(aggregateRoot != spokeConnector.FINALIZED_HASH()); + uint256 _endOfDispute = block.number + spokeConnector.disputeBlocks(); + bytes32 _expectedAggregateRootProposedHash = keccak256( + abi.encodePacked(aggregateRoot, rootTimestamp, _endOfDispute) + ); + vm.prank(_proposer); + spokeConnector.proposeAggregateRoot(aggregateRoot, rootTimestamp); + assertEq(spokeConnector.proposedAggregateRootHash(), _expectedAggregateRootProposedHash); + } + + function test_emitProposeAggregateRoot(bytes32 aggregateRoot, uint256 rootTimestamp) public { + uint256 _endOfDispute = block.number + spokeConnector.disputeBlocks(); + + vm.expectEmit(true, true, true, true); + emit AggregateRootProposed(aggregateRoot, rootTimestamp, _endOfDispute, spokeConnector.DOMAIN()); + vm.prank(_proposer); + spokeConnector.proposeAggregateRoot(aggregateRoot, rootTimestamp); + } +} + +contract SpokeConnector_Finalize is Base { + event ProposedRootFinalized(bytes32 aggregateRoot); + + function setUp() public virtual override { + super.setUp(); + MockSpokeConnector(payable(address(spokeConnector))).setOptimisticMode(true); + } + + function test_revertIfSlowModeOn(bytes32 randomRoot, uint256 randomRootTimestamp, uint256 randomEndOfDispute) public { + MockSpokeConnector(payable(address(spokeConnector))).setOptimisticMode(false); + + vm.expectRevert(abi.encodeWithSelector(SpokeConnector.SpokeConnector_onlyOptimisticMode__SlowModeOn.selector)); + spokeConnector.finalize(randomRoot, randomRootTimestamp, randomEndOfDispute); + } + + function test_revertIfSystemIsPaused( + bytes32 aggregateRoot, + uint256 rootTimestamp, + uint256 endOfDispute, + address watcher + ) public { + utils_mockIsWatcher_true(watcher); + vm.prank(watcher); + spokeConnector.pause(); + vm.expectRevert("Pausable: paused"); + spokeConnector.finalize(aggregateRoot, rootTimestamp, endOfDispute); + } + + function test_revertIfProposeInProgress( + bytes32 randomProposedHash, + uint256 randomRootTimestamp, + uint256 randomEndOfDispute + ) public { + vm.assume(randomProposedHash != spokeConnector.FINALIZED_HASH()); + vm.assume(randomEndOfDispute > block.number); + MockSpokeConnector(payable(address(spokeConnector))).setProposedAggregateRootHash(randomProposedHash); + + vm.expectRevert(abi.encodeWithSelector(SpokeConnector.SpokeConnector_finalize__ProposeInProgress.selector)); + spokeConnector.finalize(randomProposedHash, randomRootTimestamp, randomEndOfDispute); + } + + function test_revertIfProposedHashIsFinalizedHash( + bytes32 randomRoot, + uint256 randomRootTimestamp, + uint256 randomEndOfDispute + ) public { + vm.assume(randomRoot != spokeConnector.FINALIZED_HASH()); + vm.assume(randomEndOfDispute < block.number); + vm.roll(block.number + spokeConnector.disputeBlocks()); + + vm.expectRevert(SpokeConnector.SpokeConnector_finalize__ProposedHashIsFinalizedHash.selector); + spokeConnector.finalize(randomRoot, randomRootTimestamp, randomEndOfDispute); + } + + function test_revertIfAggregateRootDataIsInvalid( + bytes32 randomRoot, + uint256 randomRootTimestamp, + uint256 randomEndOfDispute + ) public { + vm.assume(randomEndOfDispute < block.number); + vm.roll(block.number + spokeConnector.disputeBlocks()); + MockSpokeConnector(payable(address(spokeConnector))).setProposedAggregateRootHash(spokeConnector.FINALIZED_HASH()); + + vm.expectRevert( + abi.encodeWithSelector(SpokeConnector.SpokeConnector_finalize__ProposedHashIsFinalizedHash.selector) + ); + + spokeConnector.finalize(randomRoot, randomRootTimestamp, randomEndOfDispute); + } + + function test_revertIfAggregateRootHashIsInvalid( + bytes32 aggregateRoot, + uint256 validRootTimestamp, + uint256 invalidRootTimestamp + ) public { + vm.assume(aggregateRoot != spokeConnector.FINALIZED_HASH()); + vm.assume(validRootTimestamp != invalidRootTimestamp); + // setting a block.number that's higher than dispute blocks. + vm.roll(spokeConnector.disputeBlocks() + 1); + bytes32 _placeholderProposedRoot = bytes32("Placeholder Root"); + uint256 _placeholderEndOfDispute = block.number - spokeConnector.disputeBlocks() - 1; + bytes32 _placeHolderProposedRootHash = keccak256( + abi.encode(_placeholderProposedRoot, validRootTimestamp, _placeholderEndOfDispute) + ); + MockSpokeConnector(payable(address(spokeConnector))).setProposedAggregateRootHash(_placeHolderProposedRootHash); + + vm.expectRevert(abi.encodeWithSelector(SpokeConnector.SpokeConnector_finalize__InvalidInputHash.selector)); + spokeConnector.finalize(aggregateRoot, invalidRootTimestamp, _placeholderEndOfDispute); + } + + function test_setFinalizedAggregateRoot(bytes32 aggregateRoot, uint256 rootTimestamp, uint256 endOfDispute) public { + vm.assume(aggregateRoot != spokeConnector.FINALIZED_HASH()); + vm.assume(endOfDispute < block.number); + + bytes32 _proposedRootHash = keccak256(abi.encode(aggregateRoot, rootTimestamp, endOfDispute)); + MockSpokeConnector(payable(address(spokeConnector))).setProposedAggregateRootHash(_proposedRootHash); + + vm.roll(block.number + spokeConnector.disputeBlocks()); + + vm.expectEmit(true, true, true, true); + emit ProposedRootFinalized(aggregateRoot); + + spokeConnector.finalize(aggregateRoot, rootTimestamp, endOfDispute); + + assertEq(spokeConnector.provenAggregateRoots(aggregateRoot), true); + assertEq(spokeConnector.proposedAggregateRootHash(), spokeConnector.FINALIZED_HASH()); + } +} + +contract SpokeConnector_SetMinDisputeBlocks is Base { + event MinDisputeBlocksUpdated(uint256 _previous, uint256 _updated); + + function test_revertIfCallerIsNotOwner(address stranger) public { + vm.assume(stranger != owner); + uint256 _newMinDisputeBlocks = spokeConnector.minDisputeBlocks() + 10; + vm.prank(stranger); + vm.expectRevert(ProposedOwnable.ProposedOwnable__onlyOwner_notOwner.selector); + spokeConnector.setMinDisputeBlocks(_newMinDisputeBlocks); + } + + function test_revertIfMinDisputeBlocksEqPrevMinDisputeBlocks() public { + uint256 _currentMinDisputeBlocks = spokeConnector.minDisputeBlocks(); + vm.prank(owner); + vm.expectRevert(SpokeConnector.SpokeConnector_setMinDisputeBlocks__SameMinDisputeBlocksAsBefore.selector); + spokeConnector.setMinDisputeBlocks(_currentMinDisputeBlocks); + } + + function test_changeMinDisputeBlocks(uint256 newMinDisputeBlocks) public { + uint256 _prevMinDisputeBlocks = spokeConnector.minDisputeBlocks(); + vm.assume(newMinDisputeBlocks != _prevMinDisputeBlocks); + vm.prank(owner); + spokeConnector.setMinDisputeBlocks(newMinDisputeBlocks); + uint256 _currentMinDisputeBlocks = spokeConnector.minDisputeBlocks(); + assertEq(_currentMinDisputeBlocks, newMinDisputeBlocks); + } + + function test_emitIfMinDisputeBlocksChanged(uint256 newMinDisputeBlocks) public { + uint256 _prevMinDisputeBlocks = spokeConnector.minDisputeBlocks(); + vm.assume(newMinDisputeBlocks != _prevMinDisputeBlocks); + vm.prank(owner); + + vm.expectEmit(true, true, true, true); + emit MinDisputeBlocksUpdated(_prevMinDisputeBlocks, newMinDisputeBlocks); + + spokeConnector.setMinDisputeBlocks(newMinDisputeBlocks); + } +} + +contract SpokeConnector_SetDisputeBlocks is Base { + event DisputeBlocksUpdated(uint256 _previous, uint256 _updated); + + function test_revertIfCallerIsNotOwner(address stranger) public { + vm.assume(stranger != owner); + uint256 _newDisputeBlocks = spokeConnector.disputeBlocks() + 10; + vm.prank(stranger); + vm.expectRevert(ProposedOwnable.ProposedOwnable__onlyOwner_notOwner.selector); + spokeConnector.setDisputeBlocks(_newDisputeBlocks); + } + + function test_revertIfDisputeBlocksAreLessThanMinAllowed(uint256 _smallDisputeBlocks) public { + uint256 _allowedMinDisputeBlocks = spokeConnector.minDisputeBlocks(); + vm.assume(_smallDisputeBlocks < _allowedMinDisputeBlocks); + vm.prank(owner); + vm.expectRevert(SpokeConnector.SpokeConnector_setDisputeBlocks__DisputeBlocksLowerThanMin.selector); + spokeConnector.setDisputeBlocks(_smallDisputeBlocks); + } + + function test_revertIfDisputeBlocksEqPrevDisputeBlocks() public { + uint256 _currentDisputeBlocks = spokeConnector.disputeBlocks(); + vm.prank(owner); + vm.expectRevert(SpokeConnector.SpokeConnector_setDisputeBlocks__SameDisputeBlocksAsBefore.selector); + spokeConnector.setDisputeBlocks(_currentDisputeBlocks); + } + + function test_changeDisputeBlocks(uint256 newDisputeBlocks) public { + uint256 _prevDisputeBlocks = spokeConnector.disputeBlocks(); + vm.assume(newDisputeBlocks > _prevDisputeBlocks); + vm.prank(owner); + spokeConnector.setDisputeBlocks(newDisputeBlocks); + uint256 _currentDisputeBlocks = spokeConnector.disputeBlocks(); + assertEq(_currentDisputeBlocks, newDisputeBlocks); + } + + function test_emitIfDisputeBlocksChanged(uint256 newDisputeBlocks) public { + uint256 _prevDisputeBlocks = spokeConnector.disputeBlocks(); + vm.assume(newDisputeBlocks > _prevDisputeBlocks); + vm.prank(owner); + + vm.expectEmit(true, true, true, true); + emit DisputeBlocksUpdated(_prevDisputeBlocks, newDisputeBlocks); + + spokeConnector.setDisputeBlocks(newDisputeBlocks); + } +} + +contract SpokeConnector_ReceiveAggregateRoot is Base { + event AggregateRootReceived(bytes32 indexed root); + + function setUp() public virtual override { + super.setUp(); + MockSpokeConnector(payable(address(spokeConnector))).setOptimisticMode(false); + } + + function test_revertIfNotInSlowMode(bytes32 newRoot) public { + MockSpokeConnector(payable(address(spokeConnector))).setOptimisticMode(true); + vm.expectRevert(SpokeConnector.SpokeConnector_receiveAggregateRoot__OptimisticModeOn.selector); + MockSpokeConnector(payable(address(spokeConnector))).receiveAggregateRootForTest(newRoot); + } + + function test_revertIfNewRootIsZero() public { + bytes32 newRoot = bytes32(""); + vm.expectRevert("new root empty"); + MockSpokeConnector(payable(address(spokeConnector))).receiveAggregateRootForTest(newRoot); + } + + function test_revertIfRootIsAlreadyPending(bytes32 newRoot) public { + vm.assume(newRoot != bytes32("")); + MockSpokeConnector(payable(address(spokeConnector))).setPendingAggregateRoot(newRoot, block.number); + vm.expectRevert("root already pending"); + MockSpokeConnector(payable(address(spokeConnector))).receiveAggregateRootForTest(newRoot); + } + + function test_revertIfRootIsAlreadyProven(bytes32 newRoot) public { + vm.assume(newRoot != bytes32("")); + MockSpokeConnector(payable(address(spokeConnector))).setProvenAggregateRoot(newRoot, true); + vm.expectRevert("root already proven"); + MockSpokeConnector(payable(address(spokeConnector))).receiveAggregateRootForTest(newRoot); + } + + function test_receiveAggregateRootSuccesfully(bytes32 newRoot) public { + vm.assume(newRoot != bytes32("")); + MockSpokeConnector(payable(address(spokeConnector))).receiveAggregateRootForTest(newRoot); + assertEq(spokeConnector.pendingAggregateRoots(newRoot), block.number); + } + + function test_emitIfAggregateRootIsReceived(bytes32 newRoot) public { + vm.assume(newRoot != bytes32("")); + vm.expectEmit(true, true, true, true); + emit AggregateRootReceived(newRoot); - vm.warp(SNAPSHOT_DURATION * _snapshotId); - assertEq(spokeConnector.getLastCompletedSnapshotId(), _snapshotId); + MockSpokeConnector(payable(address(spokeConnector))).receiveAggregateRootForTest(newRoot); } } diff --git a/packages/deployments/contracts/contracts_forge/messaging/connectors/admin/AdminHubConnector.t.sol b/packages/deployments/contracts/contracts_forge/messaging/connectors/admin/AdminHubConnector.t.sol index dc17d19374..fbfbc42dbf 100644 --- a/packages/deployments/contracts/contracts_forge/messaging/connectors/admin/AdminHubConnector.t.sol +++ b/packages/deployments/contracts/contracts_forge/messaging/connectors/admin/AdminHubConnector.t.sol @@ -15,7 +15,12 @@ import "../../../utils/ForgeHelper.sol"; contract AdminHubConnectorTest is ForgeHelper { event MessageProcessed(bytes data, address caller); event RootReceived(uint32 domain, bytes32 receivedRoot, uint256 queueIndex); - event RootsAggregated(bytes32 aggregateRoot, uint256 count, bytes32[] aggregatedMessageRoots); + event AggregateRootSavedSlow( + bytes32 aggregateRoot, + uint256 leafCount, + bytes32[] aggregatedRoots, + uint256 rootTimestamp + ); // ============ Storage ============ uint32 BNB_DOMAIN = 6450786; @@ -164,7 +169,7 @@ contract AdminHubConnectorTest is ForgeHelper { // NOTE: this event check ensures the root manager emitted the event, but not the data within // the event payload vm.expectEmit(true, true, true, true, address(rootManager)); - emit RootsAggregated(aggregateRoot, count, leaves); + emit AggregateRootSavedSlow(aggregateRoot, count, leaves, block.timestamp); rootManager.propagate(connectors, fees, encodedData); } } diff --git a/packages/deployments/contracts/contracts_forge/messaging/connectors/arbitrum/ArbitrumSpokeConnector.t.sol b/packages/deployments/contracts/contracts_forge/messaging/connectors/arbitrum/ArbitrumSpokeConnector.t.sol index 2aff11bac5..d1739bd17d 100644 --- a/packages/deployments/contracts/contracts_forge/messaging/connectors/arbitrum/ArbitrumSpokeConnector.t.sol +++ b/packages/deployments/contracts/contracts_forge/messaging/connectors/arbitrum/ArbitrumSpokeConnector.t.sol @@ -38,22 +38,22 @@ contract ArbitrumSpokeConnectorTest is ConnectorHelper { abi.encode(_alias) ); - _l2Connector = payable( - address( - new ArbitrumSpokeConnector( - _l2Domain, - _l1Domain, - _amb, - _rootManager, - _l1Connector, - _processGas, - _reserveGas, - 0, // uint256 _delayBlocks - _merkle, - address(1) // watcher manager - ) - ) - ); + SpokeConnector.ConstructorParams memory _baseParams = SpokeConnector.ConstructorParams({ + domain: _l2Domain, + mirrorDomain: _l1Domain, + amb: _amb, + rootManager: _rootManager, + mirrorConnector: _l1Connector, + processGas: _processGas, + reserveGas: _reserveGas, + delayBlocks: 0, + merkle: _merkle, + watcherManager: address(1), + minDisputeBlocks: _minDisputeBlocks, + disputeBlocks: _disputeBlocks + }); + + _l2Connector = payable(address(new ArbitrumSpokeConnector(_baseParams))); // Make sure our mocked mapL1SenderContractAddressToL2Alias was called and alias // address was set correctly. diff --git a/packages/deployments/contracts/contracts_forge/messaging/connectors/gnosis/GnosisSpokeConnector.t.sol b/packages/deployments/contracts/contracts_forge/messaging/connectors/gnosis/GnosisSpokeConnector.t.sol index 4459866fba..0f13928d59 100644 --- a/packages/deployments/contracts/contracts_forge/messaging/connectors/gnosis/GnosisSpokeConnector.t.sol +++ b/packages/deployments/contracts/contracts_forge/messaging/connectors/gnosis/GnosisSpokeConnector.t.sol @@ -15,6 +15,8 @@ contract GnosisSpokeConnectorTest is ConnectorHelper { uint256 _mirrorChainId = 1238786754; uint256 _chainId = 123213; + // ============ Storage ============ + function setUp() public { // Allow future contract mock vm.etch(_amb, new bytes(0x42)); @@ -24,24 +26,23 @@ contract GnosisSpokeConnectorTest is ConnectorHelper { _merkle = address(new MerkleTreeManager()); _l1Connector = payable(address(123123)); - _l2Connector = payable( - address( - new GnosisSpokeConnector( - _l2Domain, - _l1Domain, - _amb, - _rootManager, - _l1Connector, - _processGas, - _reserveGas, - 0, // uint256 _delayBlocks - _merkle, - address(1), // watcher manager - _gasCap, - _mirrorChainId - ) - ) - ); + + SpokeConnector.ConstructorParams memory _baseParams = SpokeConnector.ConstructorParams({ + domain: _l2Domain, + mirrorDomain: _l1Domain, + amb: _amb, + rootManager: _rootManager, + mirrorConnector: _l1Connector, + processGas: _processGas, + reserveGas: _reserveGas, + delayBlocks: 0, + merkle: _merkle, + watcherManager: address(1), + minDisputeBlocks: _minDisputeBlocks, + disputeBlocks: _disputeBlocks + }); + + _l2Connector = payable(address(new GnosisSpokeConnector(_baseParams, _gasCap, _mirrorChainId))); } // ============ Utils ============ diff --git a/packages/deployments/contracts/contracts_forge/messaging/connectors/mainnet/MainnetSpokeConnector.t.sol b/packages/deployments/contracts/contracts_forge/messaging/connectors/mainnet/MainnetSpokeConnector.t.sol index 5e5abe6b2d..028ceefb08 100644 --- a/packages/deployments/contracts/contracts_forge/messaging/connectors/mainnet/MainnetSpokeConnector.t.sol +++ b/packages/deployments/contracts/contracts_forge/messaging/connectors/mainnet/MainnetSpokeConnector.t.sol @@ -8,8 +8,26 @@ import {MerkleTreeManager} from "../../../../contracts/messaging/MerkleTreeManag import "../../../utils/ConnectorHelper.sol"; -contract MainnetSpokeConnectorTest is ConnectorHelper { +contract MainnetSpokeConnectorForTest is MainnetSpokeConnector { + constructor(SpokeConnector.ConstructorParams memory _params) MainnetSpokeConnector(_params) {} + + function setOptimisticMode__forTest(bool _optimisticMode) public { + optimisticMode = _optimisticMode; + } + + function setProvenAggregateRoots__forTest(bytes32 _aggregateRoot, bool _isProven) public { + provenAggregateRoots[_aggregateRoot] = _isProven; + } + + function setPendingAggregateRoots__forTest(bytes32 _aggregateRoot, uint256 _block) public { + pendingAggregateRoots[_aggregateRoot] = _block; + } +} + +contract Base is ConnectorHelper { // ============ Events ============ + event ProposedRootFinalized(bytes32 aggregateRoot); + event PendingAggregateRootDeleted(bytes32 indexed aggregateRoot); // ============ Storage ============ bytes32 outboundRoot = bytes32("test"); @@ -20,58 +38,58 @@ contract MainnetSpokeConnectorTest is ConnectorHelper { _l2Connector = payable(address(123321123)); + SpokeConnector.ConstructorParams memory _baseParams = SpokeConnector.ConstructorParams({ + domain: _l1Domain, + mirrorDomain: _l2Domain, + amb: _amb, + rootManager: _rootManager, + mirrorConnector: _l2Connector, + processGas: _processGas, + reserveGas: _reserveGas, + delayBlocks: 0, + merkle: _merkle, + watcherManager: address(0), + minDisputeBlocks: _minDisputeBlocks, + disputeBlocks: _disputeBlocks + }); + // deploy - _l1Connector = payable( - address( - new MainnetSpokeConnector( - _l1Domain, - _l2Domain, - _amb, - _rootManager, - _l2Connector, - _processGas, - _reserveGas, - 0, // delay blocks - _merkle, - address(0) // watcher manager - ) - ) - ); - } - - // ============ Utils ============ - - // ============ MainnetSpokeConnector.verifySender ============ + _l1Connector = payable(address(new MainnetSpokeConnectorForTest(_baseParams))); + } +} + +// ============ Utils ============ + +contract MainnetSpokeConnector__verifySender is Base { function test_MainnetSpokeConnector__verifySender_shouldWorkIfTrue() public { address expected = address(this); - - assertTrue(MainnetSpokeConnector(_l1Connector).verifySender(expected)); + assertTrue(MainnetSpokeConnectorForTest(_l1Connector).verifySender(expected)); } function test_MainnetSpokeConnector__verifySender_shouldWorkIfFalse() public { address expected = address(234); - - assertEq(MainnetSpokeConnector(_l1Connector).verifySender(expected), false); + assertEq(MainnetSpokeConnectorForTest(_l1Connector).verifySender(expected), false); } +} - // ============ MainnetSpokeConnector.sendMessage ============ +contract MainnetSpokeConnector__sendMessage is Base { function test_MainnetSpokeConnector__sendMessage_fromRootManagerWorks() public { - bytes memory _data = abi.encode(MainnetSpokeConnector(_l1Connector).outboundRoot()); + bytes memory _data = abi.encode(MainnetSpokeConnectorForTest(_l1Connector).outboundRoot()); vm.expectEmit(true, true, true, true); emit MessageSent(_data, bytes(""), _rootManager); vm.prank(_rootManager); - MainnetSpokeConnector(_l1Connector).sendMessage(_data, bytes("")); + MainnetSpokeConnectorForTest(_l1Connector).sendMessage(_data, bytes("")); } function test_MainnetSpokeConnector__sendMessage_failsIfCallerNotRootManager() public { - bytes memory _data = abi.encode(MainnetSpokeConnector(_l1Connector).outboundRoot()); + bytes memory _data = abi.encode(MainnetSpokeConnectorForTest(_l1Connector).outboundRoot()); vm.expectRevert(bytes("!rootManager")); // called as NOT root manager - MainnetSpokeConnector(_l1Connector).sendMessage(_data, bytes("")); + MainnetSpokeConnectorForTest(_l1Connector).sendMessage(_data, bytes("")); } function test_MainnetSpokeConnector__sendMessage_failsIfNot32Bytes() public { @@ -81,14 +99,102 @@ contract MainnetSpokeConnectorTest is ConnectorHelper { vm.expectRevert(bytes("!length")); vm.prank(_rootManager); - MainnetSpokeConnector(_l1Connector).sendMessage(_data, bytes("")); + MainnetSpokeConnectorForTest(_l1Connector).sendMessage(_data, bytes("")); } +} - // ============ MainnetSpokeConnector.processMessage ============ +contract MainnetSpokeConnector__processMessage is Base { function test_MainnetSpokeConnector__processMessage_reverts() public { vm.expectRevert(Connector.Connector__processMessage_notUsed.selector); - vm.prank(_amb); - MainnetSpokeConnector(_l1Connector).processMessage(bytes("")); + MainnetSpokeConnectorForTest(_l1Connector).processMessage(bytes("")); + } +} + +contract MainnetSpokeConnector__saveAggregateRoot is Base { + function test_MainnetSpokeConnector__saveAggregateRoot_revertsIfRootIsZero() public { + bytes32 _emptyRoot = bytes32(""); + MainnetSpokeConnectorForTest(_l1Connector).setOptimisticMode__forTest(true); + MainnetSpokeConnectorForTest(_l1Connector).setProvenAggregateRoots__forTest(_emptyRoot, false); + vm.expectRevert(MainnetSpokeConnector.MainnetSpokeConnector_saveAggregateRoot__EmptyRoot.selector); + vm.prank(_rootManager); + MainnetSpokeConnectorForTest(_l1Connector).saveAggregateRoot(_emptyRoot); + } + + function test_MainnetSpokeConnector__saveAggregateRoot_revertsIfOptimisticModeOff(bytes32 _aggregateRoot) public { + MainnetSpokeConnectorForTest(_l1Connector).setOptimisticMode__forTest(false); + vm.assume(_aggregateRoot != 0); + vm.expectRevert(MainnetSpokeConnector.MainnetSpokeConnector_saveAggregateRoot__OnlyOptimisticMode.selector); + vm.prank(_rootManager); + MainnetSpokeConnectorForTest(_l1Connector).saveAggregateRoot(_aggregateRoot); + } + + function test_MainnetSpokeConnector__saveAggregateRoot_revertsIfRootManagerIsNotCaller( + address _stranger, + bytes32 _aggregateRoot + ) public { + vm.assume(_stranger != _rootManager); + vm.assume(_aggregateRoot != 0); + MainnetSpokeConnectorForTest(_l1Connector).setOptimisticMode__forTest(true); + vm.expectRevert(MainnetSpokeConnector.MainnetSpokeConnector_saveAggregateRoot__CallerIsNotRootManager.selector); + vm.prank(_stranger); + MainnetSpokeConnectorForTest(_l1Connector).saveAggregateRoot(_aggregateRoot); + } + + function test_MainnetSpokeConnector__saveAggregateRoot_revertsIfRootAlreadyProven(bytes32 _aggregateRoot) public { + vm.assume(_aggregateRoot != 0); + MainnetSpokeConnectorForTest(_l1Connector).setOptimisticMode__forTest(true); + MainnetSpokeConnectorForTest(_l1Connector).setProvenAggregateRoots__forTest(_aggregateRoot, true); + vm.expectRevert(MainnetSpokeConnector.MainnetSpokeConnector_saveAggregateRoot__RootAlreadyProven.selector); + vm.prank(_rootManager); + MainnetSpokeConnectorForTest(_l1Connector).saveAggregateRoot(_aggregateRoot); + } + + function test_MainnetSpokeConnector__saveAggregateRoot_savesRootAndEmitsEvent(bytes32 _aggregateRoot) public { + vm.assume(_aggregateRoot != 0); + MainnetSpokeConnectorForTest(_l1Connector).setOptimisticMode__forTest(true); + MainnetSpokeConnectorForTest(_l1Connector).setProvenAggregateRoots__forTest(_aggregateRoot, false); + vm.expectEmit(true, true, true, true); + emit ProposedRootFinalized(_aggregateRoot); + vm.prank(_rootManager); + MainnetSpokeConnectorForTest(_l1Connector).saveAggregateRoot(_aggregateRoot); + assertEq(MainnetSpokeConnectorForTest(_l1Connector).provenAggregateRoots(_aggregateRoot), true); + } + + function test_MainnetSpokeConnector__saveAggregateRoot_deletesPendingRootAndEmitsEvent( + bytes32 _aggregateRoot, + uint256 _block + ) public { + vm.assume(_aggregateRoot != 0); + vm.assume(_block != 0); + MainnetSpokeConnectorForTest(_l1Connector).setOptimisticMode__forTest(true); + MainnetSpokeConnectorForTest(_l1Connector).setProvenAggregateRoots__forTest(_aggregateRoot, false); + MainnetSpokeConnectorForTest(_l1Connector).setPendingAggregateRoots__forTest(_aggregateRoot, _block); + vm.expectEmit(true, true, true, true); + emit PendingAggregateRootDeleted(_aggregateRoot); + vm.prank(_rootManager); + MainnetSpokeConnectorForTest(_l1Connector).saveAggregateRoot(_aggregateRoot); + assertEq(MainnetSpokeConnectorForTest(_l1Connector).pendingAggregateRoots(_aggregateRoot), 0); + } +} + +contract MainnetSpokeConnector__proposeAggregateRoot is Base { + function test_MainnetSpokeConnector__proposeAggregateRoot_reverts( + bytes32 _aggregateRoot, + uint256 _rootTimestamp + ) public { + vm.expectRevert(MainnetSpokeConnector.MainnetSpokeConnector_proposeAggregateRoot__DeprecatedInHubDomain.selector); + MainnetSpokeConnectorForTest(_l1Connector).proposeAggregateRoot(_aggregateRoot, _rootTimestamp); + } +} + +contract MainnetSpokeConnector__finalize is Base { + function test_MainnetSpokeConnector__finalize_reverts( + bytes32 _aggregateRoot, + uint256 _rootTimestamp, + uint256 _endOfDispute + ) public { + vm.expectRevert(MainnetSpokeConnector.MainnetSpokeConnector_finalize__DeprecatedInHubDomain.selector); + MainnetSpokeConnectorForTest(_l1Connector).finalize(_aggregateRoot, _rootTimestamp, _endOfDispute); } } diff --git a/packages/deployments/contracts/contracts_forge/messaging/connectors/multichain/MultichainSpokeConnector.t.sol b/packages/deployments/contracts/contracts_forge/messaging/connectors/multichain/MultichainSpokeConnector.t.sol index 9be6e07951..86cef4dceb 100644 --- a/packages/deployments/contracts/contracts_forge/messaging/connectors/multichain/MultichainSpokeConnector.t.sol +++ b/packages/deployments/contracts/contracts_forge/messaging/connectors/multichain/MultichainSpokeConnector.t.sol @@ -31,26 +31,24 @@ contract MultichainSpokeConnectorTest is ConnectorHelper { _merkle = address(new MerkleTreeManager()); + SpokeConnector.ConstructorParams memory _baseParams = SpokeConnector.ConstructorParams({ + domain: _l2Domain, + mirrorDomain: _l1Domain, + amb: _amb, + rootManager: _rootManager, + mirrorConnector: _l1Connector, + processGas: _processGas, + reserveGas: _reserveGas, + delayBlocks: 0, + merkle: _merkle, + watcherManager: address(1), + minDisputeBlocks: _minDisputeBlocks, + disputeBlocks: _disputeBlocks + }); + // Deploy vm.prank(_owner); - _l2Connector = payable( - address( - new MultichainSpokeConnector( - _l2Domain, - _l1Domain, - _amb, - _rootManager, - _l1Connector, - _processGas, - _reserveGas, - 0, // uint256 _delayBlocks - _merkle, - address(1), // watcher manager - _chainIdMainnet, - _gasCap - ) - ) - ); + _l2Connector = payable(address(new MultichainSpokeConnector(_baseParams, _chainIdMainnet, _gasCap))); assertEq(_owner, MultichainSpokeConnector(_l2Connector).owner()); } @@ -150,9 +148,10 @@ contract MultichainSpokeConnectorTest is ConnectorHelper { } // message coming from a wrong sender on the origin chain to L2 - function test_MultichainSpokeConnector__anyExecute_revertIfWrongMirror(address _wrongMirror, bytes calldata _data) - public - { + function test_MultichainSpokeConnector__anyExecute_revertIfWrongMirror( + address _wrongMirror, + bytes calldata _data + ) public { vm.assume(_wrongMirror != _l1Connector); // Mock the call to the executor, to retrieve the context @@ -167,9 +166,10 @@ contract MultichainSpokeConnectorTest is ConnectorHelper { } // message coming from a wrong chain to L2 - function test_MultichainSpokeConnector__anyExecute_revertIfWrongOriginId(uint256 _wrongId, bytes calldata _data) - public - { + function test_MultichainSpokeConnector__anyExecute_revertIfWrongOriginId( + uint256 _wrongId, + bytes calldata _data + ) public { vm.assume(_wrongId != _chainIdMainnet); // Mock the call to the executor, to retrieve the context @@ -234,9 +234,10 @@ contract MultichainSpokeConnectorTest is ConnectorHelper { } // reverse if sender != executor - function test_MultichainSpokeConnector_verifySender_revertIfSenderIsNotExecutor(address _from, address _wrongExecutor) - public - { + function test_MultichainSpokeConnector_verifySender_revertIfSenderIsNotExecutor( + address _from, + address _wrongExecutor + ) public { vm.assume(_wrongExecutor != _executor); // Mock the call to the executor, to retrieve the context diff --git a/packages/deployments/contracts/contracts_forge/messaging/connectors/optimism/OptimismSpokeConnector.t.sol b/packages/deployments/contracts/contracts_forge/messaging/connectors/optimism/OptimismSpokeConnector.t.sol index 186e345d4e..fc94ca4c77 100644 --- a/packages/deployments/contracts/contracts_forge/messaging/connectors/optimism/OptimismSpokeConnector.t.sol +++ b/packages/deployments/contracts/contracts_forge/messaging/connectors/optimism/OptimismSpokeConnector.t.sol @@ -23,24 +23,23 @@ contract OptimismSpokeConnectorTest is ConnectorHelper { _merkle = address(new MerkleTreeManager()); + SpokeConnector.ConstructorParams memory _baseParams = SpokeConnector.ConstructorParams({ + domain: _l2Domain, + mirrorDomain: _l1Domain, + amb: _amb, + rootManager: _rootManager, + mirrorConnector: _l1Connector, + processGas: _processGas, + reserveGas: _reserveGas, + delayBlocks: 0, + merkle: _merkle, + watcherManager: address(0), + minDisputeBlocks: _minDisputeBlocks, + disputeBlocks: _disputeBlocks + }); + // deploy - _l2Connector = payable( - address( - new OptimismSpokeConnector( - _l2Domain, - _l1Domain, - _amb, - _rootManager, - _l1Connector, - _processGas, - _reserveGas, - 0, // delay blocks - _merkle, - address(0), // watcher manager - _gasCap - ) - ) - ); + _l2Connector = payable(address(new OptimismSpokeConnector(_baseParams, _gasCap))); } // ============ Utils ============ diff --git a/packages/deployments/contracts/contracts_forge/messaging/connectors/polygon/PolygonSpokeConnector.t.sol b/packages/deployments/contracts/contracts_forge/messaging/connectors/polygon/PolygonSpokeConnector.t.sol index e86141953d..21f1a645db 100644 --- a/packages/deployments/contracts/contracts_forge/messaging/connectors/polygon/PolygonSpokeConnector.t.sol +++ b/packages/deployments/contracts/contracts_forge/messaging/connectors/polygon/PolygonSpokeConnector.t.sol @@ -22,22 +22,22 @@ contract PolygonSpokeConnectorTest is ConnectorHelper { _merkle = address(new MerkleTreeManager()); - _l2Connector = payable( - address( - new PolygonSpokeConnector( - _l2Domain, - _l1Domain, - _amb, - _rootManager, - address(0), - _processGas, - _reserveGas, - 0, // uint256 _delayBlocks - _merkle, - address(1) // watcher manager - ) - ) - ); + SpokeConnector.ConstructorParams memory _baseParams = SpokeConnector.ConstructorParams({ + domain: _l2Domain, + mirrorDomain: _l1Domain, + amb: _amb, + rootManager: _rootManager, + mirrorConnector: address(0), + processGas: _processGas, + reserveGas: _reserveGas, + delayBlocks: 0, + merkle: _merkle, + watcherManager: address(1), + minDisputeBlocks: _minDisputeBlocks, + disputeBlocks: _disputeBlocks + }); + + _l2Connector = payable(address(new PolygonSpokeConnector(_baseParams))); } // ============ Utils ============ diff --git a/packages/deployments/contracts/contracts_forge/messaging/connectors/wormhole/WormholeSpokeConnector.t.sol b/packages/deployments/contracts/contracts_forge/messaging/connectors/wormhole/WormholeSpokeConnector.t.sol index 3c8978083c..ade9fcdbb1 100644 --- a/packages/deployments/contracts/contracts_forge/messaging/connectors/wormhole/WormholeSpokeConnector.t.sol +++ b/packages/deployments/contracts/contracts_forge/messaging/connectors/wormhole/WormholeSpokeConnector.t.sol @@ -30,26 +30,24 @@ contract WormholeSpokeConnectorTest is ConnectorHelper { _l1Connector = payable(address(bytes20(keccak256("_l1Connector")))); _merkle = address(new MerkleTreeManager()); + SpokeConnector.ConstructorParams memory _baseParams = SpokeConnector.ConstructorParams({ + domain: _l1Domain, + mirrorDomain: _l2Domain, + amb: _amb, + rootManager: _rootManager, + mirrorConnector: _l1Connector, + processGas: _processGas, + reserveGas: _reserveGas, + delayBlocks: 0, + merkle: _merkle, + watcherManager: address(0), + minDisputeBlocks: _minDisputeBlocks, + disputeBlocks: _disputeBlocks + }); + // Deploy vm.prank(_owner); - _l2Connector = payable( - address( - new WormholeSpokeConnector( - _l1Domain, - _l2Domain, - _amb, - _rootManager, - _l1Connector, - _processGas, - _reserveGas, - 0, // uint256 _delayBlocks - _merkle, - address(1), // watcher manager - _gasCapL1, - _chainIdL1 - ) - ) - ); + _l2Connector = payable(address(new WormholeSpokeConnector(_baseParams, _gasCapL1, _chainIdL1))); } // ============ Utils ============ diff --git a/packages/deployments/contracts/contracts_forge/utils/ConnectorHelper.sol b/packages/deployments/contracts/contracts_forge/utils/ConnectorHelper.sol index 2564ac8f7f..1c55f915aa 100644 --- a/packages/deployments/contracts/contracts_forge/utils/ConnectorHelper.sol +++ b/packages/deployments/contracts/contracts_forge/utils/ConnectorHelper.sol @@ -36,4 +36,7 @@ contract ConnectorHelper is ForgeHelper { address payable _l1Connector; address payable _l2Connector; address _merkle; + + uint256 _minDisputeBlocks = 125; + uint256 _disputeBlocks = 150; } diff --git a/packages/deployments/contracts/contracts_forge/utils/Mock.sol b/packages/deployments/contracts/contracts_forge/utils/Mock.sol index d41d393f62..4e94c68851 100644 --- a/packages/deployments/contracts/contracts_forge/utils/Mock.sol +++ b/packages/deployments/contracts/contracts_forge/utils/Mock.sol @@ -215,32 +215,7 @@ contract MockSpokeConnector is SpokeConnector { // bytes32 public aggregateRoot; // uint32 public mirrorDomain; - constructor( - uint32 _domain, - uint32 _mirrorDomain, - address _amb, - address _rootManager, - address _merkle, - address _mirrorConnector, - uint256 _processGas, - uint256 _reserveGas, - uint256 _delayBlocks, - address _watcherManager - ) - ProposedOwnable() - SpokeConnector( - _domain, - _mirrorDomain, - _amb, - _rootManager, - _mirrorConnector, - _processGas, - _reserveGas, - _delayBlocks, - _merkle, - _watcherManager - ) - { + constructor(ConstructorParams memory _baseSpokeParams) ProposedOwnable() SpokeConnector(_baseSpokeParams) { _setOwner(msg.sender); verified = true; // mirrorDomain = _mirrorDomain; @@ -254,6 +229,30 @@ contract MockSpokeConnector is SpokeConnector { updatesAggregate = _updatesAggregate; } + function setAllowlistedProposer(address _proposer, bool _isProposer) public { + allowlistedProposers[_proposer] = _isProposer; + } + + function setOptimisticMode(bool _mode) public { + optimisticMode = _mode; + } + + function setProposedAggregateRootHash(bytes32 _proposedAggregateRootHash) public { + proposedAggregateRootHash = _proposedAggregateRootHash; + } + + function setPendingAggregateRoot(bytes32 _newRoot, uint256 _blockNumber) public { + pendingAggregateRoots[_newRoot] = _blockNumber; + } + + function setProvenAggregateRoot(bytes32 _newRoot, bool _proven) public { + provenAggregateRoots[_newRoot] = _proven; + } + + function receiveAggregateRootForTest(bytes32 _newRoot) public { + receiveAggregateRoot(_newRoot); + } + function _sendMessage(bytes memory _data, bytes memory _encodedData) internal override { lastOutbound = keccak256(_data); }