diff --git a/synd-contracts/src/SyndicateCombinedSequencingChain.sol b/synd-contracts/src/SyndicateCombinedSequencingChain.sol new file mode 100644 index 00000000..5219fe78 --- /dev/null +++ b/synd-contracts/src/SyndicateCombinedSequencingChain.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.28; + +import {SyndicateSequencingChainBase} from "./SyndicateSequencingChainBase.sol"; + +/// @title SyndicateCombinedSequencingChain +/// @notice Sequencing chain with an accumulator for trustless TEE module state proving +/// @dev Extends SyndicateSequencingChainBase by maintaining a hash chain accumulator +/// of all processed transactions. This enables the TEE module to trustlessly prove +/// the appchain state by verifying against the on-chain accumulator. +contract SyndicateCombinedSequencingChain is SyndicateSequencingChainBase { + /// @notice The accumulator for sequencing data - a hash chain of all transactions + bytes32[] public sequencingAccumulator; + + /// @notice Constructs the SyndicateCombinedSequencingChain contract. + /// @param _appchainId The ID of the App chain that this contract is sequencing transactions for. + constructor(uint256 _appchainId) SyndicateSequencingChainBase(_appchainId) {} + + /// @notice Updates the sequencingAccumulator and emits the TransactionProcessed event + /// @dev Overrides base implementation to add accumulator tracking before emitting event + /// @param transaction The encoded transaction data + function _transactionProcessed(bytes memory transaction) internal override { + uint256 count = sequencingAccumulator.length; + bytes32 prevAcc = count > 0 ? sequencingAccumulator[count - 1] : bytes32(0); + sequencingAccumulator.push(keccak256(abi.encodePacked(prevAcc, transaction))); + emit TransactionProcessed(msg.sender, transaction); + } +} diff --git a/synd-contracts/src/SyndicateSequencingChain.sol b/synd-contracts/src/SyndicateSequencingChain.sol index 579d6f65..26e5f170 100644 --- a/synd-contracts/src/SyndicateSequencingChain.sol +++ b/synd-contracts/src/SyndicateSequencingChain.sol @@ -1,160 +1,14 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.28; -import {SequencingModuleChecker} from "./SequencingModuleChecker.sol"; -import {GasCounter} from "./staking/GasCounter.sol"; -import {ISyndicateSequencingChain} from "./interfaces/ISyndicateSequencingChain.sol"; - -uint8 constant L2MessageType_SignedTx = 4; // a regular signed transaction +import {SyndicateSequencingChainBase} from "./SyndicateSequencingChainBase.sol"; /// @title SyndicateSequencingChain -/// @notice Core contract for transaction sequencing using Syndicate's "secure by module design" architecture -/// -/// @dev ARCHITECTURAL DESIGN - tx.origin USAGE BY DESIGN: -/// This contract intentionally uses tx.origin alongside msg.sender to enable sophisticated middleware patterns: -/// -/// USE CASES ENABLED: -/// • ATOMIC CROSS-CHAIN SEQUENCING: AtomicSequencer coordinating multiple chains -/// • TRUSTED MIDDLEWARE: Third-party contracts adding logic layers -/// • BATCH PROCESSING: Routing contracts that aggregate transactions -/// • COMPLEX AUTHORIZATION: Modules that consider both caller and originator -/// -/// SECURITY MODEL - "SECURE BY MODULE DESIGN": -/// Security is NOT enforced by this contract, but by developer-implemented permission modules: -/// -/// ┌─────────────────────────────────────────────────────────────────────────┐ -/// │ RESPONSIBILITY DISTRIBUTION: │ -/// ├─────────────────────────────────────────────────────────────────────────┤ -/// │ SyndicateSequencingChain: Routes to permission modules │ -/// │ PermissionModule (Dev): Implements authorization logic │ -/// │ Module Developer: MUST validate both msg.sender and tx.origin properly │ -/// └─────────────────────────────────────────────────────────────────────────┘ -/// -/// @dev Transaction Lifecycle: -/// 1. Transaction is submitted via processTransaction or processTransactionsBulk -/// 2. onlyWhenAllowed modifier passes both msg.sender AND tx.origin to SequencingModuleChecker -/// 3. SequencingModuleChecker delegates to the configured permissionRequirementModule -/// 4. Permission module evaluates BOTH addresses using custom logic (developer responsibility) -/// 5. If allowed, TransactionProcessed event is emitted for off-chain processing -/// 6. External systems observe events to execute transactions on the application chain -/// -/// This event-based design provides scalability and gas efficiency while maintaining security -/// through modular, developer-controlled permission systems. -contract SyndicateSequencingChain is SequencingModuleChecker, ISyndicateSequencingChain, GasCounter { - error NoTxData(); - error TransactionOrSenderNotAllowed(); - error GasTrackingAlreadyEnabled(); - error GasTrackingAlreadyDisabled(); - - /// @notice Emitted when a new transaction is processed - /// @param sender The address that submitted the transaction - /// @param data The transaction data that was processed - event TransactionProcessed(address indexed sender, bytes data); - - /// @notice Emitted when the emissions receiver is updated - /// @param oldReceiver The previous emissions receiver address - /// @param newReceiver The new emissions receiver address - event EmissionsReceiverUpdated(address indexed oldReceiver, address indexed newReceiver); - - uint256 public immutable appchainId; - - /// @notice The address that receives emissions for this sequencing chain - address public emissionsReceiver; - +/// @notice Standard sequencing chain contract that emits events for off-chain processing +/// @dev Inherits all functionality from SyndicateSequencingChainBase. +/// Uses the default _transactionProcessed implementation which simply emits TransactionProcessed events. +contract SyndicateSequencingChain is SyndicateSequencingChainBase { /// @notice Constructs the SyndicateSequencingChain contract. /// @param _appchainId The ID of the App chain that this contract is sequencing transactions for. - //#olympix-ignore-missing-revert-reason-tests - constructor(uint256 _appchainId) SequencingModuleChecker() { - // chain id zero has no replay protection: https://eips.ethereum.org/EIPS/eip-3788 - require(_appchainId != 0, "App chain ID cannot be 0"); - appchainId = _appchainId; - } - - function encodeTransaction(bytes calldata data) public pure returns (bytes memory) { - return abi.encodePacked(L2MessageType_SignedTx, data); - } - - /// @notice Process a signed transaction. - /// @param data Transaction data - //#olympix-ignore-required-tx-origin - function processTransaction(bytes calldata data) external trackGasUsage { - require(data.length > 0, NoTxData()); - - bytes memory transaction = encodeTransaction(data); - require(isAllowed(msg.sender, tx.origin, transaction), TransactionOrSenderNotAllowed()); - emit TransactionProcessed(msg.sender, transaction); - } - - /// @notice Processes multiple signed transactions in bulk. - /// @param data An array of transaction data. - //#olympix-ignore - function processTransactionsBulk(bytes[] calldata data) external trackGasUsage { - uint256 dataCount = data.length; - require(dataCount > 0, NoTxData()); - - // Process all transactions - uint256 i; - for (i = 0; i < dataCount; i++) { - require(data[i].length > 0, NoTxData()); - bytes memory transaction = encodeTransaction(data[i]); - bool isAllowed = isAllowed(msg.sender, tx.origin, transaction); //#olympix-ignore-any-tx-origin - if (isAllowed) { - // only emit the event if the transaction is allowed - emit TransactionProcessed(msg.sender, transaction); - } - } - } - - /*////////////////////////////////////////////////////////////// - EMISSIONS RECEIVER ADMIN FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - /// @notice Set the emissions receiver address - /// @dev Only callable by the contract owner - /// @param _emissionsReceiver The address to receive emissions - function setEmissionsReceiver(address _emissionsReceiver) external onlyOwner { - address oldReceiver = emissionsReceiver; - emissionsReceiver = _emissionsReceiver; - if (emissionsReceiver != address(0)) { - emit EmissionsReceiverUpdated(oldReceiver, _emissionsReceiver); - } else { - emit EmissionsReceiverUpdated(oldReceiver, owner()); - } - } - - /// @notice Get the effective emissions receiver address - /// @dev Returns emissionsReceiver if set, otherwise returns the contract owner - /// @return The address that should receive emissions - function getEmissionsReceiver() external view returns (address) { - return emissionsReceiver == address(0) ? owner() : emissionsReceiver; - } - - /// @notice Override transferOwnership to emit EmissionsReceiverUpdated event when appropriate - /// @dev When emissionsReceiver is not explicitly set (address(0)), transferring ownership - /// effectively changes the emissions receiver, so we emit the event for transparency - /// @param newOwner The address of the new owner - function transferOwnership(address newOwner) public override onlyOwner { - if (emissionsReceiver == address(0)) { - emit EmissionsReceiverUpdated(owner(), newOwner); - } - super.transferOwnership(newOwner); - } - - /*////////////////////////////////////////////////////////////// - GAS TRACKING ADMIN FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - /// @notice Disable gas tracking if needed - /// @dev Only callable by the contract owner - function disableGasTracking() external onlyOwner { - require(!gasTrackingDisabled, GasTrackingAlreadyDisabled()); - gasTrackingDisabled = true; - } - - /// @notice Enable gas tracking - /// @dev Only callable by the contract owner - function enableGasTracking() external onlyOwner { - require(gasTrackingDisabled, GasTrackingAlreadyEnabled()); - gasTrackingDisabled = false; - } + constructor(uint256 _appchainId) SyndicateSequencingChainBase(_appchainId) {} } diff --git a/synd-contracts/src/SyndicateSequencingChainBase.sol b/synd-contracts/src/SyndicateSequencingChainBase.sol new file mode 100644 index 00000000..48353204 --- /dev/null +++ b/synd-contracts/src/SyndicateSequencingChainBase.sol @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.28; + +import {SequencingModuleChecker} from "./SequencingModuleChecker.sol"; +import {GasCounter} from "./staking/GasCounter.sol"; +import {ISyndicateSequencingChain} from "./interfaces/ISyndicateSequencingChain.sol"; + +uint8 constant L2MessageType_SignedTx = 4; // a regular signed transaction + +/// @title SyndicateSequencingChainBase +/// @notice Abstract base contract for transaction sequencing using Syndicate's "secure by module design" architecture +/// +/// @dev ARCHITECTURAL DESIGN - tx.origin USAGE BY DESIGN: +/// This contract intentionally uses tx.origin alongside msg.sender to enable sophisticated middleware patterns: +/// +/// USE CASES ENABLED: +/// * ATOMIC CROSS-CHAIN SEQUENCING: AtomicSequencer coordinating multiple chains +/// * TRUSTED MIDDLEWARE: Third-party contracts adding logic layers +/// * BATCH PROCESSING: Routing contracts that aggregate transactions +/// * COMPLEX AUTHORIZATION: Modules that consider both caller and originator +/// +/// SECURITY MODEL - "SECURE BY MODULE DESIGN": +/// Security is NOT enforced by this contract, but by developer-implemented permission modules: +/// +/// +-----------------------------------------------------------------------------+ +/// | RESPONSIBILITY DISTRIBUTION: | +/// +-----------------------------------------------------------------------------+ +/// | SyndicateSequencingChain: Routes to permission modules | +/// | PermissionModule (Dev): Implements authorization logic | +/// | Module Developer: MUST validate both msg.sender and tx.origin properly | +/// +-----------------------------------------------------------------------------+ +/// +/// @dev Transaction Lifecycle: +/// 1. Transaction is submitted via processTransaction or processTransactionsBulk +/// 2. onlyWhenAllowed modifier passes both msg.sender AND tx.origin to SequencingModuleChecker +/// 3. SequencingModuleChecker delegates to the configured permissionRequirementModule +/// 4. Permission module evaluates BOTH addresses using custom logic (developer responsibility) +/// 5. If allowed, _transactionProcessed is called (can be overridden by derived contracts) +/// 6. External systems observe events to execute transactions on the application chain +/// +/// This event-based design provides scalability and gas efficiency while maintaining security +/// through modular, developer-controlled permission systems. +abstract contract SyndicateSequencingChainBase is SequencingModuleChecker, ISyndicateSequencingChain, GasCounter { + error NoTxData(); + error TransactionOrSenderNotAllowed(); + error GasTrackingAlreadyEnabled(); + error GasTrackingAlreadyDisabled(); + + /// @notice Emitted when a new transaction is processed + /// @param sender The address that submitted the transaction + /// @param data The transaction data that was processed + event TransactionProcessed(address indexed sender, bytes data); + + /// @notice Emitted when the emissions receiver is updated + /// @param oldReceiver The previous emissions receiver address + /// @param newReceiver The new emissions receiver address + event EmissionsReceiverUpdated(address indexed oldReceiver, address indexed newReceiver); + + uint256 public immutable appchainId; + + /// @notice The address that receives emissions for this sequencing chain + address public emissionsReceiver; + + /// @notice Constructs the SyndicateSequencingChainBase contract. + /// @param _appchainId The ID of the App chain that this contract is sequencing transactions for. + //#olympix-ignore-missing-revert-reason-tests + constructor(uint256 _appchainId) SequencingModuleChecker() { + // chain id zero has no replay protection: https://eips.ethereum.org/EIPS/eip-3788 + require(_appchainId != 0, "App chain ID cannot be 0"); + appchainId = _appchainId; + } + + function encodeTransaction(bytes calldata data) public pure returns (bytes memory) { + return abi.encodePacked(L2MessageType_SignedTx, data); + } + + /// @notice Process a signed transaction. + /// @param data Transaction data + //#olympix-ignore-required-tx-origin + function processTransaction(bytes calldata data) external trackGasUsage { + require(data.length > 0, NoTxData()); + + bytes memory transaction = encodeTransaction(data); + require(isAllowed(msg.sender, tx.origin, transaction), TransactionOrSenderNotAllowed()); + _transactionProcessed(transaction); + } + + /// @notice Processes multiple signed transactions in bulk. + /// @param data An array of transaction data. + //#olympix-ignore + function processTransactionsBulk(bytes[] calldata data) external trackGasUsage { + uint256 dataCount = data.length; + require(dataCount > 0, NoTxData()); + + // Process all transactions + uint256 i; + for (i = 0; i < dataCount; i++) { + require(data[i].length > 0, NoTxData()); + bytes memory transaction = encodeTransaction(data[i]); + bool allowed = isAllowed(msg.sender, tx.origin, transaction); //#olympix-ignore-any-tx-origin + if (allowed) { + _transactionProcessed(transaction); + } + } + } + + /// @notice Hook called when a transaction is processed + /// @dev Override this in derived contracts to add custom behavior (e.g., accumulator updates) + /// @param transaction The encoded transaction data + function _transactionProcessed(bytes memory transaction) internal virtual { + emit TransactionProcessed(msg.sender, transaction); + } + + /*////////////////////////////////////////////////////////////// + EMISSIONS RECEIVER ADMIN FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @notice Set the emissions receiver address + /// @dev Only callable by the contract owner + /// @param _emissionsReceiver The address to receive emissions + function setEmissionsReceiver(address _emissionsReceiver) external onlyOwner { + address oldReceiver = emissionsReceiver; + emissionsReceiver = _emissionsReceiver; + if (emissionsReceiver != address(0)) { + emit EmissionsReceiverUpdated(oldReceiver, _emissionsReceiver); + } else { + emit EmissionsReceiverUpdated(oldReceiver, owner()); + } + } + + /// @notice Get the effective emissions receiver address + /// @dev Returns emissionsReceiver if set, otherwise returns the contract owner + /// @return The address that should receive emissions + function getEmissionsReceiver() external view returns (address) { + return emissionsReceiver == address(0) ? owner() : emissionsReceiver; + } + + /// @notice Override transferOwnership to emit EmissionsReceiverUpdated event when appropriate + /// @dev When emissionsReceiver is not explicitly set (address(0)), transferring ownership + /// effectively changes the emissions receiver, so we emit the event for transparency + /// @param newOwner The address of the new owner + function transferOwnership(address newOwner) public override onlyOwner { + if (emissionsReceiver == address(0)) { + emit EmissionsReceiverUpdated(owner(), newOwner); + } + super.transferOwnership(newOwner); + } + + /*////////////////////////////////////////////////////////////// + GAS TRACKING ADMIN FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @notice Disable gas tracking if needed + /// @dev Only callable by the contract owner + function disableGasTracking() external onlyOwner { + require(!gasTrackingDisabled, GasTrackingAlreadyDisabled()); + gasTrackingDisabled = true; + } + + /// @notice Enable gas tracking + /// @dev Only callable by the contract owner + function enableGasTracking() external onlyOwner { + require(gasTrackingDisabled, GasTrackingAlreadyEnabled()); + gasTrackingDisabled = false; + } +} diff --git a/synd-contracts/src/factory/SyndicateFactory.sol b/synd-contracts/src/factory/SyndicateFactory.sol index 216016c8..36528bc4 100644 --- a/synd-contracts/src/factory/SyndicateFactory.sol +++ b/synd-contracts/src/factory/SyndicateFactory.sol @@ -2,6 +2,8 @@ pragma solidity 0.8.28; import {SyndicateSequencingChain} from "../SyndicateSequencingChain.sol"; +import {SyndicateCombinedSequencingChain} from "../SyndicateCombinedSequencingChain.sol"; +import {SyndicateSequencingChainBase} from "../SyndicateSequencingChainBase.sol"; import {IRequirementModule} from "../interfaces/IRequirementModule.sol"; import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; @@ -24,12 +26,23 @@ contract SyndicateFactory is AccessControl, Pausable { uint256 indexed appchainId, address indexed sequencingChainAddress, address indexed permissionModuleAddress ); + /// @notice Emitted when a new SyndicateCombinedSequencingChain is created + event SyndicateCombinedSequencingChainCreated( + uint256 indexed appchainId, address indexed sequencingChainAddress, address indexed permissionModuleAddress + ); + /// @notice Emitted when namespace configuration is updated event NamespaceConfigUpdated(uint256 oldNamespacePrefix, uint256 newNamespacePrefix); /// @notice Emitted when a chain ID is manually marked as used event ChainIdManuallyMarked(uint256 indexed chainId); + /// @notice Chain type for deployment + enum ChainType { + Standard, + Combined + } + bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE"); error ZeroAddress(); @@ -55,20 +68,54 @@ contract SyndicateFactory is AccessControl, Pausable { nextAutoChainId = 1; } - /// @notice Creates a new SyndicateSequencingChain contract + /// @notice Creates a new SyndicateSequencingChain contract (standard, without accumulator) /// @param appchainId The app chain ID (0 for auto-increment) /// @param admin The admin address for the new chain /// @param permissionModule The pre-deployed permission module /// @param salt The salt for CREATE2 deployment /// @return sequencingChain The deployed sequencing chain address /// @return actualChainId The chain ID that was used - //#olympix-ignore-reentrancy-events function createSyndicateSequencingChain( uint256 appchainId, address admin, IRequirementModule permissionModule, bytes32 salt ) external whenNotPaused returns (address sequencingChain, uint256 actualChainId) { + return _createChain(ChainType.Standard, appchainId, admin, permissionModule, salt); + } + + /// @notice Creates a new SyndicateCombinedSequencingChain contract (with accumulator for TEE proving) + /// @param appchainId The app chain ID (0 for auto-increment) + /// @param admin The admin address for the new chain + /// @param permissionModule The pre-deployed permission module + /// @param salt The salt for CREATE2 deployment + /// @return sequencingChain The deployed sequencing chain address + /// @return actualChainId The chain ID that was used + function createCombinedSequencingChain( + uint256 appchainId, + address admin, + IRequirementModule permissionModule, + bytes32 salt + ) external whenNotPaused returns (address sequencingChain, uint256 actualChainId) { + return _createChain(ChainType.Combined, appchainId, admin, permissionModule, salt); + } + + /// @notice Internal function to create a sequencing chain of the specified type + /// @param chainType The type of chain to create (Standard or Combined) + /// @param appchainId The app chain ID (0 for auto-increment) + /// @param admin The admin address for the new chain + /// @param permissionModule The pre-deployed permission module + /// @param salt The salt for CREATE2 deployment + /// @return sequencingChain The deployed sequencing chain address + /// @return actualChainId The chain ID that was used + //#olympix-ignore-reentrancy-events + function _createChain( + ChainType chainType, + uint256 appchainId, + address admin, + IRequirementModule permissionModule, + bytes32 salt + ) internal returns (address sequencingChain, uint256 actualChainId) { if (admin == address(0) || address(permissionModule) == address(0)) { revert ZeroAddress(); } @@ -86,25 +133,31 @@ contract SyndicateFactory is AccessControl, Pausable { } // Deploy the sequencing chain using CREATE2 - bytes memory bytecode = getBytecode(actualChainId); + bytes memory bytecode = + chainType == ChainType.Standard ? getBytecode(actualChainId) : getCombinedBytecode(actualChainId); sequencingChain = Create2.deploy(0, salt, bytecode); // Store the mapping of appchain ID to contract address appchainContracts[actualChainId] = sequencingChain; chainIDs.push(actualChainId); - // Set sequencing module - SyndicateSequencingChain(sequencingChain).updateRequirementModule(address(permissionModule)); + // Set sequencing module (both types inherit from SyndicateSequencingChainBase) + SyndicateSequencingChainBase(sequencingChain).updateRequirementModule(address(permissionModule)); // Transfer owner Ownable(sequencingChain).transferOwnership(admin); - emit SyndicateSequencingChainCreated(actualChainId, sequencingChain, address(permissionModule)); + // Emit appropriate event + if (chainType == ChainType.Standard) { + emit SyndicateSequencingChainCreated(actualChainId, sequencingChain, address(permissionModule)); + } else { + emit SyndicateCombinedSequencingChainCreated(actualChainId, sequencingChain, address(permissionModule)); + } return (sequencingChain, actualChainId); } - /// @notice Computes the address where a sequencing chain will be deployed + /// @notice Computes the address where a standard sequencing chain will be deployed /// @param salt The salt for CREATE2 deployment /// @param chainId The chain ID /// @return The computed address @@ -112,11 +165,26 @@ contract SyndicateFactory is AccessControl, Pausable { return Create2.computeAddress(salt, keccak256(getBytecode(chainId))); } - /// @notice Returns the bytecode for deploying a SyndicateSequencingChain + /// @notice Computes the address where a combined sequencing chain will be deployed + /// @param salt The salt for CREATE2 deployment + /// @param chainId The chain ID + /// @return The computed address + function computeCombinedSequencingChainAddress(bytes32 salt, uint256 chainId) external view returns (address) { + return Create2.computeAddress(salt, keccak256(getCombinedBytecode(chainId))); + } + + /// @notice Returns the bytecode for deploying a SyndicateSequencingChain (standard) /// @param chainId The chain ID /// @return The bytecode with constructor parameters function getBytecode(uint256 chainId) public pure returns (bytes memory) { - return abi.encodePacked(type(SyndicateSequencingChain).creationCode, abi.encode(chainId, false)); + return abi.encodePacked(type(SyndicateSequencingChain).creationCode, abi.encode(chainId)); + } + + /// @notice Returns the bytecode for deploying a SyndicateCombinedSequencingChain (with accumulator) + /// @param chainId The chain ID + /// @return The bytecode with constructor parameters + function getCombinedBytecode(uint256 chainId) public pure returns (bytes memory) { + return abi.encodePacked(type(SyndicateCombinedSequencingChain).creationCode, abi.encode(chainId)); } /// @notice Get the next auto-generated chain ID diff --git a/synd-contracts/test/SyndicateSequencingChainTest.t.sol b/synd-contracts/test/SyndicateSequencingChainTest.t.sol index e54b0f79..c6a4285a 100644 --- a/synd-contracts/test/SyndicateSequencingChainTest.t.sol +++ b/synd-contracts/test/SyndicateSequencingChainTest.t.sol @@ -1,13 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.28; -import {SyndicateSequencingChain, SequencingModuleChecker} from "src/SyndicateSequencingChain.sol"; +import {SyndicateSequencingChain} from "src/SyndicateSequencingChain.sol"; +import {SyndicateCombinedSequencingChain} from "src/SyndicateCombinedSequencingChain.sol"; +import {SyndicateSequencingChainBase, L2MessageType_SignedTx} from "src/SyndicateSequencingChainBase.sol"; +import {SequencingModuleChecker} from "src/SequencingModuleChecker.sol"; import {SyndicateFactory} from "src/factory/SyndicateFactory.sol"; -import { - SyndicateSequencingChain, - L2MessageType_SignedTx, - SequencingModuleChecker -} from "src/SyndicateSequencingChain.sol"; import {RequireAndModule} from "src/requirement-modules/RequireAndModule.sol"; import {RequireOrModule} from "src/requirement-modules/RequireOrModule.sol"; import {IPermissionModule} from "src/interfaces/IPermissionModule.sol"; @@ -46,6 +44,7 @@ contract DirectMockModule is IPermissionModule { contract SyndicateSequencingChainTestSetUp is Test { SyndicateSequencingChain public chain; + SyndicateCombinedSequencingChain public combinedChain; SyndicateFactory public factory; RequireAndModule public permissionModule; RequireOrModule public permissionModuleAny; @@ -62,6 +61,15 @@ contract SyndicateSequencingChainTestSetUp is Test { return SyndicateSequencingChain(chainAddress); } + function deployCombinedChain(RequireAndModule _permissionModule) public returns (SyndicateCombinedSequencingChain) { + uint256 appchainId = 10042002; + vm.startPrank(admin); + SyndicateCombinedSequencingChain newChain = new SyndicateCombinedSequencingChain(appchainId); + newChain.updateRequirementModule(address(_permissionModule)); + vm.stopPrank(); + return newChain; + } + function setUp() public virtual { // Warp to START_TIMESTAMP to avoid underflow in epoch calculations vm.warp(1754089200); // START_TIMESTAMP from EpochTracker.sol @@ -70,6 +78,7 @@ contract SyndicateSequencingChainTestSetUp is Test { permissionModule = new RequireAndModule(admin); permissionModuleAny = new RequireOrModule(admin); chain = deployFromFactory(permissionModule); + combinedChain = deployCombinedChain(permissionModule); } } @@ -82,11 +91,15 @@ contract SyndicateSequencingChainTest is SyndicateSequencingChainTestSetUp { vm.stopPrank(); vm.expectEmit(true, false, false, true); - emit SyndicateSequencingChain.TransactionProcessed( + emit SyndicateSequencingChainBase.TransactionProcessed( address(this), abi.encodePacked(L2MessageType_SignedTx, validTxn) ); - chain.processTransaction(validTxn); + combinedChain.processTransaction(validTxn); + + // Verify accumulator was updated (only available on combinedChain) + bytes memory encodedTxn = abi.encodePacked(L2MessageType_SignedTx, validTxn); + assertEq(combinedChain.sequencingAccumulator(0), keccak256(abi.encodePacked(bytes32(0), encodedTxn))); } function testProcessTransactionRequireAllFailure() public { @@ -134,7 +147,7 @@ contract SyndicateSequencingChainTest is SyndicateSequencingChainTestSetUp { vm.stopPrank(); vm.expectEmit(true, false, false, true); - emit SyndicateSequencingChain.TransactionProcessed( + emit SyndicateSequencingChainBase.TransactionProcessed( address(this), abi.encodePacked(L2MessageType_SignedTx, data) ); @@ -154,12 +167,20 @@ contract SyndicateSequencingChainTest is SyndicateSequencingChainTestSetUp { for (uint256 i = 0; i < validTxns.length; i++) { vm.expectEmit(true, false, false, true); - emit SyndicateSequencingChain.TransactionProcessed( + emit SyndicateSequencingChainBase.TransactionProcessed( address(this), abi.encodePacked(L2MessageType_SignedTx, validTxns[i]) ); } - chain.processTransactionsBulk(validTxns); + combinedChain.processTransactionsBulk(validTxns); + + // Verify accumulator (only available on combinedChain) + bytes32 prevAcc = bytes32(0); + for (uint256 i = 0; i < 3; i++) { + bytes32 expected = keccak256(abi.encodePacked(prevAcc, L2MessageType_SignedTx, validTxns[i])); + assertEq(combinedChain.sequencingAccumulator(i), expected); + prevAcc = expected; + } } function testConstructorWithZeroAppChainId() public { @@ -190,7 +211,7 @@ contract SyndicateSequencingChainTest is SyndicateSequencingChainTestSetUp { // Expect events for all transactions for (uint256 i = 0; i < txns.length; i++) { vm.expectEmit(true, false, false, true); - emit SyndicateSequencingChain.TransactionProcessed( + emit SyndicateSequencingChainBase.TransactionProcessed( address(this), abi.encodePacked(L2MessageType_SignedTx, txns[i]) ); } @@ -229,7 +250,7 @@ contract SyndicateSequencingChainTest is SyndicateSequencingChainTestSetUp { // Expect events for successful transactions for (uint256 i = 0; i < successTxns.length; i++) { vm.expectEmit(true, false, false, true); - emit SyndicateSequencingChain.TransactionProcessed( + emit SyndicateSequencingChainBase.TransactionProcessed( address(this), abi.encodePacked(L2MessageType_SignedTx, successTxns[i]) ); } @@ -286,12 +307,12 @@ contract SyndicateSequencingChainTest is SyndicateSequencingChainTestSetUp { directMock.setAllowed(abi.encodePacked(L2MessageType_SignedTx, disallowedData), false); // Test 1: Failure path of onlyWhenAllowed (processTransaction) - vm.expectRevert(SyndicateSequencingChain.TransactionOrSenderNotAllowed.selector); + vm.expectRevert(SyndicateSequencingChainBase.TransactionOrSenderNotAllowed.selector); chain.processTransaction(disallowedData); // Test 2: Success path of onlyWhenAllowed (processTransaction) vm.expectEmit(true, false, false, true); - emit SyndicateSequencingChain.TransactionProcessed( + emit SyndicateSequencingChainBase.TransactionProcessed( address(this), abi.encodePacked(L2MessageType_SignedTx, allowedData) ); chain.processTransaction(allowedData); @@ -299,7 +320,7 @@ contract SyndicateSequencingChainTest is SyndicateSequencingChainTestSetUp { function testProcessTransactionsBulkWithEmptyArray() public { bytes[] memory emptyArray = new bytes[](0); - vm.expectRevert(SyndicateSequencingChain.NoTxData.selector); + vm.expectRevert(SyndicateSequencingChainBase.NoTxData.selector); chain.processTransactionsBulk(emptyArray); } @@ -317,14 +338,14 @@ contract SyndicateSequencingChainTest is SyndicateSequencingChainTestSetUp { // Test owner can set it and it returns correct value with proper event vm.prank(admin); vm.expectEmit(true, true, false, false); - emit SyndicateSequencingChain.EmissionsReceiverUpdated(address(0), newReceiver); + emit SyndicateSequencingChainBase.EmissionsReceiverUpdated(address(0), newReceiver); chain.setEmissionsReceiver(newReceiver); assertEq(chain.getEmissionsReceiver(), newReceiver); // falls back to owner if emissionsReceiver is set to address(0) vm.prank(admin); vm.expectEmit(true, true, false, false); - emit SyndicateSequencingChain.EmissionsReceiverUpdated(newReceiver, admin); + emit SyndicateSequencingChainBase.EmissionsReceiverUpdated(newReceiver, admin); chain.setEmissionsReceiver(address(0)); assertEq(chain.getEmissionsReceiver(), admin); } @@ -335,7 +356,7 @@ contract SyndicateSequencingChainTest is SyndicateSequencingChainTestSetUp { vm.prank(admin); vm.expectEmit(true, true, false, false); - emit SyndicateSequencingChain.EmissionsReceiverUpdated(admin, newOwner); + emit SyndicateSequencingChainBase.EmissionsReceiverUpdated(admin, newOwner); chain.transferOwnership(newOwner); // Verify the emissions receiver changed diff --git a/synd-contracts/test/factory/SyndicateFactoryTest.t.sol b/synd-contracts/test/factory/SyndicateFactoryTest.t.sol index 27e92bca..bbd454e2 100644 --- a/synd-contracts/test/factory/SyndicateFactoryTest.t.sol +++ b/synd-contracts/test/factory/SyndicateFactoryTest.t.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.28; import {Test} from "forge-std/Test.sol"; import {SyndicateFactory} from "src/factory/SyndicateFactory.sol"; import {SyndicateSequencingChain} from "src/SyndicateSequencingChain.sol"; +import {SyndicateCombinedSequencingChain} from "src/SyndicateCombinedSequencingChain.sol"; import {RequireAndModule} from "src/requirement-modules/RequireAndModule.sol"; import {RequireOrModule} from "src/requirement-modules/RequireOrModule.sol"; import {RequireCompositeModule} from "src/requirement-modules/RequireCompositeModule.sol"; @@ -25,6 +26,10 @@ contract SyndicateFactoryTest is Test { uint256 indexed appchainId, address indexed sequencingChainAddress, address indexed permissionModuleAddress ); + event SyndicateCombinedSequencingChainCreated( + uint256 indexed appchainId, address indexed sequencingChainAddress, address indexed permissionModuleAddress + ); + event NamespaceConfigUpdated(uint256 oldNamespacePrefix, uint256 newNamespacePrefix); event ChainIdManuallyMarked(uint256 indexed chainId); @@ -115,6 +120,38 @@ contract SyndicateFactoryTest is Test { assertEq(permissionModule.owner(), admin); } + function testCreateCombinedSequencingChain() public { + RequireAndModule permissionModule = new RequireAndModule(admin); + address permissionModuleAddress = address(permissionModule); + + bytes32 salt = keccak256(abi.encodePacked("salt-for-combined-test")); + address expectedAddress = factory.computeCombinedSequencingChainAddress(salt, appchainId); + + vm.expectEmit(true, true, true, true); + emit SyndicateCombinedSequencingChainCreated(appchainId, expectedAddress, permissionModuleAddress); + + (address sequencingChainAddress, uint256 actualChainId) = + factory.createCombinedSequencingChain(appchainId, admin, permissionModule, salt); + + assertTrue(sequencingChainAddress != address(0)); + assertEq(actualChainId, appchainId); + assertEq(sequencingChainAddress, expectedAddress); + + SyndicateCombinedSequencingChain combinedChain = SyndicateCombinedSequencingChain(sequencingChainAddress); + + // Verify chain setup + assertEq(combinedChain.appchainId(), appchainId); + assertEq(address(combinedChain.permissionRequirementModule()), permissionModuleAddress); + assertEq(combinedChain.owner(), admin); + } + + function testGetCombinedBytecode() public view { + bytes memory bytecode = factory.getCombinedBytecode(appchainId); + bytes memory expectedBytecode = + abi.encodePacked(type(SyndicateCombinedSequencingChain).creationCode, abi.encode(appchainId)); + assertEq(bytecode, expectedBytecode); + } + function testCorrectAppChainIdAssignment() public { RequireAndModule permissionModule = new RequireAndModule(admin); RequireOrModule permissionModule2 = new RequireOrModule(admin); @@ -188,7 +225,7 @@ contract SyndicateFactoryTest is Test { function testGetBytecode() public view { bytes memory bytecode = factory.getBytecode(appchainId); bytes memory expectedBytecode = - abi.encodePacked(type(SyndicateSequencingChain).creationCode, abi.encode(appchainId, false)); + abi.encodePacked(type(SyndicateSequencingChain).creationCode, abi.encode(appchainId)); assertEq(bytecode, expectedBytecode); }