diff --git a/solidity/foundry.toml b/solidity/foundry.toml index 950cc78e4..1fe04dc3f 100644 --- a/solidity/foundry.toml +++ b/solidity/foundry.toml @@ -16,3 +16,4 @@ hyperlane = { version = "5.8.3", git = "https://gith "@layerzerolabs-lz-evm-oapp-v2" = "3.0.81" "@layerzerolabs-lz-evm-protocol-v2" = "3.0.81" aave-v3-origin = { version = "3.3.0", git = "https://github.com/aave-dao/aave-v3-origin", tag = "v3.3.0" } +succinctlabs-sp1-contracts = "4.0.0" diff --git a/solidity/remappings.txt b/solidity/remappings.txt index fc80e2f5d..112ae784a 100644 --- a/solidity/remappings.txt +++ b/solidity/remappings.txt @@ -6,3 +6,4 @@ aave-v3-origin/=dependencies/aave-v3-origin-3.3.0/src/contracts forge-std/=dependencies/forge-std-1.9.4/ hyperlane/=dependencies/hyperlane-5.8.3/solidity/contracts +succinctlabs-sp1-contracts/=dependencies/succinctlabs-sp1-contracts-4.0.0/contracts diff --git a/solidity/soldeer.lock b/solidity/soldeer.lock index 6861af4aa..f71947168 100644 --- a/solidity/soldeer.lock +++ b/solidity/soldeer.lock @@ -45,3 +45,10 @@ name = "hyperlane" version = "5.8.3" git = "https://github.com/hyperlane-xyz/hyperlane-monorepo" rev = "5b70527" + +[[dependencies]] +name = "succinctlabs-sp1-contracts" +version = "4.0.0" +url = "https://soldeer-revisions.s3.amazonaws.com/succinctlabs-sp1-contracts/4_0_0_23-02-2025_07:52:37_succinctlabs-sp1-contracts-4.0.zip" +checksum = "b470b0a328169dcdd29da4e63e3a53a1ef886f030a5251a04f6fa5fd4d311856" +integrity = "b7204891c337b2c25158becf50af77de5a9223411dbd006c3b6e86e2dfe60144" diff --git a/solidity/src/authorization/Authorization.sol b/solidity/src/authorization/Authorization.sol new file mode 100644 index 000000000..bb7234ce6 --- /dev/null +++ b/solidity/src/authorization/Authorization.sol @@ -0,0 +1,629 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.28; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {ProcessorBase} from "../processor/ProcessorBase.sol"; +import {IProcessorMessageTypes} from "../processor/interfaces/IProcessorMessageTypes.sol"; +import {ProcessorMessageDecoder} from "../processor/libs/ProcessorMessageDecoder.sol"; +import {ICallback} from "../processor/interfaces/ICallback.sol"; +import {IProcessor} from "../processor/interfaces/IProcessor.sol"; +import {VerificationGateway} from "../verification/VerificationGateway.sol"; +import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; + +/** + * @title Authorization + * @dev This contract manages authorizations for interactions with a processor contract. + * It provides mechanisms for both standard address-based authorizations and ZK proof-based authorizations. + * @notice The Authorization contract acts as a middleware for managing access control + * to the Processor contract. It controls which addresses can call specific functions + * on specific contracts through the processor. + * It will receive callbacks from the processor after executing messages and can either store + * the callback data in its state or just emit events for them. + */ +contract Authorization is Ownable, ICallback, ReentrancyGuard { + // Address of the processor that we will forward batches to + ProcessorBase public processor; + + modifier onlyProcessor() { + if (msg.sender != address(processor)) { + revert("Only processor can call this function"); + } + _; + } + + /** + * @notice Boolean indicating whether to store callbacks or just emit events for them + * @dev If true, the contract will store callback data in the contract's state + */ + bool public storeCallbacks; + + /** + * @notice Event emitted when a callback is received from the processor + * @dev This event is emitted when the processor sends a callback after executing a message + * @param executionId The ID of the executed message + * @param executionResult The result of the execution (success or failure) + * @param executedCount The number of successfully executed functions + * @param data Additional data related to the callback execution + */ + event CallbackReceived( + uint64 indexed executionId, IProcessor.ExecutionResult executionResult, uint64 executedCount, bytes data + ); + /** + * @notice Event emitted when an admin address is added + * @dev This event is emitted when a new admin address is added to the list of authorized addresses + * @dev Only used for Standard authorizations + * @param admin The address that was added as an admin + */ + event AdminAddressAdded(address indexed admin); + /** + * @notice Event emitted when an admin address is removed + * @dev This event is emitted when an admin address is removed from the list of authorized addresses + * @dev Only used for Standard authorizations + * @param admin The address that was removed from the admin list + */ + event AdminAddressRemoved(address indexed admin); + /** + * @notice Event emitted when an authorization is added + * @dev This event is emitted when a new authorization is granted to a user for a specific contract and function + * @dev Only used for Standard authorizations + * @param user The address of the user that was granted authorization. If address(0) is used, then it's permissionless + * @param contractAddress The address of the contract the user is authorized to interact with + * @param callHash The hash of the function call that the user is authorized to execute + */ + event AuthorizationAdded(address indexed user, address indexed contractAddress, bytes32 indexed callHash); + /** + * @notice Event emitted when an authorization is removed + * @dev This event is emitted when an authorization is revoked from a user for a specific contract and function + * @dev Only used for Standard authorizations + * @param user The address of the user that had authorization revoked. If address(0) is used, then it's permissionless + * @param contractAddress The address of the contract the user had authorization for + * @param callHash The hash of the function call that the user had authorization to execute + */ + event AuthorizationRemoved(address indexed user, address indexed contractAddress, bytes32 indexed callHash); + + /** + * @notice Callback data structure for processor callbacks + * @dev This struct is used to store the callback data received from the processor + * @param executionResult The result of the execution + * @param executedCount The number of successfully executed functions + * @param data Additional data related to the callback execution + */ + struct ProcessorCallback { + IProcessor.ExecutionResult executionResult; + uint64 executedCount; + bytes data; + } + + /** + * @notice Mapping of execution IDs to callback data + * @dev This mapping stores the callback data for each execution ID + * Key: execution ID, Value: Callback information + */ + mapping(uint64 => ProcessorCallback) public callbacks; + + /** + * @notice Current execution ID for tracking message execution + * @dev This ID is incremented with each message processed and helps track message sequence + */ + uint64 public executionId; + + // ========================= Standard authorizations ========================= + + /** + * @notice Mapping of addresses that are allowed to perform admin operations + * @dev Admin addresses can perform privileged operations like pausing/unpausing + */ + mapping(address => bool) public adminAddresses; + + /** + * @notice Multi-dimensional mapping for granular authorization control + * @dev Maps from user address -> contract address -> function signature hash -> boolean + * If address(0) is used as the user address, it indicates permissionless access + * Represents the operations a specific address can execute on a specific contract + */ + mapping(address => mapping(address => mapping(bytes32 => bool))) public authorizations; + + // ========================= ZK authorizations ========================= + + /** + * @notice Address of the verification gateway contract used for zero-knowledge proof verification + * @dev If zero-knowledge proofs are not being used, this can be set to address(0) + */ + VerificationGateway public verificationGateway; + + /** + * @notice Structure representing a ZK message that we'll get a proof for + * @dev This structure contains all the information to know if the sender is authorized to provide this message and to prevent replay attacks + * @param registry An ID to identify this message, similar to the label on CosmWasm authorizations + * @param blockNumber The block number when the message was created + * @param authorizationContract The address of the authorization contract that this message is for. If address(0) is used, then it's valid for any contract + * @param processorMessage The actual message to be processed and that was proven + */ + struct ZKMessage { + uint64 registry; + uint64 blockNumber; + address authorizationContract; + IProcessorMessageTypes.ProcessorMessage processorMessage; + } + + /** + * @notice Mapping of what addresses are authorized to send messages for a specific registry ID + * @dev This mapping is used to check if a user is authorized to send a message for a specific registry ID + * @dev The mapping is structured as follows: + * registry ID -> user addresses + * If address(0) is used as the user address, it indicates permissionless access + */ + mapping(uint64 => address[]) public zkAuthorizations; + + /** + * @notice Mapping of registry ID to boolean indicating if we need to validate the last block execution + * @dev This mapping is used to check if we need to validate the last block execution for a specific registry ID + * @dev The mapping is structured as follows: + * registry ID -> boolean indicating if we need to validate the last block execution + */ + mapping(uint64 => bool) public validateBlockNumberExecution; + /** + * @notice Mapping of the last block a proof was executed for + * @dev This mapping is used to prevent replay attacks by ensuring that proofs that are older or the same than the last executed one cannot be used + * @dev This is important to ensure that the same or a previous proof cannot be used + * @dev The mapping is structured as follows: + * registry ID -> last block number of the proof executed + */ + mapping(uint64 => uint64) public zkAuthorizationLastExecutionBlock; + + // ========================= Implementation ========================= + + /** + * @notice Sets up the Authorization contract with initial configuration + * @dev Initializes the contract with owner, processor, and optional verifier + * @param _owner Address that will be set as the owner of this contract + * @param _processor Address of the processor contract that will execute messages + * @param _verificationGateway Address of the ZK verification gateway contract (can be address(0) if not using ZK proofs) + * @param _storeCallbacks Boolean indicating whether to store callbacks or just emitting events + */ + constructor(address _owner, address _processor, address _verificationGateway, bool _storeCallbacks) + Ownable(_owner) + { + if (_processor == address(0)) { + revert("Processor cannot be zero address"); + } + processor = ProcessorBase(_processor); + verificationGateway = VerificationGateway(_verificationGateway); + executionId = 0; + storeCallbacks = _storeCallbacks; + } + + /** + * @notice Updates the processor contract address + * @dev Can only be called by the owner + * @param _processor New processor contract address + */ + function updateProcessor(address _processor) external onlyOwner { + if (_processor == address(0)) { + revert("Processor cannot be zero address"); + } + processor = ProcessorBase(_processor); + } + + /** + * @notice Updates the ZK verification gateway contract address + * @dev Can only be called by the owner + * @param _verificationGateway New verificationGateway contract address + */ + function updateVerificationGateway(address _verificationGateway) external onlyOwner { + verificationGateway = VerificationGateway(_verificationGateway); + } + + // ========================= Standard Authorizations ========================= + + /** + * @notice Adds an address to the list of admin addresses + * @dev Can only be called by the owner + * @param _admin Address to be granted admin privileges + */ + function addAdminAddress(address _admin) external onlyOwner { + adminAddresses[_admin] = true; + emit AdminAddressAdded(_admin); + } + + /** + * @notice Removes an address from the list of admin addresses + * @dev Can only be called by the owner + * @param _admin Address to have admin privileges revoked + */ + function removeAdminAddress(address _admin) external onlyOwner { + delete adminAddresses[_admin]; + emit AdminAddressRemoved(_admin); + } + + /** + * @notice Grants authorization for multiple users to call specific functions on specific contracts + * @dev Can only be called by the owner + * @param _users Array of addresses being granted authorization, if address(0) is used, then it's permissionless + * @param _contracts Array of contract addresses the users are authorized to interact with + * @param _calls Array of function call data (used to generate hashes for authorization checking) + */ + function addStandardAuthorizations(address[] memory _users, address[] memory _contracts, bytes[] memory _calls) + external + onlyOwner + { + // Check that the arrays are the same length + // We are allowing adding multiple authorizations at once for gas optimization + // The arrays must be the same length because for each user we have a contract and a call + require(_users.length == _contracts.length && _contracts.length == _calls.length, "Array lengths must match"); + + for (uint256 i = 0; i < _users.length; i++) { + bytes32 callHash = keccak256(_calls[i]); + authorizations[_users[i]][_contracts[i]][callHash] = true; + emit AuthorizationAdded(_users[i], _contracts[i], callHash); + } + } + + /** + * @notice Revokes authorization for multiple users to call specific functions on specific contracts + * @dev Can only be called by the owner + * @param _users Array of addresses having authorization revoked + * @param _contracts Array of contract addresses the authorizations apply to + * @param _calls Array of function call data (used to generate the hashes for lookup) + */ + function removeStandardAuthorizations(address[] memory _users, address[] memory _contracts, bytes[] memory _calls) + external + onlyOwner + { + require(_users.length == _contracts.length && _contracts.length == _calls.length, "Array lengths must match"); + + for (uint256 i = 0; i < _users.length; i++) { + address user = _users[i]; + address contractAddress = _contracts[i]; + bytes32 callHash = keccak256(_calls[i]); + delete authorizations[user][contractAddress][callHash]; + emit AuthorizationRemoved(user, contractAddress, callHash); + } + } + + /** + * @notice Main function to send messages to the processor after authorization checks + * @dev Delegates to specialized helper functions based on message type + * @param _message Encoded processor message to be executed + */ + function sendProcessorMessage(bytes calldata _message) external nonReentrant { + // Make a copy of the message to apply modifications + bytes memory message = _message; + + // Decode the message to check authorization and apply modifications + IProcessorMessageTypes.ProcessorMessage memory decodedMessage = ProcessorMessageDecoder.decode(message); + + // Process message based on type + if (decodedMessage.messageType == IProcessorMessageTypes.ProcessorMessageType.SendMsgs) { + message = _handleSendMsgsMessage(decodedMessage); + } else if (decodedMessage.messageType == IProcessorMessageTypes.ProcessorMessageType.InsertMsgs) { + message = _handleInsertMsgsMessage(decodedMessage); + } else { + _requireAdminAccess(); + } + + // Forward the validated and modified message to the processor + processor.execute(message); + + // Increment the execution ID for the next message + executionId++; + } + + /** + * @notice Handle InsertMsgs type messages + * @dev Requires admin access and sets execution ID + * @param decodedMessage The decoded processor message + * @return The modified encoded message + */ + function _handleInsertMsgsMessage(IProcessorMessageTypes.ProcessorMessage memory decodedMessage) + private + view + returns (bytes memory) + { + _requireAdminAccess(); + + IProcessorMessageTypes.InsertMsgs memory insertMsgs = + abi.decode(decodedMessage.message, (IProcessorMessageTypes.InsertMsgs)); + + // Set the execution ID of the message + insertMsgs.executionId = executionId; + + // Encode the message back after modification + decodedMessage.message = abi.encode(insertMsgs); + + // Return the encoded processor message + return abi.encode(decodedMessage); + } + + /** + * @notice Handle SendMsgs type messages + * @dev Checks function-level authorizations and modifies priority and execution ID + * @param decodedMessage The decoded processor message + * @return The modified encoded message + */ + function _handleSendMsgsMessage(IProcessorMessageTypes.ProcessorMessage memory decodedMessage) + private + view + returns (bytes memory) + { + // Decode the SendMsgs message + IProcessorMessageTypes.SendMsgs memory sendMsgs = + abi.decode(decodedMessage.message, (IProcessorMessageTypes.SendMsgs)); + + // Verify authorizations based on subroutine type + if (sendMsgs.subroutine.subroutineType == IProcessorMessageTypes.SubroutineType.Atomic) { + _verifyAtomicSubroutineAuthorization(sendMsgs); + } else { + _verifyNonAtomicSubroutineAuthorization(sendMsgs); + } + + // Apply standard modifications to all SendMsgs + sendMsgs.priority = IProcessorMessageTypes.Priority.Medium; + sendMsgs.executionId = executionId; + + // Encode the message back after modifications + decodedMessage.message = abi.encode(sendMsgs); + + // Return the encoded processor message + return abi.encode(decodedMessage); + } + + /** + * @notice Verify authorization for atomic subroutine messages + * @dev Checks that each function call is authorized for the sender + * @param sendMsgs The SendMsgs message containing the atomic subroutine + */ + function _verifyAtomicSubroutineAuthorization(IProcessorMessageTypes.SendMsgs memory sendMsgs) private view { + IProcessorMessageTypes.AtomicSubroutine memory atomicSubroutine = + abi.decode(sendMsgs.subroutine.subroutine, (IProcessorMessageTypes.AtomicSubroutine)); + + // Verify message and function array lengths match + if (atomicSubroutine.functions.length > 0 && atomicSubroutine.functions.length != sendMsgs.messages.length) { + revert("Subroutine functions length does not match messages length"); + } + + // Check authorization for each function in the atomic subroutine + for (uint256 i = 0; i < atomicSubroutine.functions.length; i++) { + if ( + !_checkAddressIsAuthorized( + msg.sender, atomicSubroutine.functions[i].contractAddress, sendMsgs.messages[i] + ) + ) { + revert("Unauthorized access"); + } + } + } + + /** + * @notice Verify authorization for non-atomic subroutine messages + * @dev Checks that each function call is authorized for the sender + * @param sendMsgs The SendMsgs message containing the non-atomic subroutine + */ + function _verifyNonAtomicSubroutineAuthorization(IProcessorMessageTypes.SendMsgs memory sendMsgs) private view { + IProcessorMessageTypes.NonAtomicSubroutine memory nonAtomicSubroutine = + abi.decode(sendMsgs.subroutine.subroutine, (IProcessorMessageTypes.NonAtomicSubroutine)); + + // Verify message and function array lengths match + if ( + nonAtomicSubroutine.functions.length > 0 && nonAtomicSubroutine.functions.length != sendMsgs.messages.length + ) { + revert("Subroutine functions length does not match messages length"); + } + + // Check authorization for each function in the non-atomic subroutine + for (uint256 i = 0; i < nonAtomicSubroutine.functions.length; i++) { + if ( + !_checkAddressIsAuthorized( + msg.sender, nonAtomicSubroutine.functions[i].contractAddress, sendMsgs.messages[i] + ) + ) { + revert("Unauthorized access"); + } + } + } + + /** + * @notice Require that sender has admin access + * @dev Reverts if sender is not in the adminAddresses mapping + */ + function _requireAdminAccess() private view { + if (!adminAddresses[msg.sender]) { + revert("Unauthorized access"); + } + } + + /** + * @notice Checks if an address is authorized to execute a specific call on a specific contract + * @dev Uses the authorizations mapping to perform the check + * @param _address Address to check authorization for + * @param _contract Address of the contract being called + * @param _call Function call data (used to generate the hash for lookup) + * @return bool True if the address is authorized, false otherwise + */ + function _checkAddressIsAuthorized(address _address, address _contract, bytes memory _call) + internal + view + returns (bool) + { + // Check if the address is authorized to call the contract with the given call + if (authorizations[_address][_contract][keccak256(_call)]) { + return true; + } else if (authorizations[address(0)][_contract][keccak256(_call)]) { + // If address(0) is used, it indicates permissionless access + return true; + } else { + return false; + } + } + + // ========================= ZK authorizations ========================= + + /** + * @notice Adds a new registry with its associated users and verification keys + * @dev This function allows the owner to add multiple registries and their associated users and verification keys + * @param registries Array of registry IDs to be added + * @param users Array of arrays of user addresses associated with each registry + * @param vks Array of verification keys associated with each registry + * @param validateBlockNumber Array of booleans indicating if we need to validate the last block execution for each registry + */ + function addRegistries( + uint64[] memory registries, + address[][] memory users, + bytes32[] calldata vks, + bool[] memory validateBlockNumber + ) external onlyOwner { + // Since we are allowing multiple registries to be added at once, we need to check that the arrays are the same length + // because for each registry we have a list of users, a verification key and a boolean + // Allowing multiple to be added is useful for gas optimization + require( + users.length == registries.length && users.length == vks.length + && users.length == validateBlockNumber.length, + "Array lengths must match" + ); + + for (uint256 i = 0; i < registries.length; i++) { + // Add the registry to the verification gateway + verificationGateway.addRegistry(registries[i], vks[i]); + zkAuthorizations[registries[i]] = users[i]; + // Only store if true because default is false + if (validateBlockNumber[i]) { + validateBlockNumberExecution[registries[i]] = true; + } + } + } + + /** + * @notice Removes a registry and its associated users + * @dev This function allows the owner to remove a registry and its associated users + * @param registries Array of registry IDs to be removed + */ + function removeRegistries(uint64[] memory registries) external onlyOwner { + for (uint256 i = 0; i < registries.length; i++) { + // Remove the registry from the verification gateway + verificationGateway.removeRegistry(registries[i]); + delete zkAuthorizations[registries[i]]; + // Delete the last execution block for the registry + delete zkAuthorizationLastExecutionBlock[registries[i]]; + // Delete the validation flag for the registry + delete validateBlockNumberExecution[registries[i]]; + } + } + + /** + * @notice Get all authorized addresses for a specific registry ID + * @param registryId The registry ID to check + * @return An array of all authorized addresses for the given registry ID + * @dev This function returns all addresses that are authorized to send messages for the given registry ID + * @dev It's useful for checking which addresses have permission to send messages in one go + */ + function getZkAuthorizationsList(uint64 registryId) public view returns (address[] memory) { + return zkAuthorizations[registryId]; + } + + /** + * @notice Executes a ZK message with proof verification + * @dev This function verifies the proof and executes the message if authorized + * @dev The proof is verified using the verification gateway before executing the message + * @param _message Encoded ZK message to be executed + * @param _proof Proof associated with the ZK message + */ + function executeZKMessage(bytes calldata _message, bytes calldata _proof) external nonReentrant { + // Check that the verification gateway is set + if (address(verificationGateway) == address(0)) { + revert("Verification gateway not set"); + } + + // Decode the message to check authorization and apply modifications + // We need to skip the first 32 bytes because this will be the coprocessor root which we don't need to decode + ZKMessage memory decodedZKMessage = abi.decode(_message[32:], (ZKMessage)); + + // Check that the message is valid for this authorization contract + if ( + decodedZKMessage.authorizationContract != address(0) + && decodedZKMessage.authorizationContract != address(this) + ) { + revert("Invalid authorization contract"); + } + + // Check that sender is authorized to send this message + address[] memory authorizedAddresses = zkAuthorizations[decodedZKMessage.registry]; + bool isAuthorized = false; + for (uint256 i = 0; i < authorizedAddresses.length; i++) { + if (authorizedAddresses[i] == msg.sender || authorizedAddresses[i] == address(0)) { + isAuthorized = true; + break; + } + } + + if (!isAuthorized) { + revert("Unauthorized address for this registry"); + } + + // Cache the validate block condition + bool validateBlockNumberExecCondition = validateBlockNumberExecution[decodedZKMessage.registry]; + + // If we need to validate the last block execution, check that the block number is greater than the last one + if (validateBlockNumberExecCondition) { + if (decodedZKMessage.blockNumber <= zkAuthorizationLastExecutionBlock[decodedZKMessage.registry]) { + revert("Proof no longer valid"); + } + } + + // Verify the proof using the verification gateway + if (!verificationGateway.verify(decodedZKMessage.registry, _proof, _message)) { + revert("Proof verification failed"); + } + + // Get the message and update the execution ID if it's a SendMsgs or InsertMsgs message, according to the + // current execution ID of the contract + if (decodedZKMessage.processorMessage.messageType == IProcessorMessageTypes.ProcessorMessageType.SendMsgs) { + IProcessorMessageTypes.SendMsgs memory sendMsgs = + abi.decode(decodedZKMessage.processorMessage.message, (IProcessorMessageTypes.SendMsgs)); + sendMsgs.executionId = executionId; + decodedZKMessage.processorMessage.message = abi.encode(sendMsgs); + } else if ( + decodedZKMessage.processorMessage.messageType == IProcessorMessageTypes.ProcessorMessageType.InsertMsgs + ) { + IProcessorMessageTypes.InsertMsgs memory insertMsgs = + abi.decode(decodedZKMessage.processorMessage.message, (IProcessorMessageTypes.InsertMsgs)); + insertMsgs.executionId = executionId; + decodedZKMessage.processorMessage.message = abi.encode(insertMsgs); + } + + // Increment the execution ID for the next message + executionId++; + + // Update the last execution block for the registry (only if we need to validate the last block execution) + if (validateBlockNumberExecCondition) { + zkAuthorizationLastExecutionBlock[decodedZKMessage.registry] = decodedZKMessage.blockNumber; + } + + // Execute the message using the processor + processor.execute(abi.encode(decodedZKMessage.processorMessage)); + } + + // ========================= Processor Callbacks ========================= + + /** + * @notice Handles callbacks from the processor after executing messages + * @dev This function is called by the processor to notify the contract of execution results + * @param callbackData Encoded callback data containing execution result and other information + */ + function handleCallback(bytes memory callbackData) external override onlyProcessor { + // Decode the callback data + IProcessor.Callback memory callback = abi.decode(callbackData, (IProcessor.Callback)); + + // Store the callback data if storeCallbacks is true + if (storeCallbacks) { + callbacks[callback.executionId] = ProcessorCallback({ + executionResult: callback.executionResult, + executedCount: uint64(callback.executedCount), + data: callback.data + }); + } + + emit CallbackReceived( + callback.executionId, callback.executionResult, uint64(callback.executedCount), callback.data + ); + } +} diff --git a/solidity/src/libraries/Forwarder.sol b/solidity/src/libraries/Forwarder.sol index f84e269fe..55c38d971 100644 --- a/solidity/src/libraries/Forwarder.sol +++ b/solidity/src/libraries/Forwarder.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.28; import {Library} from "./Library.sol"; -import {Account} from "../accounts/Account.sol"; +import {BaseAccount} from "../accounts/BaseAccount.sol"; import {IERC20} from "forge-std/src/interfaces/IERC20.sol"; /** @@ -37,8 +37,8 @@ contract Forwarder is Library { * @param minInterval Minimum interval between forwards */ struct ForwarderConfig { - Account inputAccount; - Account outputAccount; + BaseAccount inputAccount; + BaseAccount outputAccount; ForwardingConfig[] forwardingConfigs; IntervalType intervalType; uint64 minInterval; @@ -109,8 +109,8 @@ contract Forwarder is Library { function forward() external onlyProcessor { _checkInterval(); _updateLastExecution(); - Account input = config.inputAccount; - Account output = config.outputAccount; + BaseAccount input = config.inputAccount; + BaseAccount output = config.outputAccount; for (uint8 i = 0; i < config.forwardingConfigs.length; i++) { ForwardingConfig memory fConfig = config.forwardingConfigs[i]; @@ -142,7 +142,7 @@ contract Forwarder is Library { * @param input Source account * @param output Destination account */ - function _forwardToken(ForwardingConfig memory fConfig, Account input, Account output) private { + function _forwardToken(ForwardingConfig memory fConfig, BaseAccount input, BaseAccount output) private { // Check if what we are trying to forward is the native coin or ERC20 bool isNativeCoin = _isNativeCoin(fConfig.tokenAddress); diff --git a/solidity/src/processor/ProcessorBase.sol b/solidity/src/processor/ProcessorBase.sol index b523e98d5..c136ba09e 100644 --- a/solidity/src/processor/ProcessorBase.sol +++ b/solidity/src/processor/ProcessorBase.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.28; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {ProcessorErrors} from "./libs/ProcessorErrors.sol"; import {IProcessorMessageTypes} from "./interfaces/IProcessorMessageTypes.sol"; import {IProcessor} from "./interfaces/IProcessor.sol"; @@ -8,7 +9,7 @@ import {IMailbox} from "hyperlane/interfaces/IMailbox.sol"; import {ICallback} from "./interfaces/ICallback.sol"; import {ProcessorEvents} from "./libs/ProcessorEvents.sol"; -abstract contract ProcessorBase { +abstract contract ProcessorBase is Ownable { /** * @notice The authorization contract that can send messages from the main domain * @dev Stored as bytes32 to handle cross-chain address representation @@ -39,9 +40,9 @@ abstract contract ProcessorBase { /** * @notice Initializes the state variables - * @param _authorizationContract The authorization contract address in bytes32 - * @param _mailbox The Hyperlane mailbox address - * @param _originDomain The origin domain ID for sending callbacks + * @param _authorizationContract The authorization contract address in bytes32. If we dont want to use Hyperlane, just pass empty bytes here. + * @param _mailbox The Hyperlane mailbox address. If we don't want to use Hyperlane, we can set it to address(0). + * @param _originDomain The origin domain ID for sending callbacks. If address(0) is used for mailbox, this will be not be used. * @param _authorizedAddresses The addresses authorized to interact with the processor directly */ constructor( @@ -49,10 +50,7 @@ abstract contract ProcessorBase { address _mailbox, uint32 _originDomain, address[] memory _authorizedAddresses - ) { - if (_mailbox == address(0)) { - revert ProcessorErrors.InvalidAddress(); - } + ) Ownable(msg.sender) { authorizationContract = _authorizationContract; mailbox = IMailbox(_mailbox); originDomain = _originDomain; @@ -294,4 +292,39 @@ abstract contract ProcessorBase { // Emit an event to track the callback transmission emit ProcessorEvents.CallbackSent(callback.executionId, callback.executionResult, callback.executedCount); } + + /** + * @notice Adds an address to the list of authorized addresses + * @dev Only callable by the contract owner + * @param _address The address to be authorized + */ + function addAuthorizedAddress(address _address) public onlyOwner { + // Check that address is not the zero address + if (_address == address(0)) { + revert ProcessorErrors.InvalidAddress(); + } + + // Check that address is not already authorized + if (authorizedAddresses[_address]) { + revert ProcessorErrors.AddressAlreadyAuthorized(); + } + + authorizedAddresses[_address] = true; + emit ProcessorEvents.AuthorizedAddressAdded(_address); + } + + /** + * @notice Removes an address from the list of authorized addresses + * @dev Only callable by the contract owner + * @param _address The address to be removed from the authorized list + */ + function removeAuthorizedAddress(address _address) public onlyOwner { + // Check that address is currently authorized + if (!authorizedAddresses[_address]) { + revert ProcessorErrors.AddressNotAuthorized(); + } + + delete authorizedAddresses[_address]; + emit ProcessorEvents.AuthorizedAddressRemoved(_address); + } } diff --git a/solidity/src/processor/libs/ProcessorErrors.sol b/solidity/src/processor/libs/ProcessorErrors.sol index 8e8911ba3..b2a38e446 100644 --- a/solidity/src/processor/libs/ProcessorErrors.sol +++ b/solidity/src/processor/libs/ProcessorErrors.sol @@ -4,8 +4,10 @@ pragma solidity ^0.8.28; library ProcessorErrors { error UnauthorizedAccess(); error NotAuthorizationContract(); - error InvalidAddress(); error ProcessorPaused(); error UnsupportedOperation(); error InvalidOriginDomain(); + error InvalidAddress(); + error AddressAlreadyAuthorized(); + error AddressNotAuthorized(); } diff --git a/solidity/src/processor/libs/ProcessorEvents.sol b/solidity/src/processor/libs/ProcessorEvents.sol index db0e50c02..05bf29c90 100644 --- a/solidity/src/processor/libs/ProcessorEvents.sol +++ b/solidity/src/processor/libs/ProcessorEvents.sol @@ -14,6 +14,16 @@ library ProcessorEvents { */ event ProcessorWasResumed(); + /** + * @notice Emitted when an address is allowed to send messages to the processor + */ + event AuthorizedAddressAdded(address addr); + + /** + * @notice Emitted when an address is removed from the list of authorized senders + */ + event AuthorizedAddressRemoved(address addr); + /** * @notice Emitted when a callback is sent to the hyperlane mailbox * @param executionId The Execution ID of the message(s) that triggered the callback diff --git a/solidity/src/verification/SP1VerificationGateway.sol b/solidity/src/verification/SP1VerificationGateway.sol new file mode 100644 index 000000000..03b05c715 --- /dev/null +++ b/solidity/src/verification/SP1VerificationGateway.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.28; + +import {VerificationGateway} from "./VerificationGateway.sol"; +import {ISP1Verifier} from "succinctlabs-sp1-contracts/src/ISP1Verifier.sol"; + +/** + * @title SP1VerificationGateway + * @dev Specific implementation of VerificationGateway for the SP1 verifier + */ +contract SP1VerificationGateway is VerificationGateway { + /** + * @notice Returns the verifier cast to the ISP1Verifier interface + * @return The verifier as an ISP1Verifier + */ + function getVerifier() public view returns (ISP1Verifier) { + return ISP1Verifier(verifier); + } + + constructor() VerificationGateway() {} + + /** + * @dev Function that should revert when `msg.sender` is not authorized to upgrade the contract. Called by + * {upgradeTo} and {upgradeToAndCall}. + * + * Normally, this function will use an xref:access.adoc[access control] modifier such as {Ownable-onlyOwner}. + * + * @param newImplementation address of the new implementation + */ + function _authorizeUpgrade(address newImplementation) internal override onlyOwner { + // Upgrade logic comes here + } + + /** + * @notice Verifies a proof using the SP1 verifier + * @param registry The registry used in verification + * @param proof The proof to verify + * @param message The message associated with the proof + */ + function verify(uint64 registry, bytes calldata proof, bytes calldata message) + external + view + override + returns (bool) + { + // Get the VK for the sender and the registry + bytes32 vk = programVKs[msg.sender][registry]; + + // If the VK is not set, revert + require(vk != bytes32(0), "VK not set for sender and registry"); + + // Call the specific verifier + ISP1Verifier sp1Verifier = getVerifier(); + + sp1Verifier.verifyProof(vk, proof, message); + + return true; + } +} diff --git a/solidity/src/verification/VerificationGateway.sol b/solidity/src/verification/VerificationGateway.sol new file mode 100644 index 000000000..eed942ba6 --- /dev/null +++ b/solidity/src/verification/VerificationGateway.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.28; + +import {OwnableUpgradeable} from "@openzeppelin-contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {UUPSUpgradeable} from "@openzeppelin-contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {Initializable} from "@openzeppelin-contracts-upgradeable/proxy/utils/Initializable.sol"; + +/** + * @title VerificationGateway + * @dev Abstract contract that serves as a base for verification gateways. + * This contract provides the foundation for verifying proofs against registered verification keys. + */ +abstract contract VerificationGateway is Initializable, OwnableUpgradeable, UUPSUpgradeable { + /// @notice Root hash of the ZK coprocessor + bytes32 public coprocessorRoot; + + /// @notice Generic verifier address that will be specialized in derived contracts + address public verifier; + + /** + * @notice Mapping of program verification keys by user address and registry ID + * @dev Maps: user address => registry ID => verification key + */ + mapping(address => mapping(uint64 => bytes32)) public programVKs; + + // Storage gap - reserves slots for future versions + uint256[50] private __gap; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /** + * @notice Initializes the verification gateway replcaing the constructor with a coprocessor root and verifier address + * @param _coprocessorRoot The root hash of the coprocessor + * @param _verifier Address of the verification contract + */ + function initialize(bytes32 _coprocessorRoot, address _verifier) external initializer { + __Ownable_init(msg.sender); + __UUPSUpgradeable_init(); + coprocessorRoot = _coprocessorRoot; + require(_verifier != address(0), "Verifier cannot be zero address"); + verifier = _verifier; + } + + /** + * @notice Updates the verifier address + * @dev Only the owner can update the verifier address + * @param _verifier The new verifier address + */ + function updateVerifier(address _verifier) external onlyOwner { + require(_verifier != address(0), "Verifier cannot be zero address"); + verifier = _verifier; + } + + /** + * @notice Adds a verification key for a specific registry ID + * @dev Only the sender can add a VK for their own address + * @param registry The registry ID to associate with the verification key + * @param vk The verification key to register + */ + function addRegistry(uint64 registry, bytes32 vk) external { + programVKs[msg.sender][registry] = vk; + } + + /** + * @notice Removes a verification key for a specific registry ID + * @dev Only the sender can remove a VK for their own address + * @param registry The registry ID to remove + */ + function removeRegistry(uint64 registry) external { + delete programVKs[msg.sender][registry]; + } + + /** + * @notice Abstract verification function to be implemented by derived contracts + * @dev Different verification gateways will implement their own verification logic + * @param registry The registry data used in verification + * @param proof The proof to verify + * @param message The message associated with the proof + * @return True if the proof is valid, false or revert otherwise + */ + function verify(uint64 registry, bytes calldata proof, bytes calldata message) external virtual returns (bool); +} diff --git a/solidity/test/authorization/AuthorizationStandard.t.sol b/solidity/test/authorization/AuthorizationStandard.t.sol new file mode 100644 index 000000000..818dd78c0 --- /dev/null +++ b/solidity/test/authorization/AuthorizationStandard.t.sol @@ -0,0 +1,412 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.28; + +import {Test} from "forge-std/src/Test.sol"; +import {Authorization} from "../../src/authorization/Authorization.sol"; +import {Forwarder} from "../../src/libraries/Forwarder.sol"; +import {ProcessorBase} from "../../src/processor/ProcessorBase.sol"; +import {LiteProcessor} from "../../src/processor/LiteProcessor.sol"; +import {BaseAccount} from "../../src/accounts/BaseAccount.sol"; +import {IProcessorMessageTypes} from "../../src/processor/interfaces/IProcessorMessageTypes.sol"; +import {IProcessor} from "../../src/processor/interfaces/IProcessor.sol"; + +/** + * @title AuthorizationStandardTest + * @notice Test suite for the Authorization contract, verifying access control, + * processor message handling, and configuration management + */ +contract AuthorizationStandardTest is Test { + Authorization auth; + LiteProcessor processor; + Forwarder forwarder; + BaseAccount inputAccount; + BaseAccount outputAccount; + BaseAccount newOutputAccount; + + address owner = address(0x1); + address admin = address(0x2); + address user = address(0x3); + address[] users = new address[](1); + address[] contracts = new address[](1); + bytes[] calls = new bytes[](1); + address unauthorized = address(0x4); + address mockERC20 = address(0x5); + + uint256 constant MAX_AMOUNT = 1 ether; + uint64 constant MIN_INTERVAL = 3600; + + bytes updateConfigCall; + + function setUp() public { + vm.startPrank(owner); + + // Deploy main contracts + processor = new LiteProcessor(bytes32(0), address(0), 0, new address[](0)); + auth = new Authorization(owner, address(processor), address(0), true); + + // Configure processor authorization + processor.addAuthorizedAddress(address(auth)); + + // Set up accounts + inputAccount = new BaseAccount(owner, new address[](0)); + outputAccount = new BaseAccount(owner, new address[](0)); + newOutputAccount = new BaseAccount(owner, new address[](0)); + + // Create and deploy forwarder with configuration + bytes memory forwarderConfig = createForwarderConfig(address(outputAccount)); + forwarder = new Forwarder(address(processor), address(processor), forwarderConfig); + + // Set library approval + inputAccount.approveLibrary(address(forwarder)); + + // Create a new configuration for the forwarder that will be used to update + bytes memory newForwarderConfig = createForwarderConfig(address(newOutputAccount)); + + // Cache common test data + updateConfigCall = abi.encodeWithSelector(Forwarder.updateConfig.selector, newForwarderConfig); + + // Create arrays for the batch function + users[0] = user; + contracts[0] = address(forwarder); + calls[0] = updateConfigCall; + + vm.stopPrank(); + } + + // ======================= ACCESS CONTROL TESTS ======================= + + function test_RevertWhen_SendProcessorMessageUnauthorized() public { + vm.startPrank(unauthorized); + + // Create processor message with unauthorized function call + bytes memory encodedMessage = createSendMsgsMessage(updateConfigCall); + + // Should fail because unauthorized user + vm.expectRevert("Unauthorized access"); + auth.sendProcessorMessage(encodedMessage); + + vm.stopPrank(); + } + + function testAdminSendNonSendMsgsMessage() public { + // Add admin address + vm.prank(owner); + auth.addAdminAddress(admin); + + vm.startPrank(admin); + + // Create Pause message (admin-only) + IProcessorMessageTypes.ProcessorMessage memory processorMessage = IProcessorMessageTypes.ProcessorMessage({ + messageType: IProcessorMessageTypes.ProcessorMessageType.Pause, + message: bytes("") + }); + bytes memory encodedMessage = abi.encode(processorMessage); + + // Execute and verify + auth.sendProcessorMessage(encodedMessage); + assertTrue(processor.paused(), "Processor should be paused"); + + // Create Resume message (admin-only) + processorMessage = IProcessorMessageTypes.ProcessorMessage({ + messageType: IProcessorMessageTypes.ProcessorMessageType.Resume, + message: bytes("") + }); + encodedMessage = abi.encode(processorMessage); + + // Execute and verify + auth.sendProcessorMessage(encodedMessage); + assertFalse(processor.paused(), "Processor should be resumed"); + + vm.stopPrank(); + } + + function test_RevertWhen_NonAdminSendNonSendMsgsMessage() public { + vm.startPrank(user); + + // Create Pause message (admin-only) + IProcessorMessageTypes.ProcessorMessage memory processorMessage = IProcessorMessageTypes.ProcessorMessage({ + messageType: IProcessorMessageTypes.ProcessorMessageType.Pause, + message: bytes("") + }); + bytes memory encodedMessage = abi.encode(processorMessage); + + // Should fail because user is not admin + vm.expectRevert("Unauthorized access"); + auth.sendProcessorMessage(encodedMessage); + + vm.stopPrank(); + } + + // ======================= CONFIGURATION TESTS ======================= + + function testUpdateProcessor() public { + vm.startPrank(owner); + + // Deploy a new processor + ProcessorBase newProcessor = new LiteProcessor(bytes32(0), address(0), 0, new address[](0)); + + // Update and verify + auth.updateProcessor(address(newProcessor)); + assertEq(address(auth.processor()), address(newProcessor), "Processor should be updated"); + + vm.stopPrank(); + } + + function test_RevertWhen_UpdateProcessorWithZeroAddress() public { + vm.prank(owner); + vm.expectRevert("Processor cannot be zero address"); + auth.updateProcessor(address(0)); + } + + /** + * @notice Test updating the verification gateway address + */ + function testUpdateVerificationGateway() public { + vm.startPrank(owner); + + address newVerificationGateway = address(0x9); + auth.updateVerificationGateway(newVerificationGateway); + assertEq(address(auth.verificationGateway()), newVerificationGateway, "Verification Gateway should be updated"); + + vm.stopPrank(); + } + + function test_RevertWhen_HandleCallbackUnauthorized() public { + vm.startPrank(unauthorized); + + // Create a callback + IProcessor.Callback memory callback = IProcessor.Callback({ + executionId: 42, + executionResult: IProcessor.ExecutionResult.Success, + executedCount: 1, + data: abi.encode("Test callback data") + }); + bytes memory callbackData = abi.encode(callback); + + // Should fail because only processor can call + vm.expectRevert("Only processor can call this function"); + auth.handleCallback(callbackData); + + vm.stopPrank(); + } + + // ======================= ADMIN MANAGEMENT TESTS ======================= + + /** + * @notice Test adding an admin address + */ + function testAddAdminAddress() public { + vm.prank(owner); + auth.addAdminAddress(admin); + assertTrue(auth.adminAddresses(admin), "Admin address should be authorized"); + } + + /** + * @notice Test removing an admin address + */ + function testRemoveAdminAddress() public { + vm.startPrank(owner); + + // Add and verify admin + auth.addAdminAddress(admin); + assertTrue(auth.adminAddresses(admin), "Admin address should be authorized"); + + // Remove and verify admin + auth.removeAdminAddress(admin); + assertFalse(auth.adminAddresses(admin), "Admin address should no longer be authorized"); + + vm.stopPrank(); + } + + /** + * @notice Test that only owner can add admin addresses + */ + function test_RevertWhen_AddAdminAddressUnauthorized() public { + vm.prank(unauthorized); + vm.expectRevert(); + auth.addAdminAddress(admin); + } + + // ======================= AUTHORIZATION MANAGEMENT TESTS ======================= + + /** + * @notice Test adding a standard user authorization + */ + function testAddStandardAuthorization() public { + vm.prank(owner); + auth.addStandardAuthorizations(users, contracts, calls); + + assertTrue( + auth.authorizations(user, address(forwarder), keccak256(updateConfigCall)), + "Authorization should be granted" + ); + } + + /** + * @notice Test adding a permissionless authorization (zero address) + */ + function testAddPermissionlessAuthorization() public { + users[0] = address(0); + vm.prank(owner); + auth.addStandardAuthorizations(users, contracts, calls); + + assertTrue( + auth.authorizations(address(0), address(forwarder), keccak256(updateConfigCall)), + "Permissionless authorization should be granted" + ); + } + + function test_RevertWhen_AddingInvalidAuthorization() public { + // Create arrays for the batch function + address[] memory invalidUsers = new address[](2); + invalidUsers[0] = user; + invalidUsers[1] = address(0); + + vm.prank(owner); + vm.expectRevert("Array lengths must match"); + auth.addStandardAuthorizations(invalidUsers, contracts, calls); + } + + /** + * @notice Test removing a standard authorization + */ + function testRemoveStandardAuthorization() public { + vm.startPrank(owner); + + // Add authorization + auth.addStandardAuthorizations(users, contracts, calls); + + // Remove authorization and verify + auth.removeStandardAuthorizations(users, contracts, calls); + assertFalse( + auth.authorizations(user, address(forwarder), keccak256(updateConfigCall)), + "Authorization should be removed" + ); + + vm.stopPrank(); + } + + // ======================= PROCESSOR MESSAGE TESTS ======================= + + function testSendProcessorMessageAuthorized() public { + // Authorize user + vm.prank(owner); + auth.addStandardAuthorizations(users, contracts, calls); + + vm.startPrank(user); + + // Create and send processor message + bytes memory encodedMessage = createSendMsgsMessage(updateConfigCall); + auth.sendProcessorMessage(encodedMessage); + + // Verify executionId was incremented + assertEq(auth.executionId(), 1, "Execution ID should be incremented"); + + // Verify that Forwarder config was updated + (, BaseAccount updatedOutputAccount,,) = forwarder.config(); + assertEq(address(updatedOutputAccount), address(newOutputAccount), "Output account should be updated"); + + // Verify that we got a callback + (IProcessor.ExecutionResult result, uint64 executionCount, bytes memory data) = auth.callbacks(0); + assert(result == IProcessor.ExecutionResult.Success); + assertEq(executionCount, 1, "Execution count should be 1"); + assertEq(data, bytes(""), "Callback data should be empty"); + + vm.stopPrank(); + } + + function testSendProcessorMessagePermissionless() public { + // Add permissionless authorization + vm.prank(owner); + users[0] = address(0); + auth.addStandardAuthorizations(users, contracts, calls); + + vm.startPrank(unauthorized); + + // Create and send processor message + bytes memory encodedMessage = createSendMsgsMessage(updateConfigCall); + auth.sendProcessorMessage(encodedMessage); + + // Verify executionId was incremented + assertEq(auth.executionId(), 1, "Execution ID should be incremented"); + + vm.stopPrank(); + } + + // ======================= HELPER FUNCTIONS ======================= + + /** + * @notice Create a forwarder configuration for testing + * @return Encoded forwarder configuration bytes + */ + function createForwarderConfig(address _outputAccount) public view returns (bytes memory) { + // Create ERC20 forwarding configuration + Forwarder.ForwardingConfig[] memory configs = new Forwarder.ForwardingConfig[](1); + configs[0] = Forwarder.ForwardingConfig({tokenAddress: mockERC20, maxAmount: MAX_AMOUNT}); + + // Create complete forwarder config + Forwarder.ForwarderConfig memory config = Forwarder.ForwarderConfig({ + inputAccount: inputAccount, + outputAccount: BaseAccount(payable(_outputAccount)), + forwardingConfigs: configs, + intervalType: Forwarder.IntervalType.TIME, + minInterval: MIN_INTERVAL + }); + + return abi.encode(config); + } + + /** + * @notice Creates a SendMsgs processor message with the given function call + * @param functionCall The encoded function call to include in the message + * @return Encoded processor message bytes + */ + function createSendMsgsMessage(bytes memory functionCall) internal view returns (bytes memory) { + // Create atomic subroutine retry logic + IProcessorMessageTypes.RetryTimes memory times = + IProcessorMessageTypes.RetryTimes({retryType: IProcessorMessageTypes.RetryTimesType.Amount, amount: 3}); + + IProcessorMessageTypes.Duration memory duration = + IProcessorMessageTypes.Duration({durationType: IProcessorMessageTypes.DurationType.Time, value: 0}); + + IProcessorMessageTypes.RetryLogic memory retryLogic = + IProcessorMessageTypes.RetryLogic({times: times, interval: duration}); + + // Create atomic subroutine with forwarder function + IProcessorMessageTypes.AtomicFunction memory atomicFunction = + IProcessorMessageTypes.AtomicFunction({contractAddress: address(forwarder)}); + + IProcessorMessageTypes.AtomicFunction[] memory atomicFunctions = new IProcessorMessageTypes.AtomicFunction[](1); + atomicFunctions[0] = atomicFunction; + + IProcessorMessageTypes.AtomicSubroutine memory atomicSubroutine = + IProcessorMessageTypes.AtomicSubroutine({functions: atomicFunctions, retryLogic: retryLogic}); + + // Create subroutine wrapper + IProcessorMessageTypes.Subroutine memory subroutine = IProcessorMessageTypes.Subroutine({ + subroutineType: IProcessorMessageTypes.SubroutineType.Atomic, + subroutine: abi.encode(atomicSubroutine) + }); + + // Create messages array with function call + bytes[] memory messages = new bytes[](1); + messages[0] = functionCall; + + // Create SendMsgs message + IProcessorMessageTypes.SendMsgs memory sendMsgs = IProcessorMessageTypes.SendMsgs({ + subroutine: subroutine, + messages: messages, + expirationTime: 0, + executionId: 0, // Will be set by Authorization contract + priority: IProcessorMessageTypes.Priority.Medium + }); + + // Create and encode processor message + IProcessorMessageTypes.ProcessorMessage memory processorMessage = IProcessorMessageTypes.ProcessorMessage({ + messageType: IProcessorMessageTypes.ProcessorMessageType.SendMsgs, + message: abi.encode(sendMsgs) + }); + + return abi.encode(processorMessage); + } +} diff --git a/solidity/test/authorization/AuthorizationZK.t.sol b/solidity/test/authorization/AuthorizationZK.t.sol new file mode 100644 index 000000000..5a6dba915 --- /dev/null +++ b/solidity/test/authorization/AuthorizationZK.t.sol @@ -0,0 +1,379 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.28; + +import {Test} from "forge-std/src/Test.sol"; +import {Authorization} from "../../src/authorization/Authorization.sol"; +import {SP1VerificationGateway} from "../../src/verification/SP1VerificationGateway.sol"; +import {ProcessorBase} from "../../src/processor/ProcessorBase.sol"; +import {LiteProcessor} from "../../src/processor/LiteProcessor.sol"; +import {IProcessorMessageTypes} from "../../src/processor/interfaces/IProcessorMessageTypes.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +/** + * @title AuthorizationZKTest + * @notice Test suite for the ZK authorization flow in the Authorization contract + * @dev Tests focus on registry management and unauthorized access verification + */ +contract AuthorizationZKTest is Test { + Authorization auth; + LiteProcessor processor; + SP1VerificationGateway verificationGateway; + + address owner = address(0x1); + address user1 = address(0x2); + address user2 = address(0x3); + address unauthorized = address(0x4); + address verifier = address(0x5); + bytes32 coprocessorRoot = bytes32(uint256(0x42069)); + + // ZK registry configuration + uint64 registryId1 = 101; + uint64 registryId2 = 102; + bytes32 vk1 = bytes32(uint256(0x123456)); + bytes32 vk2 = bytes32(uint256(0x789abc)); + bool validateBlockNumber1 = false; + bool validateBlockNumber2 = true; + + function setUp() public { + vm.startPrank(owner); + + // Deploy processor + processor = new LiteProcessor(bytes32(0), address(0), 0, new address[](0)); + + // Deploy verification gateway + verificationGateway = new SP1VerificationGateway(); + + bytes memory initializeData = + abi.encodeWithSelector(verificationGateway.initialize.selector, coprocessorRoot, verifier); + + // Deploy the proxy and initialize it + ERC1967Proxy proxy = new ERC1967Proxy(address(verificationGateway), initializeData); + verificationGateway = SP1VerificationGateway(address(proxy)); + + // Deploy authorization contract with verification gateway + auth = new Authorization(owner, address(processor), address(verificationGateway), true); + + // Configure processor to accept messages from auth contract + processor.addAuthorizedAddress(address(auth)); + + vm.stopPrank(); + } + + // ======================= REGISTRY MANAGEMENT TESTS ======================= + + /** + * @notice Test adding registries with verification keys and authorized users + */ + function testAddRegistries() public { + vm.startPrank(owner); + + // Create registry data + uint64[] memory registries = new uint64[](2); + registries[0] = registryId1; + registries[1] = registryId2; + + // Create arrays of authorized users for each registry + address[][] memory users = new address[][](2); + + // Registry 1: user1 and user2 authorized + users[0] = new address[](2); + users[0][0] = user1; + users[0][1] = user2; + + // Registry 2: permissionless (address(0)) + users[1] = new address[](1); + users[1][0] = address(0); // Permissionless access + + // Create verification keys + bytes32[] memory vks = new bytes32[](2); + vks[0] = vk1; + vks[1] = vk2; + + // Set block number validation flags + bool[] memory validateBlockNumbers = new bool[](2); + validateBlockNumbers[0] = validateBlockNumber1; + validateBlockNumbers[1] = validateBlockNumber2; + + // Add registries + auth.addRegistries(registries, users, vks, validateBlockNumbers); + + // Verify registry 1 + bytes32 storedVk1 = verificationGateway.programVKs(address(auth), registryId1); + assertEq(storedVk1, vk1, "Verification key for registry 1 should be stored correctly"); + + // Check authorized users for registry 1 + address[] memory authorizedUsers1 = auth.getZkAuthorizationsList(registryId1); + assertEq(authorizedUsers1.length, 2, "Registry 1 should have two authorized users"); + assertEq(authorizedUsers1[0], user1, "Registry 1 should authorize user1"); + assertEq(authorizedUsers1[1], user2, "Registry 1 should authorize user2"); + + // Verify registry 2 + bytes32 storedVk2 = verificationGateway.programVKs(address(auth), registryId2); + assertEq(storedVk2, vk2, "Verification key for registry 2 should be stored correctly"); + + // Check authorized users for registry 2 + address[] memory authorizedUsers2 = auth.getZkAuthorizationsList(registryId2); + assertEq(authorizedUsers2.length, 1, "Registry 2 should have one authorized user"); + assertEq(authorizedUsers2[0], address(0), "Registry 2 should be permissionless"); + + vm.stopPrank(); + } + + /** + * @notice Test removing registries + */ + function testRemoveRegistries() public { + vm.startPrank(owner); + + // First add registries + uint64[] memory registriesToAdd = new uint64[](2); + registriesToAdd[0] = registryId1; + registriesToAdd[1] = registryId2; + + address[][] memory users = new address[][](2); + users[0] = new address[](1); + users[0][0] = user1; + users[1] = new address[](1); + users[1][0] = user2; + + bytes32[] memory vks = new bytes32[](2); + vks[0] = vk1; + vks[1] = vk2; + + bool[] memory validateBlockNumbers = new bool[](2); + validateBlockNumbers[0] = validateBlockNumber1; + validateBlockNumbers[1] = validateBlockNumber2; + + auth.addRegistries(registriesToAdd, users, vks, validateBlockNumbers); + + // Verify registries were added + bytes32 storedVk1 = verificationGateway.programVKs(address(auth), registryId1); + assertEq(storedVk1, vk1, "Registry 1 should be added"); + + // Now remove one registry + uint64[] memory registriesToRemove = new uint64[](1); + registriesToRemove[0] = registryId1; + + auth.removeRegistries(registriesToRemove); + + // Verify registry was removed + bytes32 removedVk = verificationGateway.programVKs(address(auth), registryId1); + assertEq(removedVk, bytes32(0), "Registry 1 should be removed from verification gateway"); + + // Verify the authorization data was also removed + address[] memory authorizedUsers1 = auth.getZkAuthorizationsList(registryId1); + assertEq(authorizedUsers1.length, 0, "Registry 1 should have no authorized users"); + + // Verify last execution block was cleared + uint64 lastExecBlock = auth.zkAuthorizationLastExecutionBlock(registryId1); + assertEq(lastExecBlock, 0, "Last execution block should be cleared for registry 1"); + + // Verify other registry still exists + bytes32 storedVk2 = verificationGateway.programVKs(address(auth), registryId2); + assertEq(storedVk2, vk2, "Registry 2 should still exist"); + + vm.stopPrank(); + } + + /** + * @notice Test that only owner can add registries + */ + function test_RevertWhen_AddRegistriesUnauthorized() public { + vm.startPrank(unauthorized); + + uint64[] memory registries = new uint64[](1); + registries[0] = registryId1; + + address[][] memory users = new address[][](1); + users[0] = new address[](1); + users[0][0] = user1; + + bytes32[] memory vks = new bytes32[](1); + vks[0] = vk1; + + bool[] memory validateBlockNumbers = new bool[](2); + validateBlockNumbers[0] = validateBlockNumber1; + validateBlockNumbers[1] = validateBlockNumber2; + + vm.expectRevert(); + auth.addRegistries(registries, users, vks, validateBlockNumbers); + + vm.stopPrank(); + } + + /** + * @notice Test that only owner can remove registries + */ + function test_RevertWhen_RemoveRegistriesUnauthorized() public { + vm.startPrank(unauthorized); + + uint64[] memory registries = new uint64[](1); + registries[0] = registryId1; + + vm.expectRevert(); + auth.removeRegistries(registries); + + vm.stopPrank(); + } + + /** + * @notice Test handling of invalid registry data + */ + function test_RevertWhen_AddingRegistriesWithInvalidArrayLengths() public { + vm.startPrank(owner); + + // Create mismatched arrays + uint64[] memory registries = new uint64[](2); + registries[0] = registryId1; + registries[1] = registryId2; + + address[][] memory users = new address[][](1); // Only one entry, should have two + users[0] = new address[](1); + users[0][0] = user1; + + bytes32[] memory vks = new bytes32[](2); + vks[0] = vk1; + vks[1] = vk2; + + bool[] memory validateBlockNumbers = new bool[](2); + validateBlockNumbers[0] = validateBlockNumber1; + validateBlockNumbers[1] = validateBlockNumber2; + + vm.expectRevert("Array lengths must match"); + auth.addRegistries(registries, users, vks, validateBlockNumbers); + + vm.stopPrank(); + } + + // ======================= ZK MESSAGE EXECUTION TESTS ======================= + + function test_RevertWhen_VerificationGatewayNotSet() public { + vm.startPrank(owner); + + // Deploy a new authorization contract without a verification gateway + Authorization authWithoutGateway = new Authorization(owner, address(processor), address(0), true); + + // Create a ZK message + bytes memory zkMessage = createDummyZKMessage(registryId1, address(auth)); + bytes memory dummyProof = hex"deadbeef"; // Dummy proof data + + // Should fail because verification gateway is not set + vm.expectRevert("Verification gateway not set"); + authWithoutGateway.executeZKMessage(zkMessage, dummyProof); + + vm.stopPrank(); + } + + function test_RevertWhen_InvalidAuthorizationContract() public { + vm.startPrank(owner); + + // Add registry with only user1 authorized + uint64[] memory registries = new uint64[](1); + registries[0] = registryId1; + + address[][] memory users = new address[][](1); + users[0] = new address[](1); + users[0][0] = user1; + + bytes32[] memory vks = new bytes32[](1); + vks[0] = vk1; + + bool[] memory validateBlockNumbers = new bool[](1); + validateBlockNumbers[0] = validateBlockNumber1; + + auth.addRegistries(registries, users, vks, validateBlockNumbers); + + // Create a ZK message with an invalid authorization contract + bytes memory zkMessage = createDummyZKMessage(registryId1, address(user1)); + bytes memory dummyProof = hex"deadbeef"; // Dummy proof data + + // Should fail because address is not the authorization contract + vm.expectRevert("Invalid authorization contract"); + auth.executeZKMessage(zkMessage, dummyProof); + + vm.stopPrank(); + } + + /** + * @notice Test unauthorized address verification for ZK message execution + */ + function test_RevertWhen_ExecuteZKMessageUnauthorized() public { + vm.startPrank(owner); + + // Add registry with only user1 authorized + uint64[] memory registries = new uint64[](1); + registries[0] = registryId1; + + address[][] memory users = new address[][](1); + users[0] = new address[](1); + users[0][0] = user1; + + bytes32[] memory vks = new bytes32[](1); + vks[0] = vk1; + + bool[] memory validateBlockNumbers = new bool[](1); + validateBlockNumbers[0] = validateBlockNumber1; + + auth.addRegistries(registries, users, vks, validateBlockNumbers); + + vm.stopPrank(); + + // Try to execute ZK message from unauthorized address + vm.startPrank(unauthorized); + + // Create a ZK message + bytes memory zkMessage = createDummyZKMessage(registryId1, address(auth)); + bytes memory dummyProof = hex"deadbeef"; // Dummy proof data + + // Should fail because address is unauthorized + vm.expectRevert("Unauthorized address for this registry"); + auth.executeZKMessage(zkMessage, dummyProof); + + vm.stopPrank(); + } + + // ======================= HELPER FUNCTIONS ======================= + + /** + * @notice Create a dummy ZK message for testing + * @param _registryId Registry ID to include in the message + * @param _authorizationContract Address of the authorization contract + * @return Encoded ZK message bytes + */ + function createDummyZKMessage(uint64 _registryId, address _authorizationContract) + internal + view + returns (bytes memory) + { + return createDummyZKMessageWithBlockNumber(_registryId, uint64(block.number + 1), _authorizationContract); + } + + /** + * @notice Create a dummy ZK message with a specific block number + * @param _registryId Registry ID to include in the message + * @param _blockNumber Block number to include in the message + * @return Encoded ZK message bytes + */ + function createDummyZKMessageWithBlockNumber( + uint64 _registryId, + uint64 _blockNumber, + address _authorizationContract + ) internal view returns (bytes memory) { + // Create a simple processor message (Pause message) + IProcessorMessageTypes.ProcessorMessage memory processorMessage = IProcessorMessageTypes.ProcessorMessage({ + messageType: IProcessorMessageTypes.ProcessorMessageType.Pause, + message: bytes("") + }); + + // Create the ZK message + Authorization.ZKMessage memory zkMessage = Authorization.ZKMessage({ + registry: _registryId, + blockNumber: _blockNumber, + authorizationContract: _authorizationContract, + processorMessage: processorMessage + }); + + bytes memory rootBytes = abi.encodePacked(coprocessorRoot); + return bytes.concat(rootBytes, abi.encode(zkMessage)); + } +} diff --git a/solidity/test/processor/LiteProcessor.t.sol b/solidity/test/processor/LiteProcessor.t.sol index bc71183f7..606a9c355 100644 --- a/solidity/test/processor/LiteProcessor.t.sol +++ b/solidity/test/processor/LiteProcessor.t.sol @@ -39,12 +39,6 @@ contract LiteProcessorTest is Test { assertFalse(processor.paused()); } - /// @notice Test that constructor reverts when given zero address for mailbox - function testConstructorRevertOnZeroMailbox() public { - vm.expectRevert(ProcessorErrors.InvalidAddress.selector); - new LiteProcessor(AUTH_CONTRACT, address(0), ORIGIN_DOMAIN, AUTHORIZED_ADDRESSES); - } - /// @notice Test that handle() reverts when called by an address that is not the mailbox address or an authorized address function testHandleRevertOnUnauthorizedSender() public { bytes memory message = _encodePauseMessage(); diff --git a/solidity/test/processor/Processor.t.sol b/solidity/test/processor/Processor.t.sol index 8e71c4582..9a528bc49 100644 --- a/solidity/test/processor/Processor.t.sol +++ b/solidity/test/processor/Processor.t.sol @@ -30,10 +30,4 @@ contract ProcessorTest is Test { assertEq(processor.authorizedAddresses(AUTHORIZED_ADDRESSES[0]), true); assertFalse(processor.paused()); } - - /// @notice Test that constructor reverts when given zero address for mailbox - function testConstructorRevertOnZeroMailbox() public { - vm.expectRevert(ProcessorErrors.InvalidAddress.selector); - new Processor(AUTH_CONTRACT, address(0), ORIGIN_DOMAIN, AUTHORIZED_ADDRESSES); - } }