diff --git a/synd-contracts/foundry.toml b/synd-contracts/foundry.toml index f9c0e55ff..cc0f44ce8 100644 --- a/synd-contracts/foundry.toml +++ b/synd-contracts/foundry.toml @@ -23,7 +23,7 @@ exo = "${EXO_RPC_URL}" holesky = "https://ethereum-holesky-rpc.publicnode.com" localhost = "http://localhost:8545" mainnet = "${ETH_MAINNET_RPC_URL}" -risa_devnet = "https://risa-testnet.g.alchemy.com/public" +risa_devnet = "${RISA_DEVNET_RPC_URL}" sepolia = "https://ethereum-sepolia-rpc.publicnode.com" syndicate_frame = "https://rpc-frame.syndicate.io" diff --git a/synd-contracts/script/upgrade/UpgradeContracts.s.sol b/synd-contracts/script/upgrade/UpgradeContracts.s.sol index 5cb423b9b..7965ae783 100644 --- a/synd-contracts/script/upgrade/UpgradeContracts.s.sol +++ b/synd-contracts/script/upgrade/UpgradeContracts.s.sol @@ -45,7 +45,7 @@ contract UpgradeSyndicateFactory is Script { vm.stopBroadcast(); // Verify upgrade - uint256 version = factory.version(); + uint256 version = factory.VERSION(); console2.log("=== Upgrade Complete ==="); console2.log("Proxy:", factoryAddress); console2.log("Implementation:", address(newImplementation)); diff --git a/synd-contracts/src/factory/ChainRegistry.sol b/synd-contracts/src/factory/ChainRegistry.sol new file mode 100644 index 000000000..0b202b1f3 --- /dev/null +++ b/synd-contracts/src/factory/ChainRegistry.sol @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.28; + +import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; +import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol"; +import {IRequirementModule} from "../interfaces/IRequirementModule.sol"; +import {SyndicateForwarder} from "./SyndicateForwarder.sol"; + +/// @title ChainRegistry +/// @notice Registry contract for managing chain registrations on L1 +/// @dev This contract serves as the source of truth for chain registrations and forwards +/// deployment requests to the SyndicateFactory on L2 via the SyndicateForwarder +contract ChainRegistry is AccessControl, Pausable { + /*////////////////////////////////////////////////////////////// + STATE VARIABLES + //////////////////////////////////////////////////////////////*/ + + /// @notice Role for managing chain registrations and pausing + bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE"); + + /// @notice Address of the SyndicateForwarder contract that handles cross-chain messages + SyndicateForwarder public syndicateForwarder; + + /// @notice Mapping of chain IDs to their registration status + mapping(uint256 chainId => bool isRegistered) public registeredChains; + + /// @notice Mapping of addresses to their nonces for chain ID generation + mapping(address sender => uint256 nonce) public nonces; + + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + /// @notice Thrown when a zero address is provided where a valid address is required + error ZeroAddress(); + + /// @notice Thrown when attempting to register a chain with an already used chain ID + error ChainIdAlreadyRegistered(); + + /// @notice Thrown when the forwarder is not set + error ForwarderNotSet(); + + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + /// @notice Emitted when a new chain is registered + /// @param sender The address that requested the registration + /// @param chainId The unique identifier for the chain + /// @param admin The admin address for the new chain + /// @param permissionModule The permission module address + /// @param ticketId The Arbitrum retryable ticket ID + event ChainRegistered( + address indexed sender, + uint256 indexed chainId, + address indexed admin, + address permissionModule, + uint256 ticketId + ); + + /// @notice Emitted when a deterministic chainID is generated for a user + /// @param sender The address that requested the chain ID generation + /// @param nonce The nonce used in the chain ID generation + /// @param chainId The resulting deterministic chain ID + event DeterministicChainIdGenerated(address indexed sender, uint256 indexed nonce, uint256 indexed chainId); + + /// @notice Emitted when the forwarder address is updated + /// @param oldForwarder The previous forwarder address + /// @param newForwarder The new forwarder address + event ForwarderUpdated(address indexed oldForwarder, address indexed newForwarder); + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + /// @notice Initializes the ChainRegistry + /// @param _admin The admin address that will have DEFAULT_ADMIN_ROLE + /// @param _manager The manager address that will have MANAGER_ROLE + /// @param _syndicateForwarder The address of the SyndicateForwarder contract + constructor(address _admin, address _manager, address _syndicateForwarder) { + if (_admin == address(0) || _manager == address(0) || _syndicateForwarder == address(0)) { + revert ZeroAddress(); + } + + _grantRole(DEFAULT_ADMIN_ROLE, _admin); + _grantRole(MANAGER_ROLE, _manager); + + syndicateForwarder = SyndicateForwarder(_syndicateForwarder); + } + + /*////////////////////////////////////////////////////////////// + EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @notice Registers a new chain with deterministic chainID + /// @param admin The admin address for the new chain on L2 + /// @param permissionModule The pre-deployed permission module + /// @return chainId The chain ID that was registered + function registerChain(address admin, IRequirementModule permissionModule) + external + payable + whenNotPaused + returns (uint256 chainId) + { + if (admin == address(0) || address(permissionModule) == address(0)) { + revert ZeroAddress(); + } + if (address(syndicateForwarder) == address(0)) revert ForwarderNotSet(); + + // Get and increment nonce for this sender + uint256 nonce = nonces[msg.sender]++; + + // Generate deterministic chainID + chainId = generateDeterministicChainId(msg.sender, nonce); + + // Validate chain ID is not already registered + if (registeredChains[chainId]) { + revert ChainIdAlreadyRegistered(); + } + + // Mark chain as registered + registeredChains[chainId] = true; + + // Emit deterministic chain ID generation event + emit DeterministicChainIdGenerated(msg.sender, nonce, chainId); + + // Forward the deployment request to L2 via the forwarder + uint256 ticketId = + syndicateForwarder.forwardCreateChain{value: msg.value}(chainId, admin, address(permissionModule)); + + emit ChainRegistered(msg.sender, chainId, admin, address(permissionModule), ticketId); + + return chainId; + } + + /// @notice Registers a chain with a custom chainID (manager only) + /// @param customChainId The custom chain ID to use + /// @param admin The admin address for the new chain on L2 + /// @param permissionModule The pre-deployed permission module + /// @return chainId The chain ID that was registered (same as customChainId) + function registerChainWithCustomId(uint256 customChainId, address admin, IRequirementModule permissionModule) + external + payable + onlyRole(MANAGER_ROLE) + whenNotPaused + returns (uint256 chainId) + { + if (admin == address(0) || address(permissionModule) == address(0)) { + revert ZeroAddress(); + } + if (customChainId == 0) { + revert ZeroAddress(); // Reusing this error for zero chainID + } + if (address(syndicateForwarder) == address(0)) revert ForwarderNotSet(); + + // Validate chain ID is not already registered + if (registeredChains[customChainId]) { + revert ChainIdAlreadyRegistered(); + } + + // Mark chain as registered + registeredChains[customChainId] = true; + + // Forward the deployment request to L2 via the forwarder + uint256 ticketId = + syndicateForwarder.forwardCreateChain{value: msg.value}(customChainId, admin, address(permissionModule)); + + emit ChainRegistered(msg.sender, customChainId, admin, address(permissionModule), ticketId); + + return customChainId; + } + + /*////////////////////////////////////////////////////////////// + VIEW FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @notice Generate deterministic chainID from sender address and nonce + /// @param sender The sender address + /// @param nonce The nonce for this sender + /// @return chainId The deterministic chain ID + function generateDeterministicChainId(address sender, uint256 nonce) public pure returns (uint256 chainId) { + // Use keccak256 hash of sender + nonce, then take modulo to keep within reasonable range + bytes32 hash = keccak256(abi.encodePacked(sender, nonce)); + // Use modulo to keep chainId in a reasonable range + chainId = uint256(hash) % (10 ** 18); // Max 18 digits + // Ensure chainID is never 0 as this is used as a null value indicator + if (chainId == 0) { + chainId = 1; + } + } + + /*////////////////////////////////////////////////////////////// + ADMIN FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @notice Update the forwarder address (admin only) + /// @param newForwarder The new forwarder address + function setForwarder(address newForwarder) external onlyRole(DEFAULT_ADMIN_ROLE) { + if (newForwarder == address(0)) revert ZeroAddress(); + + address oldForwarder = address(syndicateForwarder); + syndicateForwarder = SyndicateForwarder(newForwarder); + + emit ForwarderUpdated(oldForwarder, newForwarder); + } + + /// @notice Pause the registry (manager only) + function pause() external onlyRole(MANAGER_ROLE) { + _pause(); + } + + /// @notice Unpause the registry (admin only) + function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) { + _unpause(); + } +} diff --git a/synd-contracts/src/factory/SyndicateFactory.sol b/synd-contracts/src/factory/SyndicateFactory.sol index ae2705c57..b856bc5e1 100644 --- a/synd-contracts/src/factory/SyndicateFactory.sol +++ b/synd-contracts/src/factory/SyndicateFactory.sol @@ -21,9 +21,9 @@ struct SyndicateFactoryStorage { /// @notice Current implementation address used for new deployments /// @dev This can be updated by admins to use newer versions of SyndicateSequencingChain address syndicateChainImpl; - /// @notice Version of the SyndicateFactory contract - /// @dev Used to track the current version of the factory contract - uint256 version; + /// @notice Trusted forwarder address for cross-chain deployments from L1 + /// @dev This is the SyndicateForwarder contract address on L2 that can trigger deployments + address trustedForwarder; } /// @title SyndicateFactory @@ -39,6 +39,10 @@ contract SyndicateFactory is Initializable, AccessControlUpgradeable, PausableUp bytes32 public constant SYNDICATE_FACTORY_STORAGE_LOCATION = 0x172dc7d0dcf94b29f0d6a133670a38a5db195bb2828e4d07ec71b370800c9300; + /// @notice Version of the SyndicateFactory contract + /// @dev Used to track the current version of the factory contract + uint256 public constant VERSION = 1_000_000; // 1.0.0 + /// @notice Internal function to access the ERC-7201 namespaced storage /// @dev Uses inline assembly to access the specific storage slot for this contract's data /// @return $ Storage pointer to the SyndicateFactoryStorage struct @@ -62,11 +66,11 @@ contract SyndicateFactory is Initializable, AccessControlUpgradeable, PausableUp return $.syndicateChainImpl; } - /// @notice Get the current version of this contract implementation - /// @return The version number - function version() public view returns (uint256) { + /// @notice Get the trusted forwarder address + /// @return The trusted forwarder address + function trustedForwarder() public view returns (address) { SyndicateFactoryStorage storage $ = _getSyndicateFactoryStorage(); - return $.version; + return $.trustedForwarder; } /*////////////////////////////////////////////////////////////// @@ -82,6 +86,9 @@ contract SyndicateFactory is Initializable, AccessControlUpgradeable, PausableUp /// @notice Thrown when the proxy upgrade to the latest implementation fails error FailedToInitializeSyndicateSequencingChain(); + /// @notice Thrown when the caller is not the trusted forwarder + error NotTrustedForwarder(); + /*////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////*/ @@ -103,6 +110,11 @@ contract SyndicateFactory is Initializable, AccessControlUpgradeable, PausableUp /// @param chainId The resulting deterministic chain ID event DeterministicChainIdGenerated(address indexed sender, uint256 indexed nonce, uint256 indexed chainId); + /// @notice Emitted when the trusted forwarder address is updated + /// @param oldForwarder The previous forwarder address + /// @param newForwarder The new forwarder address + event TrustedForwarderUpdated(address indexed oldForwarder, address indexed newForwarder); + /*////////////////////////////////////////////////////////////// INITIALIZER //////////////////////////////////////////////////////////////*/ @@ -118,7 +130,6 @@ contract SyndicateFactory is Initializable, AccessControlUpgradeable, PausableUp /// - Role-based access control with the provided admin /// - Deterministic stub implementation deployment /// - Real SyndicateSequencingChain implementation deployment - /// - Initial version setting /// @param admin The admin address that will have DEFAULT_ADMIN_ROLE and full control over the factory function initialize(address admin) external initializer { if (admin == address(0)) revert ZeroAddress(); @@ -131,9 +142,6 @@ contract SyndicateFactory is Initializable, AccessControlUpgradeable, PausableUp SyndicateFactoryStorage storage $ = _getSyndicateFactoryStorage(); - // Set initial version - $.version = 1_000_000; // 1.0.0 - // Deploy minimal stub implementation using CREATE2 for deterministic address bytes memory stubBytecode = abi.encodePacked(type(MinimalUUPSStub).creationCode); $.stubImplementation = Create2.deploy(0, bytes32("SYNDICATE_STUB_V1"), stubBytecode); @@ -147,7 +155,8 @@ contract SyndicateFactory is Initializable, AccessControlUpgradeable, PausableUp EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ - /// @notice Creates a new SyndicateSequencingChain contract with deterministic chainID to prevent squatting + /// @notice Creates a new SyndicateSequencingChain contract with deterministic chainID (admin only) + /// @dev This function is now restricted to admins. For cross-chain deployments, use ChainRegistry on L1 /// @param nonce The user-specified nonce for chainID generation /// @param admin The admin address for the new chain /// @param permissionModule The pre-deployed permission module @@ -156,6 +165,7 @@ contract SyndicateFactory is Initializable, AccessControlUpgradeable, PausableUp //#olympix-ignore-reentrancy-events function createSyndicateSequencingChain(uint256 nonce, address admin, IRequirementModule permissionModule) external + onlyRole(DEFAULT_ADMIN_ROLE) whenNotPaused returns (address sequencingChain, uint256 chainId) { @@ -292,17 +302,46 @@ contract SyndicateFactory is Initializable, AccessControlUpgradeable, PausableUp return (_doCreateChain(customChainId, admin, permissionModule), customChainId); } + /// @notice Creates a new SyndicateSequencingChain from L1's SyndicateForwarder (cross-chain deployments) + /// @dev This function is called when receiving cross-chain messages from L1's SyndicateForwarder via Arbitrum Inbox + /// The msg.sender will be the aliased address of the L1 SyndicateForwarder contract + /// Aliasing: L2_address = L1_address + 0x1111000000000000000000000000000000001111 + /// @param chainId The chain ID to use (must not be 0 or already used) + /// @param admin The admin address for the new chain + /// @param permissionModule The pre-deployed permission module + /// @return sequencingChain The deployed sequencing chain address + function createFromForwarder(uint256 chainId, address admin, IRequirementModule permissionModule) + external + whenNotPaused + returns (address sequencingChain) + { + SyndicateFactoryStorage storage $ = _getSyndicateFactoryStorage(); + + // Only the aliased L1 SyndicateForwarder can call this function + // trustedForwarder should be set to: L1_SyndicateForwarder_address + 0x1111000000000000000000000000000000001111 + if (msg.sender != $.trustedForwarder) { + revert NotTrustedForwarder(); + } + + if (admin == address(0) || address(permissionModule) == address(0)) { + revert ZeroAddress(); + } + if (chainId == 0) { + revert ZeroAddress(); // Reusing this error for zero chainID + } + + // Validate chain ID is not already used + if (isChainIdUsed(chainId)) { + revert ChainIdAlreadyExists(); + } + + return _doCreateChain(chainId, admin, permissionModule); + } + /// @notice Authorizes upgrades to new implementations (admin only) /// @param newImplementation The address of the new implementation function _authorizeUpgrade(address newImplementation) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} - /// @notice Updates the contract version (admin only, typically called during upgrades) - /// @param newVersion The new version number (e.g., 1) - function updateVersion(uint256 newVersion) external onlyRole(DEFAULT_ADMIN_ROLE) { - SyndicateFactoryStorage storage $ = _getSyndicateFactoryStorage(); - $.version = newVersion; - } - /// @notice Pause the factory (admin only) function pause() external onlyRole(DEFAULT_ADMIN_ROLE) { _pause(); @@ -323,4 +362,15 @@ contract SyndicateFactory is Initializable, AccessControlUpgradeable, PausableUp SyndicateFactoryStorage storage $ = _getSyndicateFactoryStorage(); $.syndicateChainImpl = newImplementation; } + + /// @notice Set the trusted forwarder address (admin only) + /// @dev Updates the trusted forwarder that can trigger cross-chain deployments + /// @param newForwarder The new trusted forwarder address + function setTrustedForwarder(address newForwarder) external onlyRole(DEFAULT_ADMIN_ROLE) { + SyndicateFactoryStorage storage $ = _getSyndicateFactoryStorage(); + address oldForwarder = $.trustedForwarder; + $.trustedForwarder = newForwarder; + + emit TrustedForwarderUpdated(oldForwarder, newForwarder); + } } diff --git a/synd-contracts/src/factory/SyndicateForwarder.sol b/synd-contracts/src/factory/SyndicateForwarder.sol new file mode 100644 index 000000000..5770dc645 --- /dev/null +++ b/synd-contracts/src/factory/SyndicateForwarder.sol @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.28; + +import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; +import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol"; +import {IL1Bridge} from "./interfaces/IL1Bridge.sol"; + +/// @title SyndicateForwarder +/// @notice L1 contract that forwards cross-chain messages to L2's SyndicateFactory +/// @dev This contract is deployed on L1 and uses a bridge adapter (Arbitrum, Optimism/Base, etc.) +/// to send messages to L2. The bridge adapter abstracts the differences between L2 implementations. +contract SyndicateForwarder is AccessControl, Pausable { + /*////////////////////////////////////////////////////////////// + STATE VARIABLES + //////////////////////////////////////////////////////////////*/ + + /// @notice Role for managing pause functionality and forwarding messages + bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE"); + + /// @notice The bridge adapter for sending L1→L2 messages + IL1Bridge public bridge; + + /// @notice The L2 SyndicateFactory address that will receive messages + address public l2Target; + + /// @notice Default gas limit for L2 execution + uint256 public defaultGasLimit; + + /// @notice Default max fee per gas for L2 + uint256 public defaultMaxFeePerGas; + + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + /// @notice Thrown when a zero address is provided where a valid address is required + error ZeroAddress(); + + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + /// @notice Emitted when a message is forwarded to L2 + /// @param sender The address that initiated the forward + /// @param l2Target The L2 target address + /// @param chainId The chain ID being created + /// @param messageId The bridge message ID (retryable ticket ID for Arbitrum) + event MessageForwarded( + address indexed sender, address indexed l2Target, uint256 indexed chainId, uint256 messageId + ); + + /// @notice Emitted when the L2 target address is updated + /// @param oldTarget The previous L2 target address + /// @param newTarget The new L2 target address + event L2TargetUpdated(address indexed oldTarget, address indexed newTarget); + + /// @notice Emitted when the bridge address is updated + /// @param oldBridge The previous bridge address + /// @param newBridge The new bridge address + event BridgeUpdated(address indexed oldBridge, address indexed newBridge); + + /// @notice Emitted when gas parameters are updated + /// @param gasLimit The new gas limit + /// @param maxFeePerGas The new max fee per gas + event GasParametersUpdated(uint256 gasLimit, uint256 maxFeePerGas); + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + /// @notice Initializes the SyndicateForwarder + /// @param _admin The admin address that will have DEFAULT_ADMIN_ROLE + /// @param _manager The manager address that will have MANAGER_ROLE + /// @param _bridge The bridge adapter address (ArbitrumL1Bridge or OptimismL1Bridge) + /// @param _l2Target The L2 SyndicateFactory address + /// @param _defaultGasLimit Default gas limit for L2 execution + /// @param _defaultMaxFeePerGas Default max fee per gas for L2 + constructor( + address _admin, + address _manager, + address _bridge, + address _l2Target, + uint256 _defaultGasLimit, + uint256 _defaultMaxFeePerGas + ) { + if (_admin == address(0)) revert ZeroAddress(); + if (_manager == address(0)) revert ZeroAddress(); + if (_bridge == address(0)) revert ZeroAddress(); + if (_l2Target == address(0)) revert ZeroAddress(); + + _grantRole(DEFAULT_ADMIN_ROLE, _admin); + _grantRole(MANAGER_ROLE, _manager); + + bridge = IL1Bridge(_bridge); + l2Target = _l2Target; + defaultGasLimit = _defaultGasLimit; + defaultMaxFeePerGas = _defaultMaxFeePerGas; + } + + /*////////////////////////////////////////////////////////////// + EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @notice Forwards a chain creation request to L2's SyndicateFactory + /// @dev Uses the configured bridge adapter to send cross-chain message + /// For Arbitrum: Uses retryable tickets, requires ETH for gas + /// For Optimism/Base: Uses CrossDomainMessenger, different fee model + /// Only manager can call this function + /// @param chainId The chain ID to create + /// @param admin The admin address for the new chain + /// @param permissionModule The permission module address + /// @return messageId The bridge message ID (implementation-specific) + function forwardCreateChain(uint256 chainId, address admin, address permissionModule) + external + payable + onlyRole(MANAGER_ROLE) + whenNotPaused + returns (uint256 messageId) + { + // Encode the call to SyndicateFactory.createFromForwarder + bytes memory data = + abi.encodeWithSignature("createFromForwarder(uint256,address,address)", chainId, admin, permissionModule); + + // Send message via bridge adapter + // msg.value is forwarded to handle different fee models (Arbitrum requires ETH upfront) + messageId = bridge.sendMessage{value: msg.value}(l2Target, data, defaultGasLimit, defaultMaxFeePerGas); + + emit MessageForwarded(msg.sender, l2Target, chainId, messageId); + + return messageId; + } + + /*////////////////////////////////////////////////////////////// + ADMIN FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @notice Update the L2 target address (admin only) + /// @param newTarget The new L2 target address + function setL2Target(address newTarget) external onlyRole(DEFAULT_ADMIN_ROLE) { + if (newTarget == address(0)) revert ZeroAddress(); + + address oldTarget = l2Target; + l2Target = newTarget; + + emit L2TargetUpdated(oldTarget, newTarget); + } + + /// @notice Update the bridge adapter address (admin only) + /// @dev This allows switching between different bridge implementations + /// @param newBridge The new bridge adapter address + function setBridge(address newBridge) external onlyRole(DEFAULT_ADMIN_ROLE) { + if (newBridge == address(0)) revert ZeroAddress(); + + address oldBridge = address(bridge); + bridge = IL1Bridge(newBridge); + + emit BridgeUpdated(oldBridge, newBridge); + } + + /// @notice Update gas parameters (admin only) + /// @param gasLimit The new gas limit + /// @param maxFeePerGas The new max fee per gas + function setGasParameters(uint256 gasLimit, uint256 maxFeePerGas) external onlyRole(DEFAULT_ADMIN_ROLE) { + defaultGasLimit = gasLimit; + defaultMaxFeePerGas = maxFeePerGas; + + emit GasParametersUpdated(gasLimit, maxFeePerGas); + } + + /// @notice Pause the forwarder (manager only) + function pause() external onlyRole(MANAGER_ROLE) { + _pause(); + } + + /// @notice Unpause the forwarder (admin only) + function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) { + _unpause(); + } +} diff --git a/synd-contracts/src/factory/bridges/ArbitrumL1Bridge.sol b/synd-contracts/src/factory/bridges/ArbitrumL1Bridge.sol new file mode 100644 index 000000000..c7b32af5e --- /dev/null +++ b/synd-contracts/src/factory/bridges/ArbitrumL1Bridge.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.28; + +import {IL1Bridge} from "../interfaces/IL1Bridge.sol"; +import {IInbox} from "@arbitrum/nitro-contracts/src/bridge/IInbox.sol"; + +/// @title ArbitrumL1Bridge +/// @notice Arbitrum implementation of L1→L2 bridge messaging +/// @dev Uses Arbitrum's Inbox for retryable tickets +contract ArbitrumL1Bridge is IL1Bridge { + /// @notice The Arbitrum Inbox contract + IInbox public immutable inbox; + + /// @notice Thrown when insufficient ETH is provided for the retryable ticket + error InsufficientValue(); + + /// @notice Initializes the Arbitrum bridge adapter + /// @param _inbox The Arbitrum Inbox contract address + constructor(address _inbox) { + inbox = IInbox(_inbox); + } + + /// @notice Sends a cross-chain message to Arbitrum L2 + /// @param target The L2 contract address to call + /// @param data The calldata to send to the target + /// @param gasLimit The gas limit for L2 execution + /// @param maxFeePerGas The max fee per gas for L2 + /// @return messageId The retryable ticket ID + function sendMessage(address target, bytes calldata data, uint256 gasLimit, uint256 maxFeePerGas) + external + payable + override + returns (uint256 messageId) + { + if (msg.value == 0) revert InsufficientValue(); + + // Send retryable ticket to L2 + // msg.value should cover: maxSubmissionCost + (gasLimit * maxFeePerGas) + messageId = inbox.createRetryableTicket{value: msg.value}( + target, // destination + 0, // l2CallValue (no ETH sent to target) + msg.value, // maxSubmissionCost (use all msg.value for submission) + msg.sender, // excessFeeRefundAddress + msg.sender, // callValueRefundAddress + gasLimit, // gasLimit + maxFeePerGas, // maxFeePerGas + data // data + ); + + return messageId; + } +} diff --git a/synd-contracts/src/factory/bridges/OptimismL1Bridge.sol b/synd-contracts/src/factory/bridges/OptimismL1Bridge.sol new file mode 100644 index 000000000..3ffeeebe4 --- /dev/null +++ b/synd-contracts/src/factory/bridges/OptimismL1Bridge.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.28; + +import {IL1Bridge} from "../interfaces/IL1Bridge.sol"; +import {ICrossDomainMessenger} from + "eigenlayer-middleware/lib/openzeppelin-contracts/contracts/vendor/optimism/ICrossDomainMessenger.sol"; + +/// @title OptimismL1Bridge +/// @notice Optimism/Base implementation of L1→L2 bridge messaging +/// @dev Uses Optimism's CrossDomainMessenger (also works for Base and other OP Stack chains) +contract OptimismL1Bridge is IL1Bridge { + /// @notice The Optimism CrossDomainMessenger contract + ICrossDomainMessenger public immutable messenger; + + /// @notice Counter for generating unique message IDs + /// @dev Optimism's sendMessage doesn't return an ID, so we track our own + uint256 private messageCounter; + + /// @notice Emitted when a message is sent + /// @param messageId Our internal message ID + /// @param target The L2 target address + /// @param data The calldata sent + /// @param gasLimit The gas limit for L2 execution + event MessageSent(uint256 indexed messageId, address indexed target, bytes data, uint256 gasLimit); + + /// @notice Initializes the Optimism bridge adapter + /// @param _messenger The Optimism CrossDomainMessenger contract address + constructor(address _messenger) { + messenger = ICrossDomainMessenger(_messenger); + } + + /// @notice Sends a cross-chain message to Optimism/Base L2 + /// @param target The L2 contract address to call + /// @param data The calldata to send to the target + /// @param gasLimit The gas limit for L2 execution + /// @param maxFeePerGas Not used for Optimism (kept for interface compatibility) + /// @return messageId Our internal message ID + function sendMessage(address target, bytes calldata data, uint256 gasLimit, uint256 maxFeePerGas) + external + payable + override + returns (uint256 messageId) + { + // Optimism uses uint32 for gas limit + require(gasLimit <= type(uint32).max, "Gas limit too high"); + + // Generate our own message ID + messageId = ++messageCounter; + + // Send message via CrossDomainMessenger + // Note: Optimism doesn't require ETH payment upfront like Arbitrum + // The relayer fee model is different + messenger.sendMessage(target, data, uint32(gasLimit)); + + emit MessageSent(messageId, target, data, gasLimit); + + return messageId; + } +} diff --git a/synd-contracts/src/factory/interfaces/IL1Bridge.sol b/synd-contracts/src/factory/interfaces/IL1Bridge.sol new file mode 100644 index 000000000..32ae46747 --- /dev/null +++ b/synd-contracts/src/factory/interfaces/IL1Bridge.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.28; + +/// @title IL1Bridge +/// @notice Interface for L1→L2 bridge messaging +/// @dev Different implementations for Arbitrum, Optimism/Base, etc. +interface IL1Bridge { + /// @notice Sends a cross-chain message to L2 + /// @param target The L2 contract address to call + /// @param data The calldata to send to the target + /// @param gasLimit The gas limit for L2 execution + /// @param maxFeePerGas The max fee per gas for L2 + /// @return messageId A unique identifier for the cross-chain message + function sendMessage(address target, bytes calldata data, uint256 gasLimit, uint256 maxFeePerGas) + external + payable + returns (uint256 messageId); +} diff --git a/synd-contracts/test/factory/SyndicateFactoryAddressAliasing.t.sol b/synd-contracts/test/factory/SyndicateFactoryAddressAliasing.t.sol new file mode 100644 index 000000000..aeea88020 --- /dev/null +++ b/synd-contracts/test/factory/SyndicateFactoryAddressAliasing.t.sol @@ -0,0 +1,293 @@ +// SPDX-License-Identifier: UNLICENSED +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 {RequireAndModule} from "src/requirement-modules/RequireAndModule.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {AddressAliasHelper} from "@arbitrum/nitro-contracts/src/libraries/AddressAliasHelper.sol"; + +/// @title SyndicateFactoryAddressAliasingTest +/// @notice Tests for address aliasing functionality in SyndicateFactory +/// @dev Address aliasing is used to distinguish L1 contract addresses when they interact with L2 +/// This is important for cross-chain admin control where an L1 contract needs to manage L2 contracts +contract SyndicateFactoryAddressAliasingTest is Test { + SyndicateFactory public factory; + address public l1Admin; + address public l2AliasedAdmin; + address public nonAdmin; + uint256 public appchainId = 10042001; + + // Constants for role checking + bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00; + + // Events + event SyndicateSequencingChainCreated( + uint256 indexed appchainId, address indexed sequencingChainAddress, address indexed permissionModuleAddress + ); + + function setUp() public { + vm.warp(1754089200 + 1 days); // after epoch start + + // Simulate an L1 admin contract address + l1Admin = address(0xC0FFEE); + + // Apply L1 to L2 aliasing to get the L2 representation + l2AliasedAdmin = AddressAliasHelper.applyL1ToL2Alias(l1Admin); + + nonAdmin = address(0x3); + + // Deploy factory implementation and proxy with ALIASED admin + SyndicateFactory implementation = new SyndicateFactory(); + bytes memory initData = abi.encodeCall(SyndicateFactory.initialize, (l2AliasedAdmin)); + ERC1967Proxy proxy = new ERC1967Proxy(address(implementation), initData); + factory = SyndicateFactory(address(proxy)); + } + + /// @notice Test that the aliased admin has correct permissions + function testAliasedAdminHasAdminRole() public view { + assertTrue(factory.hasRole(DEFAULT_ADMIN_ROLE, l2AliasedAdmin), "Aliased admin should have DEFAULT_ADMIN_ROLE"); + assertFalse(factory.hasRole(DEFAULT_ADMIN_ROLE, l1Admin), "Original L1 admin should NOT have admin role"); + } + + /// @notice Test that the original L1 address cannot perform admin actions + function testL1AdminCannotPerformAdminActions() public { + RequireAndModule permissionModule = new RequireAndModule(l1Admin); + + vm.prank(l1Admin); + vm.expectRevert(); // AccessControl will revert + factory.createSyndicateSequencingChainWithCustomId(appchainId, l1Admin, permissionModule); + } + + /// @notice Test that the aliased L2 address can perform admin actions + function testL2AliasedAdminCanPerformAdminActions() public { + RequireAndModule permissionModule = new RequireAndModule(l2AliasedAdmin); + + vm.prank(l2AliasedAdmin); + (address sequencingChain, uint256 actualChainId) = + factory.createSyndicateSequencingChainWithCustomId(appchainId, l2AliasedAdmin, permissionModule); + + assertTrue(sequencingChain != address(0), "Sequencing chain should be deployed"); + assertEq(actualChainId, appchainId, "Chain ID should match"); + } + + /// @notice Test pause/unpause with aliased admin + function testAliasedAdminCanPauseUnpause() public { + assertFalse(factory.paused(), "Factory should not be paused initially"); + + vm.prank(l2AliasedAdmin); + factory.pause(); + assertTrue(factory.paused(), "Factory should be paused"); + + vm.prank(l2AliasedAdmin); + factory.unpause(); + assertFalse(factory.paused(), "Factory should be unpaused"); + } + + /// @notice Test that L1 admin cannot pause (only aliased address can) + function testL1AdminCannotPause() public { + vm.prank(l1Admin); + vm.expectRevert(); // AccessControl will revert + factory.pause(); + } + + /// @notice Test version is constant + function testVersionIsConstant() public view { + assertEq(factory.VERSION(), 1_000_000, "Version should be 1.0.0"); + } + + /// @notice Test undoing alias to get back original L1 address + function testUndoL2ToL1Alias() public view { + address recoveredL1Address = AddressAliasHelper.undoL1ToL2Alias(l2AliasedAdmin); + assertEq(recoveredL1Address, l1Admin, "Undoing alias should recover original L1 address"); + } + + /// @notice Test aliasing is deterministic + function testAliasingIsDeterministic() public view { + address alias1 = AddressAliasHelper.applyL1ToL2Alias(l1Admin); + address alias2 = AddressAliasHelper.applyL1ToL2Alias(l1Admin); + assertEq(alias1, alias2, "Aliasing should be deterministic"); + assertEq(alias1, l2AliasedAdmin, "Should match the aliased admin"); + } + + /// @notice Test that aliasing changes the address + function testAliasingChangesAddress() public view { + assertTrue(l2AliasedAdmin != l1Admin, "Aliased address should differ from original"); + + // The offset is 0x1111000000000000000000000000000000001111 + uint160 expectedDiff = uint160(0x1111000000000000000000000000000000001111); + uint160 actualDiff = uint160(l2AliasedAdmin) - uint160(l1Admin); + assertEq(actualDiff, expectedDiff, "Alias offset should match expected value"); + } + + /// @notice Test setting implementation with aliased admin + function testAliasedAdminCanSetImplementation() public { + SyndicateSequencingChain newImpl = new SyndicateSequencingChain(); + + vm.prank(l2AliasedAdmin); + factory.setSyndicateSequencingChainImplementation(address(newImpl)); + + assertEq(factory.syndicateChainImpl(), address(newImpl), "Implementation should be updated"); + } + + /// @notice Test that L1 admin cannot set implementation + function testL1AdminCannotSetImplementation() public { + SyndicateSequencingChain newImpl = new SyndicateSequencingChain(); + + vm.prank(l1Admin); + vm.expectRevert(); // AccessControl will revert + factory.setSyndicateSequencingChainImplementation(address(newImpl)); + } + + /// @notice Test multiple different L1 addresses and their aliases + function testMultipleL1AddressAliases() public { + address l1Addr1 = address(0x1111); + address l1Addr2 = address(0x2222); + address l1Addr3 = address(0x3333); + + address l2Alias1 = AddressAliasHelper.applyL1ToL2Alias(l1Addr1); + address l2Alias2 = AddressAliasHelper.applyL1ToL2Alias(l1Addr2); + address l2Alias3 = AddressAliasHelper.applyL1ToL2Alias(l1Addr3); + + // All aliases should be different + assertTrue(l2Alias1 != l2Alias2, "Alias 1 and 2 should differ"); + assertTrue(l2Alias1 != l2Alias3, "Alias 1 and 3 should differ"); + assertTrue(l2Alias2 != l2Alias3, "Alias 2 and 3 should differ"); + + // All should be different from originals + assertTrue(l2Alias1 != l1Addr1, "Alias 1 should differ from original"); + assertTrue(l2Alias2 != l1Addr2, "Alias 2 should differ from original"); + assertTrue(l2Alias3 != l1Addr3, "Alias 3 should differ from original"); + + // Should be able to recover originals + assertEq(AddressAliasHelper.undoL1ToL2Alias(l2Alias1), l1Addr1); + assertEq(AddressAliasHelper.undoL1ToL2Alias(l2Alias2), l1Addr2); + assertEq(AddressAliasHelper.undoL1ToL2Alias(l2Alias3), l1Addr3); + } + + /// @notice Test that non-admin cannot grant roles to aliased addresses + function testNonAdminCannotGrantRoles() public { + bytes32 someRole = keccak256("SOME_ROLE"); + + vm.prank(nonAdmin); + vm.expectRevert(); // AccessControl will revert + factory.grantRole(someRole, l2AliasedAdmin); + } + + /// @notice Test that aliased admin can grant roles + function testAliasedAdminCanGrantRoles() public { + bytes32 someRole = keccak256("SOME_ROLE"); + address recipient = address(0x999); + + vm.prank(l2AliasedAdmin); + factory.grantRole(someRole, recipient); + + assertTrue(factory.hasRole(someRole, recipient), "Role should be granted"); + } + + /// @notice Fuzz test: various L1 addresses should produce valid aliases + function testFuzzL1ToL2Aliasing(address l1Address) public view { + // Skip zero address + vm.assume(l1Address != address(0)); + + address l2Alias = AddressAliasHelper.applyL1ToL2Alias(l1Address); + + // Alias should be different from original + assertTrue(l2Alias != l1Address, "Alias must differ from original"); + + // Should be able to recover original + address recovered = AddressAliasHelper.undoL1ToL2Alias(l2Alias); + assertEq(recovered, l1Address, "Should recover original address"); + } + + /// @notice Test that applying alias twice doesn't break things + function testDoubleAliasing() public view { + address doubleAliased = AddressAliasHelper.applyL1ToL2Alias(l2AliasedAdmin); + + // Double aliasing should produce a different address + assertTrue(doubleAliased != l2AliasedAdmin, "Double alias should differ"); + assertTrue(doubleAliased != l1Admin, "Double alias should differ from L1 original"); + + // Undoing once should give us the first alias + address undoOnce = AddressAliasHelper.undoL1ToL2Alias(doubleAliased); + assertEq(undoOnce, l2AliasedAdmin, "Undo once should give first alias"); + + // Undoing twice should give us the original + address undoTwice = AddressAliasHelper.undoL1ToL2Alias(undoOnce); + assertEq(undoTwice, l1Admin, "Undo twice should give original"); + } + + /// @notice Test factory deployment with different aliasing scenarios + function testDeployFactoryWithDifferentAdmins() public { + // Test 1: Regular EOA admin (no aliasing needed) + address eoaAdmin = address(0xDEADBEEF); + SyndicateFactory impl1 = new SyndicateFactory(); + bytes memory initData1 = abi.encodeCall(SyndicateFactory.initialize, (eoaAdmin)); + ERC1967Proxy proxy1 = new ERC1967Proxy(address(impl1), initData1); + SyndicateFactory factory1 = SyndicateFactory(address(proxy1)); + + assertTrue(factory1.hasRole(DEFAULT_ADMIN_ROLE, eoaAdmin), "EOA admin should have role"); + + // Test 2: Aliased contract admin (for L1 contract control) + address l1ContractAdmin = address(0xCAFEBABE); + address l2AliasedContractAdmin = AddressAliasHelper.applyL1ToL2Alias(l1ContractAdmin); + + SyndicateFactory impl2 = new SyndicateFactory(); + bytes memory initData2 = abi.encodeCall(SyndicateFactory.initialize, (l2AliasedContractAdmin)); + ERC1967Proxy proxy2 = new ERC1967Proxy(address(impl2), initData2); + SyndicateFactory factory2 = SyndicateFactory(address(proxy2)); + + assertTrue(factory2.hasRole(DEFAULT_ADMIN_ROLE, l2AliasedContractAdmin), "Aliased admin should have role"); + assertFalse(factory2.hasRole(DEFAULT_ADMIN_ROLE, l1ContractAdmin), "Original L1 address should not have role"); + } + + /// @notice Test creating sequencing chains with aliased admins for the chains themselves + function testCreateSequencingChainWithAliasedChainAdmin() public { + // L1 address that will manage the sequencing chain + address l1ChainManager = address(0xBEEF); + address l2AliasedChainManager = AddressAliasHelper.applyL1ToL2Alias(l1ChainManager); + + RequireAndModule permissionModule = new RequireAndModule(l2AliasedChainManager); + + vm.prank(l2AliasedAdmin); + (address sequencingChain, uint256 actualChainId) = factory.createSyndicateSequencingChainWithCustomId( + appchainId, l2AliasedChainManager, permissionModule + ); + + assertTrue(sequencingChain != address(0), "Sequencing chain should be deployed"); + assertEq(actualChainId, appchainId, "Chain ID should match"); + + SyndicateSequencingChain chain = SyndicateSequencingChain(sequencingChain); + + // The chain should have the aliased manager as owner, not the L1 address + assertEq(chain.owner(), l2AliasedChainManager, "Chain should recognize aliased admin as owner"); + assertTrue(chain.owner() != l1ChainManager, "Chain should not recognize L1 address as owner"); + } + + /// @notice Test edge case: zero address aliasing + function testZeroAddressAliasing() public view { + address zeroAlias = AddressAliasHelper.applyL1ToL2Alias(address(0)); + + // Zero address + offset = offset value + assertEq(zeroAlias, address(uint160(0x1111000000000000000000000000000000001111)), "Zero alias should equal offset"); + + // Undoing should give back zero + address recovered = AddressAliasHelper.undoL1ToL2Alias(zeroAlias); + assertEq(recovered, address(0), "Should recover zero address"); + } + + /// @notice Test max address aliasing (edge case) + function testMaxAddressAliasing() public view { + // This will overflow and wrap around due to unchecked in AddressAliasHelper + address maxAddress = address(type(uint160).max); + address aliased = AddressAliasHelper.applyL1ToL2Alias(maxAddress); + + // Due to overflow, the aliased address will wrap around + assertTrue(aliased != maxAddress, "Should produce different address"); + + // Undoing should still recover original due to symmetric underflow + address recovered = AddressAliasHelper.undoL1ToL2Alias(aliased); + assertEq(recovered, maxAddress, "Should recover max address"); + } +} diff --git a/synd-contracts/test/factory/SyndicateFactoryTest.t.sol b/synd-contracts/test/factory/SyndicateFactoryTest.t.sol index 1025c1ab3..15d2ace04 100644 --- a/synd-contracts/test/factory/SyndicateFactoryTest.t.sol +++ b/synd-contracts/test/factory/SyndicateFactoryTest.t.sol @@ -530,13 +530,13 @@ contract SyndicateFactoryTest is Test { address sender2 = address(0x222); // Both senders use nonce 0 - vm.prank(sender1); + vm.prank(admin); (, uint256 chainId1) = factory.createSyndicateSequencingChain(0, chainAdmin, permissionModule); - vm.prank(sender2); - (, uint256 chainId2) = factory.createSyndicateSequencingChain(0, chainAdmin, permissionModule); + vm.prank(admin); + (, uint256 chainId2) = factory.createSyndicateSequencingChain(1, chainAdmin, permissionModule); - // Chain IDs should be different (because sender addresses are different) + // Chain IDs should be different (because nonces are different) assertTrue(chainId1 != chainId2); } @@ -557,6 +557,7 @@ contract SyndicateFactoryTest is Test { function testCreateSequencingChainDeterministicRevertsOnZeroAdmin() public { RequireAndModule permissionModule = new RequireAndModule(admin); + vm.prank(admin); vm.expectRevert(SyndicateFactory.ZeroAddress.selector); factory.createSyndicateSequencingChain(0, address(0), permissionModule); } @@ -564,6 +565,7 @@ contract SyndicateFactoryTest is Test { function testCreateSequencingChainDeterministicRevertsOnZeroPermissionModule() public { address chainAdmin = address(0x789); + vm.prank(admin); vm.expectRevert(SyndicateFactory.ZeroAddress.selector); factory.createSyndicateSequencingChain(0, chainAdmin, IRequirementModule(address(0))); } @@ -653,25 +655,23 @@ contract SyndicateFactoryTest is Test { // This test demonstrates that the same sender will generate deterministic chainIDs // preventing squatting across different deployments - address sender = address(0x555); - - // Deploy a chain - this will use nonce 0 for the sender + // Deploy a chain - this will use nonce 0 for the admin RequireAndModule permissionModule = new RequireAndModule(admin); address chainAdmin = address(0x789); - // Generate expected chainID for sender with nonce 0 (first deployment) - uint256 expectedChainId = factory.generateDeterministicChainId(sender, 0); + // Generate expected chainID for admin with nonce 0 (first deployment) + uint256 expectedChainId = factory.generateDeterministicChainId(admin, 0); - vm.prank(sender); + vm.prank(admin); (, uint256 actualChainId) = factory.createSyndicateSequencingChain(0, chainAdmin, permissionModule); // ChainID should match expected assertEq(actualChainId, expectedChainId); - // Now simulate trying to deploy again with the same sender + // Now simulate trying to deploy again with the same sender (admin) // This should generate a different chain ID (nonce 1) but should not revert - uint256 expectedChainId2 = factory.generateDeterministicChainId(sender, 1); - vm.prank(sender); + uint256 expectedChainId2 = factory.generateDeterministicChainId(admin, 1); + vm.prank(admin); (, uint256 actualChainId2) = factory.createSyndicateSequencingChain(1, chainAdmin, permissionModule); assertEq(actualChainId2, expectedChainId2); @@ -704,7 +704,7 @@ contract SyndicateFactoryTest is Test { // Deploy 5 sequential chains using explicit nonces for (uint256 i = 0; i < 5; i++) { - vm.prank(sender); + vm.prank(admin); (, chainIds[i]) = factory.createSyndicateSequencingChain(i, chainAdmin, permissionModule); } @@ -748,6 +748,7 @@ contract SyndicateFactoryTest is Test { assertEq(customChainId1, 6001); // Test 3: Deterministic chain creation + vm.prank(admin); (, uint256 detChainId) = factory.createSyndicateSequencingChain(0, admin, permissionModule); assertTrue(detChainId > 0); @@ -802,6 +803,7 @@ contract SyndicateFactoryTest is Test { vm.expectRevert(SyndicateFactory.ZeroAddress.selector); factory.createSyndicateSequencingChainWithCustomId(1001, address(0), permissionModule); + vm.prank(admin); vm.expectRevert(SyndicateFactory.ZeroAddress.selector); factory.createSyndicateSequencingChain(0, address(0), permissionModule); @@ -814,6 +816,7 @@ contract SyndicateFactoryTest is Test { vm.expectRevert(SyndicateFactory.ZeroAddress.selector); factory.createSyndicateSequencingChainWithCustomId(1003, admin, IRequirementModule(address(0))); + vm.prank(admin); vm.expectRevert(SyndicateFactory.ZeroAddress.selector); factory.createSyndicateSequencingChain(0, admin, IRequirementModule(address(0))); @@ -882,49 +885,7 @@ contract SyndicateFactoryTest is Test { // ================== VERSION TRACKING TESTS ================== - function testInitialVersion() public view { - assertEq(factory.version(), 1_000_000, "Initial version should be 1.0.0"); - } - - function testUpdateVersion() public { - vm.prank(admin); - factory.updateVersion(11); - - assertEq(factory.version(), 11, "Version should be updated to 11"); - } - - function testUpdateVersionOnlyAdmin() public { - vm.prank(nonAdmin); - vm.expectRevert(); - factory.updateVersion(11); - } - - function testUpdateVersionWithDifferentFormats() public { - uint256[] memory versions = new uint256[](5); - versions[0] = 11; - versions[1] = 22; - versions[2] = 23; - versions[3] = 31; - versions[4] = 102520; - - for (uint256 i = 0; i < versions.length; i++) { - vm.prank(admin); - factory.updateVersion(versions[i]); - assertEq(factory.version(), versions[i], "Version should match updated value"); - } - } - - function testVersionPersistsAfterOperations() public { - // Update version - vm.prank(admin); - factory.updateVersion(15); - - // Perform other operations - RequireAndModule permissionModule = new RequireAndModule(admin); - vm.prank(admin); - factory.createSyndicateSequencingChainWithCustomId(12345, admin, permissionModule); - - // Version should still be the same - assertEq(factory.version(), 15, "Version should persist after operations"); + function testVersion() public view { + assertEq(factory.VERSION(), 1_000_000, "Version should be 1.0.0"); } } diff --git a/synd-contracts/test/upgrade/UpgradeFlowIntegrationTest.t.sol b/synd-contracts/test/upgrade/UpgradeFlowIntegrationTest.t.sol index dc091b3e2..a902a7ced 100644 --- a/synd-contracts/test/upgrade/UpgradeFlowIntegrationTest.t.sol +++ b/synd-contracts/test/upgrade/UpgradeFlowIntegrationTest.t.sol @@ -122,7 +122,7 @@ contract UpgradeFlowIntegrationTest is Test, EpochTracker { function test_InitialDeployment() public view { // Verify factory assertTrue(address(factoryProxy) != address(0), "Factory should be deployed"); - assertEq(factoryProxy.version(), 1_000_000, "Factory version should be 1.0.0"); + assertEq(factoryProxy.VERSION(), 1_000_000, "Factory version should be 1.0.0"); // Verify gas aggregator assertTrue(address(gasAggregator) != address(0), "GasAggregator should be deployed"); @@ -193,7 +193,7 @@ contract UpgradeFlowIntegrationTest is Test, EpochTracker { function test_UpgradeFactoryToV2() public { // Store pre-upgrade state - uint256 preUpgradeVersion = factoryProxy.version(); + uint256 preUpgradeVersion = factoryProxy.VERSION(); vm.startPrank(ADMIN); @@ -211,7 +211,7 @@ contract UpgradeFlowIntegrationTest is Test, EpochTracker { vm.stopPrank(); // Verify storage preserved - assertEq(factoryProxy.version(), preUpgradeVersion, "Version should be preserved"); + assertEq(factoryProxy.VERSION(), preUpgradeVersion, "Version should be preserved"); assertTrue(factoryProxy.hasRole(factoryProxy.DEFAULT_ADMIN_ROLE(), ADMIN), "Admin role should be preserved"); // Verify new V2 functionality