diff --git a/FunctionSignatures.md b/FunctionSignatures.md index 6f9cdf76..3eb60034 100644 --- a/FunctionSignatures.md +++ b/FunctionSignatures.md @@ -419,7 +419,6 @@ | `completeOwnershipHandover` | `0xf04e283e` | | `grantRole` | `0x2f2ff15d` | | `hasRole` | `0x91d14854` | -| `initialPacketCount` | `0x7c138814` | | `isAttested` | `0xc13c2396` | | `nextNonce` | `0x0cd55abf` | | `owner` | `0x8da5cb5b` | diff --git a/contracts/base/AppGatewayBase.sol b/contracts/base/AppGatewayBase.sol index b16f8b32..7fdc8b47 100644 --- a/contracts/base/AppGatewayBase.sol +++ b/contracts/base/AppGatewayBase.sol @@ -13,12 +13,13 @@ import {FAST} from "../protocol/utils/common/Constants.sol"; /// @title AppGatewayBase /// @notice Abstract contract for the app gateway +/// @dev This contract contains helpers for contract deployment, overrides, hooks and request processing abstract contract AppGatewayBase is AddressResolverUtil, IAppGateway, FeesPlugin { OverrideParams public overrideParams; + bool public isAsyncModifierSet; address public auctionManager; - bytes public onCompleteData; bytes32 public sbType; - bool public isAsyncModifierSet; + bytes public onCompleteData; mapping(address => bool) public isValidPromise; mapping(bytes32 => mapping(uint32 => address)) public override forwarderAddresses; @@ -27,11 +28,14 @@ abstract contract AppGatewayBase is AddressResolverUtil, IAppGateway, FeesPlugin /// @notice Modifier to treat functions async modifier async() { if (fees.feePoolChain == 0) revert FeesNotSet(); + isAsyncModifierSet = true; deliveryHelper__().clearQueue(); addressResolver__.clearPromises(); _clearOverrides(); + _; + isAsyncModifierSet = false; deliveryHelper__().batch(fees, auctionManager, onCompleteData); _markValidPromises(); @@ -87,13 +91,6 @@ abstract contract AppGatewayBase is AddressResolverUtil, IAppGateway, FeesPlugin } } - /// @notice Gets the socket address - /// @param chainSlug_ The chain slug - /// @return socketAddress_ The socket address - function getSocketAddress(uint32 chainSlug_) public view returns (address) { - return watcherPrecompileConfig().sockets(chainSlug_); - } - /// @notice Sets the validity of an onchain contract (plug) to authorize it to send information to a specific AppGateway /// @param chainSlug_ The unique identifier of the chain where the contract resides /// @param contractId The bytes32 identifier of the contract to be validated @@ -127,9 +124,9 @@ abstract contract AppGatewayBase is AddressResolverUtil, IAppGateway, FeesPlugin if (!isAsyncModifierSet) revert AsyncModifierNotUsed(); address asyncPromise = addressResolver__.deployAsyncPromiseContract(address(this)); - isValidPromise[asyncPromise] = true; IPromise(asyncPromise).then(this.setAddress.selector, abi.encode(chainSlug_, contractId_)); + isValidPromise[asyncPromise] = true; onCompleteData = abi.encode(chainSlug_, true); QueuePayloadParams memory queuePayloadParams = QueuePayloadParams({ @@ -156,7 +153,6 @@ abstract contract AppGatewayBase is AddressResolverUtil, IAppGateway, FeesPlugin /// @param returnData_ The return data function setAddress(bytes memory data_, bytes memory returnData_) external onlyPromises { (uint32 chainSlug, bytes32 contractId) = abi.decode(data_, (uint32, bytes32)); - address forwarderContractAddress = addressResolver__.getOrDeployForwarderContract( address(this), abi.decode(returnData_, (address)), @@ -166,6 +162,13 @@ abstract contract AppGatewayBase is AddressResolverUtil, IAppGateway, FeesPlugin forwarderAddresses[contractId][chainSlug] = forwarderContractAddress; } + /// @notice Gets the socket address + /// @param chainSlug_ The chain slug + /// @return socketAddress_ The socket address + function getSocketAddress(uint32 chainSlug_) public view returns (address) { + return watcherPrecompileConfig().sockets(chainSlug_); + } + /// @notice Gets the on-chain address /// @param contractId_ The contract ID /// @param chainSlug_ The chain slug @@ -342,7 +345,10 @@ abstract contract AppGatewayBase is AddressResolverUtil, IAppGateway, FeesPlugin /// @param onCompleteData_ The on complete data /// @dev only payload delivery can call this /// @dev callback in pd promise to be called after all contracts are deployed - function onRequestComplete(uint40, bytes calldata onCompleteData_) external override { + function onRequestComplete( + uint40, + bytes calldata onCompleteData_ + ) external override onlyDeliveryHelper { if (onCompleteData_.length == 0) return; (uint32 chainSlug, bool isDeploy) = abi.decode(onCompleteData_, (uint32, bool)); if (isDeploy) { @@ -350,7 +356,8 @@ abstract contract AppGatewayBase is AddressResolverUtil, IAppGateway, FeesPlugin } } - /// @notice Initializes the contract + /// @notice Initializes the contract after deployment + /// @dev can be overridden by the app gateway to add custom logic /// @param chainSlug_ The chain slug function initialize(uint32 chainSlug_) public virtual {} diff --git a/contracts/base/PlugBase.sol b/contracts/base/PlugBase.sol index e86daa9f..cd06319e 100644 --- a/contracts/base/PlugBase.sol +++ b/contracts/base/PlugBase.sol @@ -7,6 +7,7 @@ import {NotSocket} from "../protocol/utils/common/Errors.sol"; /// @title PlugBase /// @notice Abstract contract for plugs +/// @dev This contract contains helpers for socket connection, disconnection, and overrides abstract contract PlugBase is IPlug { ISocket public socket__; bytes32 public appGatewayId; @@ -23,7 +24,7 @@ abstract contract PlugBase is IPlug { _; } - /// @notice Modifier to ensure the socket is initialized + /// @notice Modifier to ensure the socket is initialized and if not already initialized, it will be initialized modifier socketInitializer() { if (isSocketInitialized == 1) revert SocketAlreadyInitialized(); isSocketInitialized = 1; diff --git a/contracts/base/ProxyFactory.sol b/contracts/base/ProxyFactory.sol index 0f903337..5c081ec3 100644 --- a/contracts/base/ProxyFactory.sol +++ b/contracts/base/ProxyFactory.sol @@ -2,6 +2,4 @@ pragma solidity ^0.8.0; import {ERC1967Factory} from "solady/utils/ERC1967Factory.sol"; -contract ProxyFactory is ERC1967Factory { - constructor() {} -} +contract ProxyFactory is ERC1967Factory {} diff --git a/contracts/interfaces/IAddressResolver.sol b/contracts/interfaces/IAddressResolver.sol index 7a34fcbd..ce5fcdbf 100644 --- a/contracts/interfaces/IAddressResolver.sol +++ b/contracts/interfaces/IAddressResolver.sol @@ -12,24 +12,45 @@ interface IAddressResolver { /// @param newAddress The new address of the contract event AddressSet(bytes32 indexed name, address oldAddress, address newAddress); + /// @notice Emitted when a new plug is added to the resolver + /// @param appGateway The address of the app gateway + /// @param chainSlug The chain slug + /// @param plug The address of the plug + event PlugAdded(address appGateway, uint32 chainSlug, address plug); + + /// @notice Emitted when a new forwarder is deployed + /// @param newForwarder The address of the new forwarder + /// @param salt The salt used to deploy the forwarder + event ForwarderDeployed(address newForwarder, bytes32 salt); + + /// @notice Emitted when a new async promise is deployed + /// @param newAsyncPromise The address of the new async promise + /// @param salt The salt used to deploy the async promise + event AsyncPromiseDeployed(address newAsyncPromise, bytes32 salt); + + /// @notice Emitted when an implementation is updated + /// @param contractName The name of the contract + /// @param newImplementation The new implementation address + event ImplementationUpdated(string contractName, address newImplementation); + /// @notice Gets the address of the delivery helper contract - /// @return IMiddleware The delivery helper interface - /// @dev Returns interface pointing to zero address if not configured + /// @return The delivery helper contract address + /// @dev Returns zero address if not configured function deliveryHelper() external view returns (address); /// @notice Gets the address of the fees manager contract - /// @return IFeesManager The fees manager interface - /// @dev Returns interface pointing to zero address if not configured + /// @return The fees manager contract address + /// @dev Returns zero address if not configured function feesManager() external view returns (address); /// @notice Gets the address of the default auction manager contract - /// @return IAuctionManager The auction manager interface - /// @dev Returns interface pointing to zero address if not configured + /// @return The auction manager contract address + /// @dev Returns zero address if not configured function defaultAuctionManager() external view returns (address); - /// @notice Gets the watcher precompile contract interface - /// @return IWatcherPrecompile The watcher precompile interface - /// @dev Returns interface pointing to zero address if not configured + /// @notice Gets the watcher precompile contract instance + /// @return The watcher precompile contract instance + /// @dev Returns instance with zero address if not configured function watcherPrecompile__() external view returns (IWatcherPrecompile); /// @notice Maps contract addresses to their corresponding gateway addresses @@ -41,29 +62,17 @@ interface IAddressResolver { /// @return Array of async promise contract addresses function getPromises() external view returns (address[] memory); - // State-changing functions - /// @notice Sets the auction house contract address - /// @param deliveryHelper_ The new delivery helper contract address - /// @dev Only callable by contract owner - function setDeliveryHelper(address deliveryHelper_) external; - - /// @notice Sets the watcher precompile contract address - /// @param watcherPrecompile_ The new watcher precompile contract address - /// @dev Only callable by contract owner - function setWatcherPrecompile(address watcherPrecompile_) external; - /// @notice Maps a contract address to its gateway /// @param contractAddress_ The contract address to map /// @dev Creates bidirectional mapping between contract and gateway function setContractsToGateways(address contractAddress_) external; - /// @notice Clears the list of deployed async promise contracts - /// @dev Only callable by contract owner + /// @notice Clears the list of deployed async promise contracts array function clearPromises() external; - /// @notice Deploys a new forwarder contract if not already deployed - /// @param chainContractAddress_ The contract address on the destination chain - /// @param chainSlug_ The identifier of the destination chain + /// @notice Deploys or returns the address of a new forwarder contract if not already deployed + /// @param chainContractAddress_ The contract address on the `chainSlug_` + /// @param chainSlug_ The identifier of the chain /// @return The address of the newly deployed forwarder contract function getOrDeployForwarderContract( address appGateway_, diff --git a/contracts/interfaces/IAppGateway.sol b/contracts/interfaces/IAppGateway.sol index bdda2198..79a78889 100644 --- a/contracts/interfaces/IAppGateway.sol +++ b/contracts/interfaces/IAppGateway.sol @@ -3,31 +3,57 @@ pragma solidity ^0.8.21; import {Fees, Read, Parallel, QueuePayloadParams, OverrideParams, CallType, WriteFinality, PayloadParams} from "../protocol/utils/common/Structs.sol"; +/// @title IAppGateway +/// @notice Interface for the app gateway interface IAppGateway { + /// @notice Checks if the async modifier is set + /// @return isAsyncModifierSet_ True if the async modifier is set, false otherwise function isAsyncModifierSet() external view returns (bool); + /// @notice Gets the override parameters + /// @return read_ The read parameters + /// @return parallel_ The parallel parameters + /// @return writeFinality_ The write finality parameters + /// @return readTimeout_ The read timeout + /// @return writeTimeout_ The write timeout + /// @return writeFinalityTimeout_ The write finality timeout + /// @return sbType_ The switchboard type function getOverrideParams() external view returns (Read, Parallel, WriteFinality, uint256, uint256, uint256, bytes32); + /// @notice Handles the request complete event + /// @param requestCount_ The request count + /// @param onCompleteData_ The on complete data function onRequestComplete(uint40 requestCount_, bytes calldata onCompleteData_) external; + /// @notice Handles the revert event + /// @param requestCount_ The request count + /// @param payloadId_ The payload id function handleRevert(uint40 requestCount_, bytes32 payloadId_) external; /// @notice initialize the contracts on chain + /// @param chainSlug_ The chain slug function initialize(uint32 chainSlug_) external; /// @notice deploy contracts to chain + /// @param chainSlug_ The chain slug function deployContracts(uint32 chainSlug_) external; /// @notice get the on-chain address of a contract + /// @param contractId_ The contract id + /// @param chainSlug_ The chain slug + /// @return onChainAddress The on-chain address function getOnChainAddress( bytes32 contractId_, uint32 chainSlug_ ) external view returns (address onChainAddress); /// @notice get the forwarder address of a contract + /// @param contractId_ The contract id + /// @param chainSlug_ The chain slug + /// @return forwarderAddress The forwarder address function forwarderAddresses( bytes32 contractId_, uint32 chainSlug_ diff --git a/contracts/interfaces/IAuctionManager.sol b/contracts/interfaces/IAuctionManager.sol index bee93dc1..12046099 100644 --- a/contracts/interfaces/IAuctionManager.sol +++ b/contracts/interfaces/IAuctionManager.sol @@ -1,9 +1,14 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.3; -import {Bid, Fees} from "../protocol/utils/common/Structs.sol"; +import {Bid, Fees, RequestMetadata, RequestParams} from "../protocol/utils/common/Structs.sol"; interface IAuctionManager { + /// @notice Bids for an auction + /// @param requestCount_ The request count + /// @param fee_ The fee + /// @param transmitterSignature_ The transmitter signature + /// @param extraData_ The extra data function bid( uint40 requestCount_, uint256 fee_, @@ -11,9 +16,17 @@ interface IAuctionManager { bytes memory extraData_ ) external; + /// @notice Ends an auction + /// @param requestCount_ The request count function endAuction(uint40 requestCount_) external; + /// @notice Checks if an auction is closed + /// @param requestCount_ The request count + /// @return isClosed_ Whether the auction is closed function auctionClosed(uint40 requestCount_) external view returns (bool); + /// @notice Checks if an auction is started + /// @param requestCount_ The request count + /// @return isStarted_ Whether the auction is started function auctionStarted(uint40 requestCount_) external view returns (bool); } diff --git a/contracts/interfaces/IFeesPlug.sol b/contracts/interfaces/IFeesPlug.sol index 9515ee81..cc546977 100644 --- a/contracts/interfaces/IFeesPlug.sol +++ b/contracts/interfaces/IFeesPlug.sol @@ -2,19 +2,17 @@ pragma solidity ^0.8.21; interface IFeesPlug { - function balanceOf(address appGateway_, address token_) external view returns (uint256); + function balanceOf(address token_) external view returns (uint256); - function feesRedeemed(uint256 feesCounter_) external view returns (bool); + function feesRedeemed(bytes32 feesId_) external view returns (bool); function deposit(address token_, address appGateway_, uint256 amount_) external payable; - function connect(address appGateway_, address switchboard_) external; - function distributeFee( address feeToken_, uint256 fee_, address transmitter_, - bytes32 feesCounter_ + bytes32 feesId_ ) external; function withdrawFees(address token_, uint256 amount_, address receiver_) external; diff --git a/contracts/interfaces/IForwarder.sol b/contracts/interfaces/IForwarder.sol index 9372cc5f..25072ea3 100644 --- a/contracts/interfaces/IForwarder.sol +++ b/contracts/interfaces/IForwarder.sol @@ -1,9 +1,14 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.21; +/// @title IForwarder +/// @notice Interface for the Forwarder contract that allows contracts to call async promises interface IForwarder { - // View functions + /// @notice Returns the on-chain address of the contract being referenced + /// @return The on-chain address function getOnChainAddress() external view returns (address); + /// @notice Returns the chain slug of the on chain contract + /// @return The chain slug function getChainSlug() external view returns (uint32); } diff --git a/contracts/interfaces/IMiddleware.sol b/contracts/interfaces/IMiddleware.sol index 3509457d..658c15fe 100644 --- a/contracts/interfaces/IMiddleware.sol +++ b/contracts/interfaces/IMiddleware.sol @@ -1,29 +1,45 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.3; -import {QueuePayloadParams, Bid, Fees, WriteFinality, CallType, Parallel, IsPlug, RequestMetadata} from "../protocol/utils/common/Structs.sol"; +import {PayloadSubmitParams, QueuePayloadParams, Bid, Fees, WriteFinality, CallType, Parallel, IsPlug, RequestMetadata} from "../protocol/utils/common/Structs.sol"; +/// @title IMiddleware +/// @notice Interface for the Middleware contract interface IMiddleware { - event AuctionEnded( - uint40 indexed requestCount, - Bid winningBid // Replaced winningTransmitter and winningBid with Bid struct - ); - + /// @notice Returns the timeout after which a bid expires function bidTimeout() external view returns (uint128); + /// @notice Returns the metadata for a request + /// @param requestCount_ The request id + /// @return requestMetadata The metadata for the request function getRequestMetadata( uint40 requestCount_ ) external view returns (RequestMetadata memory); + /// @notice Clears the temporary queue used to store payloads for a request function clearQueue() external; + /// @notice Queues a payload for a request + /// @param queuePayloadParams_ The parameters for the payload function queue(QueuePayloadParams memory queuePayloadParams_) external; + /// @notice Batches a request + /// @param fees_ The fees for the request + /// @param auctionManager_ The address of the auction manager + /// @param onCompleteData_ The data to be passed to the onComplete callback + /// @return requestCount The request id function batch( Fees memory fees_, address auctionManager_, bytes memory onCompleteData_ ) external returns (uint40 requestCount); + /// @notice Withdraws funds to a receiver + /// @param chainSlug_ The chain slug + /// @param token_ The token address + /// @param amount_ The amount to withdraw + /// @param receiver_ The receiver address + /// @param auctionManager_ The address of the auction manager + /// @param fees_ The fees for the request function withdrawTo( uint32 chainSlug_, address token_, @@ -33,15 +49,28 @@ interface IMiddleware { Fees memory fees_ ) external returns (uint40); + /// @notice Cancels a request + /// @param requestCount_ The request id function cancelRequest(uint40 requestCount_) external; + /// @notice Increases the fees for a request + /// @param requestCount_ The request id + /// @param fees_ The new fees function increaseFees(uint40 requestCount_, uint256 fees_) external; + /// @notice Starts the request processing + /// @param requestCount_ The request id + /// @param winningBid_ The winning bid function startRequestProcessing(uint40 requestCount_, Bid memory winningBid_) external; + /// @notice Returns the fees for a request function getFees(uint40 requestCount_) external view returns (Fees memory); + /// @notice Finishes a request by assigning fees and calling the onComplete callback + /// @param requestCount_ The request id function finishRequest(uint40 requestCount_) external; + /// @notice Handles request reverts by unblocking the fees and calling the onRevert callback + /// @param requestCount_ The request id function handleRequestReverts(uint40 requestCount_) external; } diff --git a/contracts/interfaces/IPlug.sol b/contracts/interfaces/IPlug.sol index c7cad4aa..be18836e 100644 --- a/contracts/interfaces/IPlug.sol +++ b/contracts/interfaces/IPlug.sol @@ -6,7 +6,13 @@ pragma solidity ^0.8.21; * @notice Interface for a plug contract that executes the payload received from a source chain. */ interface IPlug { + /// @notice Initializes the socket + /// @param appGatewayId_ The app gateway id + /// @param socket_ The socket address + /// @param switchboard_ The switchboard address function initSocket(bytes32 appGatewayId_, address socket_, address switchboard_) external; - function overrides() external view returns (bytes memory); + /// @notice Gets the overrides + /// @return overrides_ The overrides + function overrides() external view returns (bytes memory overrides_); } diff --git a/contracts/interfaces/IPromise.sol b/contracts/interfaces/IPromise.sol index 13318ac0..d2f21e8c 100644 --- a/contracts/interfaces/IPromise.sol +++ b/contracts/interfaces/IPromise.sol @@ -1,16 +1,27 @@ // SPDX-License-Identifier: Unlicense pragma solidity ^0.8.21; +/// @title IPromise interface IPromise { - function then(bytes4 selector_, bytes memory data_) external returns (address _promise); + /// @notice Sets the callback selector and data for the promise. + /// @param selector_ The function selector for the callback. + /// @param data_ The data to be passed to the callback. + /// @return promise_ The address of the current promise + function then(bytes4 selector_, bytes memory data_) external returns (address promise_); + /// @notice Marks the promise as resolved and executes the callback if set. + /// @dev Only callable by the watcher precompile. + /// @param returnData_ The data returned from the async payload execution. function markResolved( uint40 requestCount_, bytes32 payloadId_, bytes memory returnData_ ) external returns (bool success); + /// @notice Marks the promise as onchain reverting. + /// @dev Only callable by the watcher precompile. function markOnchainRevert(uint40 requestCount_, bytes32 payloadId_) external; + /// @notice Indicates whether the promise has been resolved. function resolved() external view returns (bool); } diff --git a/contracts/interfaces/ISocket.sol b/contracts/interfaces/ISocket.sol index dc924f87..042fb7cc 100644 --- a/contracts/interfaces/ISocket.sol +++ b/contracts/interfaces/ISocket.sol @@ -14,13 +14,13 @@ import {ExecuteParams} from "../protocol/utils/common/Structs.sol"; interface ISocket { /** * @notice emits the status of payload after external call - * @param payloadId msg id which is executed + * @param payloadId payload id which is executed */ event ExecutionSuccess(bytes32 payloadId, bytes returnData); /** * @notice emits the status of payload after external call - * @param payloadId msg id which is executed + * @param payloadId payload id which is executed */ event ExecutionFailed(bytes32 payloadId, bytes returnData); @@ -33,8 +33,8 @@ interface ISocket { event PlugConnected(address plug, bytes32 appGatewayId, address switchboard); /** - * @notice emits the message details when a new message arrives at outbound - * @param triggerId call id + * @notice emits the payload details when a new payload arrives at outbound + * @param triggerId trigger id * @param chainSlug local chain slug * @param plug local plug address * @param overrides params, for specifying details like fee pool chain, fee pool token and max fees if required @@ -63,6 +63,9 @@ interface ISocket { */ function connect(bytes32 appGatewayId_, address switchboard_) external; + /** + * @notice registers a switchboard for the socket + */ function registerSwitchboard() external; /** diff --git a/contracts/interfaces/ISocketBatcher.sol b/contracts/interfaces/ISocketBatcher.sol index ab753866..d2991f4e 100644 --- a/contracts/interfaces/ISocketBatcher.sol +++ b/contracts/interfaces/ISocketBatcher.sol @@ -3,7 +3,19 @@ pragma solidity ^0.8.21; import {ExecuteParams} from "../protocol/utils/common/Structs.sol"; +/** + * @title ISocketBatcher + * @notice Interface for a helper contract for socket which batches attest (on sb) and execute calls (on socket). + */ interface ISocketBatcher { + /** + * @notice Attests a payload and executes it + * @param executeParams_ The execution parameters + * @param digest_ The digest of the payload + * @param proof_ The proof of the payload + * @param transmitterSignature_ The signature of the transmitter + * @return The return data after execution + */ function attestAndExecute( ExecuteParams calldata executeParams_, bytes32 digest_, diff --git a/contracts/interfaces/ISwitchboard.sol b/contracts/interfaces/ISwitchboard.sol index c6998445..4f1095ea 100644 --- a/contracts/interfaces/ISwitchboard.sol +++ b/contracts/interfaces/ISwitchboard.sol @@ -3,8 +3,8 @@ pragma solidity ^0.8.21; /** * @title ISwitchboard - * @dev The interface for a switchboard contract that is responsible for verification of payloadss between - * different blockchain networks. + * @dev The interface for a switchboard contract that is responsible for verification of payloads if the correct + * digest is executed. */ interface ISwitchboard { /** @@ -13,7 +13,12 @@ interface ISwitchboard { * @param payloadId_ The unique identifier for the payloads. * @return A boolean indicating whether the payloads is allowed to go through the switchboard or not. */ - function allowPacket(bytes32 digest_, bytes32 payloadId_) external view returns (bool); + function allowPayload(bytes32 digest_, bytes32 payloadId_) external view returns (bool); + /** + * @notice Attests a payload + * @param digest_ The digest of the payload + * @param proof_ The proof of the payload + */ function attest(bytes32 digest_, bytes calldata proof_) external; } diff --git a/contracts/interfaces/IWatcherPrecompile.sol b/contracts/interfaces/IWatcherPrecompile.sol index 86763681..45de683c 100644 --- a/contracts/interfaces/IWatcherPrecompile.sol +++ b/contracts/interfaces/IWatcherPrecompile.sol @@ -9,8 +9,12 @@ import {IWatcherPrecompileConfig} from "./IWatcherPrecompileConfig.sol"; /// @notice Interface for the Watcher Precompile system that handles payload verification and execution /// @dev Defines core functionality for payload processing and promise resolution interface IWatcherPrecompile { + /// @notice Emitted when a new call is made to an app gateway + /// @param triggerId The unique identifier for the trigger event CalledAppGateway(bytes32 triggerId); + /// @notice Emitted when a call to an app gateway fails + /// @param triggerId The unique identifier for the trigger event AppGatewayCallFailed(bytes32 triggerId); /// @notice Emitted when a new query is requested @@ -32,20 +36,31 @@ interface IWatcherPrecompile { /// @param payloadId The unique identifier for the not resolved promise event PromiseNotResolved(bytes32 indexed payloadId, address asyncPromise); + /// @notice Emitted when a payload is marked as revert + /// @param payloadId The unique identifier for the payload + /// @param isRevertingOnchain Whether the payload is reverting onchain event MarkedRevert(bytes32 indexed payloadId, bool isRevertingOnchain); - event TimeoutRequested( - bytes32 timeoutId, - address target, - bytes payload, - uint256 executeAt // Epoch time when the task should execute - ); + + /// @notice Emitted when a timeout is requested + /// @param timeoutId The unique identifier for the timeout + /// @param target The target address for the timeout callback + /// @param payload The payload data + /// @param executeAt The epoch time when the task should execute + event TimeoutRequested(bytes32 timeoutId, address target, bytes payload, uint256 executeAt); /// @notice Emitted when a timeout is resolved /// @param timeoutId The unique identifier for the timeout - /// @param target The target address for the timeout + /// @param target The target address for the callback /// @param payload The payload data /// @param executedAt The epoch time when the task was executed - event TimeoutResolved(bytes32 timeoutId, address target, bytes payload, uint256 executedAt); + /// @param returnData The return data from the callback + event TimeoutResolved( + bytes32 timeoutId, + address target, + bytes payload, + uint256 executedAt, + bytes returnData + ); event RequestSubmitted( address middleware, @@ -53,12 +68,18 @@ interface IWatcherPrecompile { PayloadParams[] payloadParamsArray ); + event MaxTimeoutDelayInSecondsSet(uint256 maxTimeoutDelayInSeconds); + + event ExpiryTimeSet(uint256 expiryTime); + + event WatcherPrecompileLimitsSet(address watcherPrecompileLimits); + + event WatcherPrecompileConfigSet(address watcherPrecompileConfig); + /// @notice Error thrown when an invalid chain slug is provided error InvalidChainSlug(); /// @notice Error thrown when an invalid app gateway reaches a plug error InvalidConnection(); - /// @notice Error thrown if winning bid is assigned to an invalid transmitter - error InvalidTransmitter(); /// @notice Error thrown when a timeout request is invalid error InvalidTimeoutRequest(); /// @notice Error thrown when a payload id is invalid @@ -74,8 +95,10 @@ interface IWatcherPrecompile { error RequestCancelled(); error AlreadyStarted(); + error RequestNotProcessing(); error InvalidLevelNumber(); error DeadlineNotPassedForOnChainRevert(); + /// @notice Calculates the digest hash of payload parameters /// @param params_ The payload parameters /// @return digest The calculated digest @@ -161,4 +184,6 @@ interface IWatcherPrecompile { function watcherPrecompileLimits__() external view returns (IWatcherPrecompileLimits); function getRequestParams(uint40 requestCount) external view returns (RequestParams memory); + + function nextRequestCount() external view returns (uint40); } diff --git a/contracts/interfaces/IWatcherPrecompileConfig.sol b/contracts/interfaces/IWatcherPrecompileConfig.sol index bf4ccf6f..2919417a 100644 --- a/contracts/interfaces/IWatcherPrecompileConfig.sol +++ b/contracts/interfaces/IWatcherPrecompileConfig.sol @@ -50,7 +50,8 @@ interface IWatcherPrecompileConfig { uint32 chainSlug_, address target_, address appGateway_, - address switchboard_ + address switchboard_, + address middleware_ ) external view; function setAppGateways( diff --git a/contracts/interfaces/IWatcherPrecompileLimits.sol b/contracts/interfaces/IWatcherPrecompileLimits.sol index 70a0c01a..62b52ce4 100644 --- a/contracts/interfaces/IWatcherPrecompileLimits.sol +++ b/contracts/interfaces/IWatcherPrecompileLimits.sol @@ -31,14 +31,10 @@ interface IWatcherPrecompileLimits { /// @notice Set the default limit value /// @param defaultLimit_ The new default limit value - function setDefaultLimit(uint256 defaultLimit_) external; - - /// @notice Set the rate at which limit replenishes - /// @param defaultRatePerSecond_ The new rate per second - function setDefaultRatePerSecond(uint256 defaultRatePerSecond_) external; + function setDefaultLimitAndRatePerSecond(uint256 defaultLimit_) external; /// @notice Number of decimals used in limit calculations - function LIMIT_DECIMALS() external view returns (uint256); + function limitDecimals() external view returns (uint256); /// @notice Default limit value for any app gateway function defaultLimit() external view returns (uint256); @@ -46,6 +42,10 @@ interface IWatcherPrecompileLimits { /// @notice Rate at which limit replenishes per second function defaultRatePerSecond() external view returns (uint256); + /// @notice Consumes a limit for an app gateway + /// @param appGateway_ The app gateway address + /// @param limitType_ The type of limit to consume + /// @param consumeLimit_ The amount of limit to consume function consumeLimit(address appGateway_, bytes32 limitType_, uint256 consumeLimit_) external; /// @notice Emitted when limit parameters are updated @@ -53,13 +53,4 @@ interface IWatcherPrecompileLimits { /// @notice Emitted when an app gateway is activated with default limits event AppGatewayActivated(address indexed appGateway, uint256 maxLimit, uint256 ratePerSecond); - - error ActionNotSupported(address appGateway_, bytes32 limitType_); - error NotDeliveryHelper(); - error LimitExceeded( - address appGateway, - bytes32 limitType, - uint256 requested, - uint256 available - ); } diff --git a/contracts/protocol/AddressResolver.sol b/contracts/protocol/AddressResolver.sol index df9bf3b6..f7cbc322 100644 --- a/contracts/protocol/AddressResolver.sol +++ b/contracts/protocol/AddressResolver.sol @@ -17,16 +17,16 @@ abstract contract AddressResolverStorage is IAddressResolver { IWatcherPrecompile public override watcherPrecompile__; // slot 51 - address public override deliveryHelper; + UpgradeableBeacon public forwarderBeacon; // slot 52 - address public override feesManager; + UpgradeableBeacon public asyncPromiseBeacon; // slot 53 - UpgradeableBeacon public forwarderBeacon; + address public override deliveryHelper; // slot 54 - UpgradeableBeacon public asyncPromiseBeacon; + address public override feesManager; // slot 55 address public forwarderImplementation; @@ -42,15 +42,13 @@ abstract contract AddressResolverStorage is IAddressResolver { // slot 59 uint64 public version; + address public override defaultAuctionManager; // slot 60 mapping(address => address) public override contractsToGateways; - // slot 61 - address public override defaultAuctionManager; - - // slots [62-110] reserved for gap - uint256[49] _gap_after; + // slots [61-110] reserved for gap + uint256[50] _gap_after; } /// @title AddressResolver Contract @@ -60,16 +58,13 @@ contract AddressResolver is AddressResolverStorage, Initializable, Ownable { /// @notice Error thrown if AppGateway contract was already set by a different address error InvalidAppGateway(address contractAddress_); - event PlugAdded(address appGateway, uint32 chainSlug, address plug); - event ForwarderDeployed(address newForwarder, bytes32 salt); - event AsyncPromiseDeployed(address newAsyncPromise, bytes32 salt); - event ImplementationUpdated(string contractName, address newImplementation); - constructor() { _disableInitializers(); // disable for implementation } /// @notice Initializer to replace constructor for upgradeable contracts + /// @dev it deploys the forwarder and async promise implementations and beacons for them + /// @dev this contract is owner of the beacons for upgrading later /// @param owner_ The address of the contract owner function initialize(address owner_) public reinitializer(1) { version = 1; @@ -84,6 +79,8 @@ contract AddressResolver is AddressResolverStorage, Initializable, Ownable { } /// @notice Gets or deploys a Forwarder proxy contract + /// @dev it checks if the forwarder is already deployed, if yes, it returns the address + /// @dev it maps the forwarder with the app gateway which is used for verifying if they are linked /// @param chainContractAddress_ The address of the chain contract /// @param chainSlug_ The chain slug /// @return newForwarder The address of the deployed Forwarder proxy contract @@ -94,18 +91,25 @@ contract AddressResolver is AddressResolverStorage, Initializable, Ownable { ) public returns (address newForwarder) { // predict address address forwarderAddress = getForwarderAddress(chainContractAddress_, chainSlug_); + // check if addr has code, if yes, return if (forwarderAddress.code.length > 0) { return forwarderAddress; } + // creates init data and salt (bytes32 salt, bytes memory initData) = _createForwarderParams( chainContractAddress_, chainSlug_ ); + // deploys the proxy newForwarder = _deployProxy(salt, address(forwarderBeacon), initData); + + // sets the config _setConfig(appGateway_, newForwarder); + + // emits the event emit ForwarderDeployed(newForwarder, salt); } @@ -120,6 +124,8 @@ contract AddressResolver is AddressResolverStorage, Initializable, Ownable { chainContractAddress_, address(this) ); + + // creates salt with constructor args salt = keccak256(constructorArgs); } @@ -127,6 +133,8 @@ contract AddressResolver is AddressResolverStorage, Initializable, Ownable { address invoker_ ) internal view returns (bytes32 salt, bytes memory initData) { bytes memory constructorArgs = abi.encode(invoker_, msg.sender, address(this)); + + // creates init data initData = abi.encodeWithSelector( AsyncPromise.initialize.selector, invoker_, @@ -134,6 +142,7 @@ contract AddressResolver is AddressResolverStorage, Initializable, Ownable { address(this) ); + // creates salt with a counter salt = keccak256(abi.encodePacked(constructorArgs, asyncPromiseCounter)); } @@ -143,9 +152,11 @@ contract AddressResolver is AddressResolverStorage, Initializable, Ownable { function deployAsyncPromiseContract( address invoker_ ) external returns (address newAsyncPromise) { + // creates init data and salt (bytes32 salt, bytes memory initData) = _createAsyncPromiseParams(invoker_); asyncPromiseCounter++; + // deploys the proxy newAsyncPromise = _deployProxy(salt, address(asyncPromiseBeacon), initData); _promises.push(newAsyncPromise); @@ -157,10 +168,10 @@ contract AddressResolver is AddressResolverStorage, Initializable, Ownable { address beacon_, bytes memory initData_ ) internal returns (address) { - // 1. Deploy proxy without initialization args + // Deploy proxy without initialization args address proxy = LibClone.deployDeterministicERC1967BeaconProxy(beacon_, salt_); - // 2. Explicitly initialize after deployment + // Explicitly initialize after deployment (bool success, ) = proxy.call(initData_); require(success, "Initialization failed"); diff --git a/contracts/protocol/AsyncPromise.sol b/contracts/protocol/AsyncPromise.sol index 1758c0de..b295b3e9 100644 --- a/contracts/protocol/AsyncPromise.sol +++ b/contracts/protocol/AsyncPromise.sol @@ -29,7 +29,7 @@ abstract contract AsyncPromiseStorage is IPromise { address public localInvoker; // slot 51 - /// @notice The forwarder address which can call the callback + /// @notice The forwarder address which can set the callback selector and data address public forwarder; // slot 52 @@ -39,11 +39,11 @@ abstract contract AsyncPromiseStorage is IPromise { // slots [53-102] reserved for gap uint256[50] _gap_after; - // slots 103-154 reserved for addr resolver util + // slots 103-154 (51) reserved for addr resolver util } /// @title AsyncPromise -/// @notice this contract stores the callback address and data to be executed once the previous call is executed +/// @notice this contract stores the callback selector and data to be executed once the on-chain call is executed /// This promise expires once the callback is executed contract AsyncPromise is AsyncPromiseStorage, Initializable, AddressResolverUtil { using LibCall for address; @@ -60,25 +60,24 @@ contract AsyncPromise is AsyncPromiseStorage, Initializable, AddressResolverUtil _disableInitializers(); // disable for implementation } - /// @notice Initializer to replace constructor for upgradeable contracts - /// @param invoker_ The address of the local invoker. - /// @param forwarder_ The address of the forwarder. - /// @param addressResolver_ The address resolver contract address. + /// @notice Initialize promise states + /// @param invoker_ The address of the local invoker + /// @param forwarder_ The address of the forwarder + /// @param addressResolver_ The address resolver contract address function initialize( address invoker_, address forwarder_, address addressResolver_ ) public initializer { - _setAddressResolver(addressResolver_); localInvoker = invoker_; forwarder = forwarder_; - state = AsyncPromiseState.WAITING_FOR_SET_CALLBACK_SELECTOR; - resolved = false; + + _setAddressResolver(addressResolver_); } /// @notice Marks the promise as resolved and executes the callback if set. - /// @param returnData_ The data returned from the async payload execution. /// @dev Only callable by the watcher precompile. + /// @param returnData_ The data returned from the async payload execution. function markResolved( uint40 requestCount_, bytes32 payloadId_, @@ -91,6 +90,7 @@ contract AsyncPromise is AsyncPromiseStorage, Initializable, AddressResolverUtil // Call callback to app gateway if (callbackSelector == bytes4(0)) return true; + bytes memory combinedCalldata = abi.encodePacked( callbackSelector, abi.encode(callbackData, returnData_) @@ -117,7 +117,7 @@ contract AsyncPromise is AsyncPromiseStorage, Initializable, AddressResolverUtil AsyncPromiseState state_ ) internal { // to update the state in case selector is bytes(0) but reverting onchain - resolved = false; + resolved = true; state = state_; try IAppGateway(localInvoker).handleRevert(requestCount_, payloadId_) { // Successfully handled revert @@ -134,14 +134,17 @@ contract AsyncPromise is AsyncPromiseStorage, Initializable, AddressResolverUtil bytes4 selector_, bytes memory data_ ) external override returns (address promise_) { + // allows forwarder or local invoker to set the callback selector and data if (msg.sender != forwarder && msg.sender != localInvoker) { revert OnlyForwarderOrLocalInvoker(); } + // if the promise is already set up, revert if (state == AsyncPromiseState.WAITING_FOR_CALLBACK_EXECUTION) { revert PromiseAlreadySetUp(); } + // if the promise is waiting for the callback selector, set it and update the state if (state == AsyncPromiseState.WAITING_FOR_SET_CALLBACK_SELECTOR) { callbackSelector = selector_; callbackData = data_; diff --git a/contracts/protocol/Forwarder.sol b/contracts/protocol/Forwarder.sol index cb611373..7ffe5d5b 100644 --- a/contracts/protocol/Forwarder.sol +++ b/contracts/protocol/Forwarder.sol @@ -6,18 +6,19 @@ import "../interfaces/IMiddleware.sol"; import "../interfaces/IAppGateway.sol"; import "../interfaces/IPromise.sol"; import "../interfaces/IForwarder.sol"; - import {AddressResolverUtil} from "./utils/AddressResolverUtil.sol"; +import {AsyncModifierNotUsed} from "./utils/common/Errors.sol"; import "solady/utils/Initializable.sol"; +/// @title Forwarder Storage +/// @notice Storage contract for the Forwarder contract that contains the state variables abstract contract ForwarderStorage is IForwarder { // slots [0-49] reserved for gap uint256[50] _gap_before; // slot 50 - /// @notice chain id + /// @notice chain slug on which the contract is deployed uint32 public chainSlug; - /// @notice on-chain address associated with this forwarder address public onChainAddress; @@ -25,23 +26,29 @@ abstract contract ForwarderStorage is IForwarder { /// @notice caches the latest async promise address for the last call address public latestAsyncPromise; - // slots [52-101] reserved for gap + // slot 52 + /// @notice the address of the contract that called the latest async promise + address public latestPromiseCaller; + /// @notice the request count of the latest async promise + uint40 public latestRequestCount; + + // slots [53-102] reserved for gap uint256[50] _gap_after; + + // slots 103-154 (51) reserved for addr resolver util } /// @title Forwarder Contract /// @notice This contract acts as a forwarder for async calls to the on-chain contracts. contract Forwarder is ForwarderStorage, Initializable, AddressResolverUtil { - error AsyncModifierNotUsed(); - constructor() { _disableInitializers(); // disable for implementation } /// @notice Initializer to replace constructor for upgradeable contracts - /// @param chainSlug_ chain id - /// @param onChainAddress_ on-chain address - /// @param addressResolver_ address resolver contract address + /// @param chainSlug_ chain slug on which the contract is deployed + /// @param onChainAddress_ on-chain address associated with this forwarder + /// @param addressResolver_ address resolver contract function initialize( uint32 chainSlug_, address onChainAddress_, @@ -54,13 +61,20 @@ contract Forwarder is ForwarderStorage, Initializable, AddressResolverUtil { /// @notice Stores the callback address and data to be executed once the promise is resolved. /// @dev This function should not be called before the fallback function. + /// @dev It resets the latest async promise address /// @param selector_ The function selector for callback /// @param data_ The data to be passed to callback /// @return promise_ The address of the new promise function then(bytes4 selector_, bytes memory data_) external returns (address promise_) { if (latestAsyncPromise == address(0)) revert("Forwarder: no async promise found"); - promise_ = IPromise(latestAsyncPromise).then(selector_, data_); + if (latestPromiseCaller != msg.sender) revert("Forwarder: promise caller mismatch"); + if (latestRequestCount != watcherPrecompile__().nextRequestCount()) + revert("Forwarder: request count mismatch"); + + address latestAsyncPromise_ = latestAsyncPromise; latestAsyncPromise = address(0); + + promise_ = IPromise(latestAsyncPromise_).then(selector_, data_); } /// @notice Returns the on-chain address associated with this forwarder. @@ -69,27 +83,31 @@ contract Forwarder is ForwarderStorage, Initializable, AddressResolverUtil { return onChainAddress; } - /// @notice Returns the chain id - /// @return chain id + /// @notice Returns the chain slug on which the contract is deployed. + /// @return chain slug function getChainSlug() external view returns (uint32) { return chainSlug; } /// @notice Fallback function to process the contract calls to onChainAddress - /// @dev It queues the calls in the auction house and deploys the promise contract - fallback() external payable { - // Retrieve the auction house address from the address resolver. + /// @dev It queues the calls in the middleware and deploys the promise contract + fallback() external { if (address(deliveryHelper__()) == address(0)) { revert("Forwarder: deliveryHelper not found"); } + // validates if the async modifier is set bool isAsyncModifierSet = IAppGateway(msg.sender).isAsyncModifierSet(); if (!isAsyncModifierSet) revert AsyncModifierNotUsed(); // Deploy a new async promise contract. latestAsyncPromise = addressResolver__.deployAsyncPromiseContract(msg.sender); - // Determine if the call is a read or write operation. + // set the latest promise caller and request count for validating if the future .then call is valid + latestPromiseCaller = msg.sender; + latestRequestCount = watcherPrecompile__().nextRequestCount(); + + // fetch the override params from app gateway ( Read isReadCall, Parallel isParallelCall, @@ -99,9 +117,11 @@ contract Forwarder is ForwarderStorage, Initializable, AddressResolverUtil { uint256 value, bytes32 sbType ) = IAppGateway(msg.sender).getOverrideParams(); + + // get the switchboard address from the watcher precompile config address switchboard = watcherPrecompileConfig().switchboards(chainSlug, sbType); - // Queue the call in the auction house. + // Queue the call in the middleware. deliveryHelper__().queue( QueuePayloadParams({ chainSlug: chainSlug, @@ -121,6 +141,4 @@ contract Forwarder is ForwarderStorage, Initializable, AddressResolverUtil { }) ); } - - receive() external payable {} } diff --git a/contracts/protocol/payload-delivery/AuctionManager.sol b/contracts/protocol/payload-delivery/AuctionManager.sol index 860d527e..c2e45473 100644 --- a/contracts/protocol/payload-delivery/AuctionManager.sol +++ b/contracts/protocol/payload-delivery/AuctionManager.sol @@ -4,16 +4,15 @@ pragma solidity ^0.8.0; import {ECDSA} from "solady/utils/ECDSA.sol"; import "solady/utils/Initializable.sol"; import "../utils/AccessControl.sol"; - +import "../../interfaces/IAuctionManager.sol"; import {IMiddleware} from "../../interfaces/IMiddleware.sol"; import {IFeesManager} from "../../interfaces/IFeesManager.sol"; -import {IAuctionManager} from "../../interfaces/IAuctionManager.sol"; - import {AddressResolverUtil} from "../utils/AddressResolverUtil.sol"; -import {Fees, Bid, RequestMetadata, RequestParams} from "../utils/common/Structs.sol"; import {AuctionClosed, AuctionAlreadyStarted, BidExceedsMaxFees, LowerBidAlreadyExists, InvalidTransmitter} from "../utils/common/Errors.sol"; import {TRANSMITTER_ROLE} from "../utils/common/AccessRoles.sol"; +/// @title AuctionManagerStorage +/// @notice Storage for the AuctionManager contract abstract contract AuctionManagerStorage is IAuctionManager { // slots [0-49] reserved for gap uint256[50] _gap_before; @@ -22,31 +21,29 @@ abstract contract AuctionManagerStorage is IAuctionManager { uint32 public evmxSlug; // slot 51 - mapping(uint40 => Bid) public winningBids; + uint256 public maxReAuctionCount; // slot 52 - // requestCount => auction status - mapping(uint40 => bool) public override auctionClosed; + uint256 public auctionEndDelaySeconds; // slot 53 - mapping(uint40 => bool) public override auctionStarted; + mapping(uint40 => Bid) public winningBids; // slot 54 - uint256 public auctionEndDelaySeconds; + // requestCount => auction status + mapping(uint40 => bool) public override auctionClosed; // slot 55 - mapping(address => bool) public whitelistedTransmitters; + mapping(uint40 => bool) public override auctionStarted; // slot 56 mapping(uint40 => uint256) public reAuctionCount; - // slot 57 - uint256 public maxReAuctionCount; - - // slots [57-104] reserved for gap - uint256[48] _gap_after; + // slots [57-106] reserved for gap + uint256[50] _gap_after; - // slots 105-155 reserved for addr resolver util + // slots 107-157 (51) reserved for access control + // slots 158-208 (51) reserved for addr resolver util } /// @title AuctionManager @@ -61,6 +58,7 @@ contract AuctionManager is event AuctionStarted(uint40 requestCount); event AuctionEnded(uint40 requestCount, Bid winningBid); event BidPlaced(uint40 requestCount, Bid bid); + event AuctionEndDelaySecondsSet(uint256 auctionEndDelaySeconds); error InvalidBid(); error MaxReAuctionCountReached(); @@ -84,6 +82,7 @@ contract AuctionManager is ) public reinitializer(1) { _setAddressResolver(addressResolver_); _initializeOwner(owner_); + evmxSlug = evmxSlug_; auctionEndDelaySeconds = auctionEndDelaySeconds_; maxReAuctionCount = maxReAuctionCount_; @@ -91,14 +90,7 @@ contract AuctionManager is function setAuctionEndDelaySeconds(uint256 auctionEndDelaySeconds_) external onlyOwner { auctionEndDelaySeconds = auctionEndDelaySeconds_; - } - - function startAuction(uint40 requestCount_) internal { - if (auctionClosed[requestCount_]) revert AuctionClosed(); - if (auctionStarted[requestCount_]) revert AuctionAlreadyStarted(); - - auctionStarted[requestCount_] = true; - emit AuctionStarted(requestCount_); + emit AuctionEndDelaySecondsSet(auctionEndDelaySeconds_); } /// @notice Places a bid for an auction @@ -113,25 +105,34 @@ contract AuctionManager is ) external { if (auctionClosed[requestCount_]) revert AuctionClosed(); + // check if the transmitter is valid address transmitter = _recoverSigner( keccak256(abi.encode(address(this), evmxSlug, requestCount_, fee, extraData)), transmitterSignature ); if (!_hasRole(TRANSMITTER_ROLE, transmitter)) revert InvalidTransmitter(); + // create a new bid Bid memory newBid = Bid({fee: fee, transmitter: transmitter, extraData: extraData}); + // get the request metadata RequestMetadata memory requestMetadata = IMiddleware(addressResolver__.deliveryHelper()) .getRequestMetadata(requestCount_); - if (fee > requestMetadata.fees.amount) revert BidExceedsMaxFees(); + + // check if the bid is for this auction manager if (requestMetadata.auctionManager != address(this)) revert InvalidBid(); + // check if the bid exceeds the max fees quoted by app gateway + if (fee > requestMetadata.fees.amount) revert BidExceedsMaxFees(); + // check if the bid is lower than the existing bid if ( winningBids[requestCount_].transmitter != address(0) && fee >= winningBids[requestCount_].fee ) revert LowerBidAlreadyExists(); + // update the winning bid winningBids[requestCount_] = newBid; + // block the fees IFeesManager(addressResolver__.feesManager()).blockFees( requestMetadata.appGateway, requestMetadata.fees, @@ -139,8 +140,9 @@ contract AuctionManager is requestCount_ ); + // end the auction if the no auction end delay if (auctionEndDelaySeconds > 0) { - startAuction(requestCount_); + _startAuction(requestCount_); watcherPrecompile__().setTimeout( auctionEndDelaySeconds, abi.encodeWithSelector(this.endAuction.selector, requestCount_) @@ -150,31 +152,42 @@ contract AuctionManager is } emit BidPlaced(requestCount_, newBid); - auctionClosed[requestCount_] = true; } /// @notice Ends an auction /// @param requestCount_ The ID of the auction function endAuction(uint40 requestCount_) external onlyWatcherPrecompile { + if (auctionClosed[requestCount_]) return; _endAuction(requestCount_); } function _endAuction(uint40 requestCount_) internal { - auctionClosed[requestCount_] = true; + // get the winning bid, if no transmitter is set, revert Bid memory winningBid = winningBids[requestCount_]; if (winningBid.transmitter == address(0)) revert InvalidTransmitter(); + auctionClosed[requestCount_] = true; + + // set the timeout for the bid expiration + // useful in case a transmitter did bid but did not execute payloads watcherPrecompile__().setTimeout( IMiddleware(addressResolver__.deliveryHelper()).bidTimeout(), abi.encodeWithSelector(this.expireBid.selector, requestCount_) ); + + // start the request processing, it will finalize the request IMiddleware(addressResolver__.deliveryHelper()).startRequestProcessing( requestCount_, winningBid ); + emit AuctionEnded(requestCount_, winningBid); } + /// @notice Expires a bid and restarts an auction in case a request is not fully executed. + /// @dev Auction can be restarted only for `maxReAuctionCount` times. + /// @dev It also unblocks the fees from last transmitter to be assigned to the new winner. + /// @param requestCount_ The request id function expireBid(uint40 requestCount_) external onlyWatcherPrecompile { if (reAuctionCount[requestCount_] >= maxReAuctionCount) revert MaxReAuctionCountReached(); RequestParams memory requestParams = watcherPrecompile__().getRequestParams(requestCount_); @@ -189,6 +202,14 @@ contract AuctionManager is emit AuctionRestarted(requestCount_); } + function _startAuction(uint40 requestCount_) internal { + if (auctionClosed[requestCount_]) revert AuctionClosed(); + if (auctionStarted[requestCount_]) revert AuctionAlreadyStarted(); + + auctionStarted[requestCount_] = true; + emit AuctionStarted(requestCount_); + } + function _recoverSigner( bytes32 digest_, bytes memory signature_ diff --git a/contracts/protocol/payload-delivery/ContractFactoryPlug.sol b/contracts/protocol/payload-delivery/ContractFactoryPlug.sol index 94634e8b..3ef28ada 100644 --- a/contracts/protocol/payload-delivery/ContractFactoryPlug.sol +++ b/contracts/protocol/payload-delivery/ContractFactoryPlug.sol @@ -21,11 +21,22 @@ contract ContractFactoryPlug is PlugBase, AccessControl, IContractFactoryPlug { error DeploymentFailed(); error ExecutionFailed(); + /// @notice Constructor for the ContractFactoryPlug + /// @param socket_ The socket address + /// @param owner_ The owner address constructor(address socket_, address owner_) { _initializeOwner(owner_); _setSocket(socket_); } + /// @notice Deploys a contract + /// @param isPlug_ Whether the contract to be deployed is a plug + /// @param salt_ The salt used for create 2 + /// @param appGatewayId_ The app gateway id + /// @param switchboard_ The switchboard address + /// @param creationCode_ The creation code + /// @param initCallData_ The init call data + /// @return addr The address of the deployed contract function deployContract( IsPlug isPlug_, bytes32 salt_, @@ -33,12 +44,7 @@ contract ContractFactoryPlug is PlugBase, AccessControl, IContractFactoryPlug { address switchboard_, bytes memory creationCode_, bytes memory initCallData_ - ) public override returns (address) { - if (msg.sender != address(socket__)) { - revert NotSocket(); - } - - address addr; + ) public override onlySocket returns (address addr) { assembly { addr := create2(callvalue(), add(creationCode_, 0x20), mload(creationCode_), salt_) if iszero(addr) { @@ -73,7 +79,6 @@ contract ContractFactoryPlug is PlugBase, AccessControl, IContractFactoryPlug { } emit Deployed(addr, salt_, returnData); - return addr; } /// @notice Gets the address for a deployed contract diff --git a/contracts/protocol/payload-delivery/FeesManager.sol b/contracts/protocol/payload-delivery/FeesManager.sol index 7b9804b5..6f600fb7 100644 --- a/contracts/protocol/payload-delivery/FeesManager.sol +++ b/contracts/protocol/payload-delivery/FeesManager.sol @@ -9,7 +9,7 @@ import {IFeesPlug} from "../../interfaces/IFeesPlug.sol"; import {IFeesManager} from "../../interfaces/IFeesManager.sol"; import {AddressResolverUtil} from "../utils/AddressResolverUtil.sol"; -import {NotAuctionManager} from "../utils/common/Errors.sol"; +import {NotAuctionManager, InvalidWatcherSignature, NonceUsed} from "../utils/common/Errors.sol"; import {Bid, Fees, CallType, Parallel, WriteFinality, TokenBalance, QueuePayloadParams, IsPlug, PayloadSubmitParams, RequestParams, RequestMetadata} from "../utils/common/Structs.sol"; abstract contract FeesManagerStorage is IFeesManager { @@ -49,7 +49,7 @@ abstract contract FeesManagerStorage is IFeesManager { // slots [57-106] reserved for gap uint256[50] _gap_after; - // slots 107-157 reserved for addr resolver util + // slots 107-157 (51) reserved for addr resolver util } /// @title FeesManager @@ -110,10 +110,6 @@ contract FeesManager is FeesManagerStorage, Initializable, Ownable, AddressResol error NoFeesForTransmitter(); /// @notice Error thrown when no fees was blocked error NoFeesBlocked(); - /// @notice Error thrown when watcher signature is invalid - error InvalidWatcherSignature(); - /// @notice Error thrown when nonce is used - error NonceUsed(); /// @notice Error thrown when caller is invalid error InvalidCaller(); @@ -191,7 +187,7 @@ contract FeesManager is FeesManagerStorage, Initializable, Ownable, AddressResol return availableFees >= fees_.amount; } - /// @notice Blocks fees for transmitter + /// @notice Blocks fees for a request count /// @param originAppGateway_ The app gateway address /// @param feesGivenByApp_ The fees data struct given by the app gateway /// @param requestCount_ The batch identifier @@ -400,6 +396,9 @@ contract FeesManager is FeesManagerStorage, Initializable, Ownable, AddressResol }); } + /// @notice hook called by watcher precompile when request is finished + function finishRequest(uint40) external {} + function _queue(uint32 chainSlug_, bytes memory payload_) internal { QueuePayloadParams memory queuePayloadParams = _createQueuePayloadParams( chainSlug_, diff --git a/contracts/protocol/payload-delivery/FeesPlug.sol b/contracts/protocol/payload-delivery/FeesPlug.sol index c73200e2..af405d1f 100644 --- a/contracts/protocol/payload-delivery/FeesPlug.sol +++ b/contracts/protocol/payload-delivery/FeesPlug.sol @@ -5,23 +5,28 @@ import "solady/utils/SafeTransferLib.sol"; import "../../base/PlugBase.sol"; import "../utils/AccessControl.sol"; import {RESCUE_ROLE} from "../utils/common/AccessRoles.sol"; +import {IFeesPlug} from "../../interfaces/IFeesPlug.sol"; import "../utils/RescueFundsLib.sol"; import {ETH_ADDRESS} from "../utils/common/Constants.sol"; -import {InvalidTokenAddress} from "../utils/common/Errors.sol"; +import {InvalidTokenAddress, FeesAlreadyPaid} from "../utils/common/Errors.sol"; /// @title FeesManager -/// @notice Abstract contract for managing fees -contract FeesPlug is PlugBase, AccessControl { - mapping(address => uint256) public balanceOf; - mapping(bytes32 => bool) public feesRedeemed; +/// @notice Contract for managing fees on a network +/// @dev The amount deposited here is locked and updated in the EVMx for an app gateway +/// @dev The fees are redeemed by the transmitters executing request or can be withdrawn by the owner +contract FeesPlug is IFeesPlug, PlugBase, AccessControl { + /// @notice Mapping to store the balance of each token + mapping(address => uint256) public override balanceOf; + /// @notice Mapping to store if fees have been redeemed for a given fees ID + mapping(bytes32 => bool) public override feesRedeemed; + /// @notice Mapping to store if a token is whitelisted mapping(address => bool) public whitelistedTokens; - /// @notice Error thrown when attempting to pay fees again - error FeesAlreadyPaid(); /// @notice Error thrown when balance is not enough to cover fees error InsufficientTokenBalance(address token_); /// @notice Error thrown when deposit amount does not match msg.value error InvalidDepositAmount(); + /// @notice Error thrown when token is not whitelisted error TokenNotWhitelisted(address token_); /// @notice Event emitted when fees are deposited @@ -33,38 +38,53 @@ contract FeesPlug is PlugBase, AccessControl { /// @notice Event emitted when a token is removed from whitelist event TokenRemovedFromWhitelist(address token); + /// @notice Modifier to check if the balance of a token is enough to withdraw modifier isFeesEnough(uint256 fee_, address feeToken_) { if (balanceOf[feeToken_] < fee_) revert InsufficientTokenBalance(feeToken_); _; } + /// @notice Constructor for the FeesPlug contract + /// @param socket_ The socket address + /// @param owner_ The owner address constructor(address socket_, address owner_) { _setSocket(socket_); _initializeOwner(owner_); - whitelistedTokens[ETH_ADDRESS] = true; // ETH is whitelisted by default + + // ETH is whitelisted by default + whitelistedTokens[ETH_ADDRESS] = true; } + /// @notice Distributes fees to the transmitter + /// @param feeToken_ The token address + /// @param fee_ The amount of fees + /// @param transmitter_ The transmitter address + /// @param feesId_ The fees ID function distributeFee( address feeToken_, uint256 fee_, address transmitter_, bytes32 feesId_ - ) external onlySocket isFeesEnough(fee_, feeToken_) { + ) external override onlySocket isFeesEnough(fee_, feeToken_) { if (feesRedeemed[feesId_]) revert FeesAlreadyPaid(); feesRedeemed[feesId_] = true; - balanceOf[feeToken_] -= fee_; + _transferTokens(feeToken_, fee_, transmitter_); } + /// @notice Withdraws fees + /// @param token_ The token address + /// @param amount_ The amount + /// @param receiver_ The receiver address function withdrawFees( address token_, uint256 amount_, address receiver_ - ) external onlySocket isFeesEnough(amount_, token_) { + ) external override onlySocket isFeesEnough(amount_, token_) { balanceOf[token_] -= amount_; - _transferTokens(token_, amount_, receiver_); + _transferTokens(token_, amount_, receiver_); emit FeesWithdrawn(token_, amount_, receiver_); } @@ -72,7 +92,11 @@ contract FeesPlug is PlugBase, AccessControl { /// @param token_ The token address /// @param amount_ The amount /// @param appGateway_ The app gateway address - function deposit(address token_, address appGateway_, uint256 amount_) external payable { + function deposit( + address token_, + address appGateway_, + uint256 amount_ + ) external payable override { if (!whitelistedTokens[token_]) revert TokenNotWhitelisted(token_); if (token_ == ETH_ADDRESS) { @@ -140,7 +164,5 @@ contract FeesPlug is PlugBase, AccessControl { RescueFundsLib._rescueFunds(token_, rescueTo_, amount_); } - fallback() external payable {} - receive() external payable {} } diff --git a/contracts/protocol/payload-delivery/app-gateway/DeliveryHelper.sol b/contracts/protocol/payload-delivery/app-gateway/DeliveryHelper.sol index 81d9e7b3..52e58f4a 100644 --- a/contracts/protocol/payload-delivery/app-gateway/DeliveryHelper.sol +++ b/contracts/protocol/payload-delivery/app-gateway/DeliveryHelper.sol @@ -3,6 +3,8 @@ pragma solidity ^0.8.0; import "./FeesHelpers.sol"; +/// @title DeliveryHelper +/// @notice Contract for managing payload delivery contract DeliveryHelper is FeesHelpers { constructor() { _disableInitializers(); // disable for implementation @@ -17,14 +19,15 @@ contract DeliveryHelper is FeesHelpers { uint128 bidTimeout_ ) public reinitializer(1) { _setAddressResolver(addressResolver_); - bidTimeout = bidTimeout_; _initializeOwner(owner_); - } - function endTimeout(uint40 requestCount_) external onlyWatcherPrecompile { - IAuctionManager(requests[requestCount_].auctionManager).endAuction(requestCount_); + bidTimeout = bidTimeout_; } + /// @notice Calls the watcher precompile to start processing a request + /// @dev If a transmitter was already assigned, it updates the transmitter in watcher precompile too + /// @param requestCount_ The ID of the request + /// @param winningBid_ The winning bid function startRequestProcessing( uint40 requestCount_, Bid memory winningBid_ @@ -33,8 +36,8 @@ contract DeliveryHelper is FeesHelpers { if (winningBid_.transmitter == address(0)) revert InvalidTransmitter(); RequestMetadata storage requestMetadata_ = requests[requestCount_]; + // if a transmitter was already assigned, it means the request was restarted bool isRestarted = requestMetadata_.winningBid.transmitter != address(0); - requestMetadata_.winningBid.transmitter = winningBid_.transmitter; if (!isRestarted) { @@ -44,6 +47,8 @@ contract DeliveryHelper is FeesHelpers { } } + /// @notice Finishes the request processing by assigning fees and calling the on complete hook on app gateway + /// @param requestCount_ The ID of the request function finishRequest(uint40 requestCount_) external onlyWatcherPrecompile { RequestMetadata storage requestMetadata_ = requests[requestCount_]; @@ -60,32 +65,32 @@ contract DeliveryHelper is FeesHelpers { ); } - /// @notice Cancels a request + /// @notice Cancels a request and settles the fees + /// @dev if no transmitter was assigned, fees is unblocked to app gateway + /// @dev Only app gateway can call this function /// @param requestCount_ The ID of the request function cancelRequest(uint40 requestCount_) external { if (msg.sender != requests[requestCount_].appGateway) { revert OnlyAppGateway(); } - // If the request has a winning bid, ie. transmitter already assigned, unblock and assign fees - if (requests[requestCount_].winningBid.transmitter != address(0)) { - IFeesManager(addressResolver__.feesManager()).unblockAndAssignFees( - requestCount_, - requests[requestCount_].winningBid.transmitter, - requests[requestCount_].appGateway - ); - } else { - // If the request has no winning bid, ie. transmitter not assigned, unblock fees - IFeesManager(addressResolver__.feesManager()).unblockFees(requestCount_); - } + _settleFees(requestCount_); watcherPrecompile__().cancelRequest(requestCount_); emit RequestCancelled(requestCount_); } - /// @notice Handles request reverts + /// @notice For request reverts, settles the fees /// @param requestCount_ The ID of the request function handleRequestReverts(uint40 requestCount_) external onlyWatcherPrecompile { - // assign fees after expiry time + _settleFees(requestCount_); + } + + /// @notice Settles the fees for a request + /// @dev If a transmitter was already assigned, it unblocks and assigns fees to the transmitter + /// @dev If no transmitter was assigned, it unblocks fees to the app gateway + /// @param requestCount_ The ID of the request + function _settleFees(uint40 requestCount_) internal { + // If the request has a winning bid, ie. transmitter already assigned, unblock and assign fees if (requests[requestCount_].winningBid.transmitter != address(0)) { IFeesManager(addressResolver__.feesManager()).unblockAndAssignFees( requestCount_, @@ -93,10 +98,14 @@ contract DeliveryHelper is FeesHelpers { requests[requestCount_].appGateway ); } else { + // If the request has no winning bid, ie. transmitter not assigned, unblock fees IFeesManager(addressResolver__.feesManager()).unblockFees(requestCount_); } } + /// @notice Returns the request metadata + /// @param requestCount_ The ID of the request + /// @return requestMetadata The request metadata function getRequestMetadata( uint40 requestCount_ ) external view returns (RequestMetadata memory) { diff --git a/contracts/protocol/payload-delivery/app-gateway/DeliveryHelperStorage.sol b/contracts/protocol/payload-delivery/app-gateway/DeliveryHelperStorage.sol index de3c471c..6b986bab 100644 --- a/contracts/protocol/payload-delivery/app-gateway/DeliveryHelperStorage.sol +++ b/contracts/protocol/payload-delivery/app-gateway/DeliveryHelperStorage.sol @@ -1,19 +1,15 @@ // SPDX-License-Identifier: Unlicense pragma solidity ^0.8.21; -import {IMiddleware} from "../../../interfaces/IMiddleware.sol"; -import {IPromise} from "../../../interfaces/IPromise.sol"; +import "../../../interfaces/IMiddleware.sol"; import {IAddressResolver} from "../../../interfaces/IAddressResolver.sol"; import {IContractFactoryPlug} from "../../../interfaces/IContractFactoryPlug.sol"; import {IAppGateway} from "../../../interfaces/IAppGateway.sol"; -import {IAppGateway} from "../../../interfaces/IAppGateway.sol"; -import {IAddressResolver} from "../../../interfaces/IAddressResolver.sol"; import {IAuctionManager} from "../../../interfaces/IAuctionManager.sol"; import {IFeesManager} from "../../../interfaces/IFeesManager.sol"; -import {QueuePayloadParams, Fees, CallType, Bid, Parallel, IsPlug, WriteFinality, RequestMetadata} from "../../utils/common/Structs.sol"; -import {NotAuctionManager, InvalidPromise, InvalidIndex, PromisesNotResolved, InvalidTransmitter} from "../../utils/common/Errors.sol"; -import {FORWARD_CALL, DISTRIBUTE_FEE, DEPLOY, QUERY, FINALIZE} from "../../utils/common/Constants.sol"; +import {NotAuctionManager, InvalidTransmitter, InvalidIndex} from "../../utils/common/Errors.sol"; +import {DEPLOY, PAYLOAD_SIZE_LIMIT, REQUEST_PAYLOAD_COUNT_LIMIT} from "../../utils/common/Constants.sol"; /// @title DeliveryHelperStorage /// @notice Storage contract for DeliveryHelper @@ -21,15 +17,28 @@ abstract contract DeliveryHelperStorage is IMiddleware { // slots [0-49] reserved for gap uint256[50] _gap_before; + // slot 50 + /// @notice The timeout after which a bid expires uint128 public bidTimeout; + + // slot 51 + /// @notice The counter for the salt used to generate/deploy the contract address uint256 public saltCounter; - /// @notice The call parameters array + // slot 52 + /// @notice The parameters array used to store payloads for a request QueuePayloadParams[] public queuePayloadParams; + // slot 53 + /// @notice The metadata for a request mapping(uint40 => RequestMetadata) public requests; + // slot 54 + /// @notice The maximum message value limit for a chain mapping(uint32 => uint256) public chainMaxMsgValueLimit; - // slots [59-108] reserved for gap - uint256[49] _gap_after; + + // slots [55-104] reserved for gap + uint256[50] _gap_after; + + // slots 105-155 (51) reserved for addr resolver utils } diff --git a/contracts/protocol/payload-delivery/app-gateway/DeliveryUtils.sol b/contracts/protocol/payload-delivery/app-gateway/DeliveryUtils.sol index caa890f8..9bd0baa9 100644 --- a/contracts/protocol/payload-delivery/app-gateway/DeliveryUtils.sol +++ b/contracts/protocol/payload-delivery/app-gateway/DeliveryUtils.sol @@ -1,11 +1,10 @@ // SPDX-License-Identifier: Unlicense pragma solidity ^0.8.21; -import {Ownable} from "solady/auth/Ownable.sol"; import "solady/utils/Initializable.sol"; +import {Ownable} from "solady/auth/Ownable.sol"; import {AddressResolverUtil} from "../../utils/AddressResolverUtil.sol"; import "./DeliveryHelperStorage.sol"; -import {PayloadSubmitParams} from "../../utils/common/Structs.sol"; /// @notice Abstract contract for managing asynchronous payloads abstract contract DeliveryUtils is @@ -14,16 +13,9 @@ abstract contract DeliveryUtils is Ownable, AddressResolverUtil { - // slots [0-108] reserved for delivery helper storage and [109-159] reserved for addr resolver util - // slots [160-209] reserved for gap + // slots [156-206] reserved for gap uint256[50] _gap_delivery_utils; - /// @notice Error thrown when attempting to executed payloads after all have been executed - error AllPayloadsExecuted(); - /// @notice Error thrown request did not come from Forwarder address - error NotFromForwarder(); - /// @notice Error thrown when a payload call fails - error CallFailed(bytes32 payloadId); /// @notice Error thrown if payload is too large error PayloadTooLarge(); /// @notice Error thrown if trying to cancel a batch without being the application gateway @@ -40,8 +32,6 @@ abstract contract DeliveryUtils is /// @notice Error thrown when a maximum message value limit is exceeded error MaxMsgValueLimitExceeded(); - event CallBackReverted(uint40 requestCount_, bytes32 payloadId_); - event RequestCancelled(uint40 indexed requestCount); event BidTimeoutUpdated(uint256 newBidTimeout); event PayloadSubmitted( uint40 indexed requestCount, @@ -57,6 +47,10 @@ abstract contract DeliveryUtils is uint40 indexed requestCount, uint256 newMaxFees ); + /// @notice Emitted when chain max message value limits are updated + event ChainMaxMsgValueLimitsUpdated(uint32[] chainSlugs, uint256[] maxMsgValueLimits); + /// @notice Emitted when a request is cancelled + event RequestCancelled(uint40 indexed requestCount); modifier onlyAuctionManager(uint40 requestCount_) { if (msg.sender != requests[requestCount_].auctionManager) revert NotAuctionManager(); @@ -89,5 +83,7 @@ abstract contract DeliveryUtils is for (uint256 i = 0; i < chainSlugs_.length; i++) { chainMaxMsgValueLimit[chainSlugs_[i]] = maxMsgValueLimits_[i]; } + + emit ChainMaxMsgValueLimitsUpdated(chainSlugs_, maxMsgValueLimits_); } } diff --git a/contracts/protocol/payload-delivery/app-gateway/FeesHelpers.sol b/contracts/protocol/payload-delivery/app-gateway/FeesHelpers.sol index 4655c8b8..ad64dc3b 100644 --- a/contracts/protocol/payload-delivery/app-gateway/FeesHelpers.sol +++ b/contracts/protocol/payload-delivery/app-gateway/FeesHelpers.sol @@ -6,11 +6,16 @@ import "./RequestQueue.sol"; /// @title RequestAsync /// @notice Abstract contract for managing asynchronous payload batches abstract contract FeesHelpers is RequestQueue { - // slots [210-259] reserved for gap + // slots [258-308] reserved for gap uint256[50] _gap_batch_async; + /// @notice Increases the fees for a request if no bid is placed + /// @param requestCount_ The ID of the request + /// @param newMaxFees_ The new maximum fees function increaseFees(uint40 requestCount_, uint256 newMaxFees_) external override { address appGateway = _getCoreAppGateway(msg.sender); + + // todo: should we allow core app gateway too? if (appGateway != requests[requestCount_].appGateway) { revert OnlyAppGateway(); } @@ -45,6 +50,9 @@ abstract contract FeesHelpers is RequestQueue { return _batch(msg.sender, auctionManager_, fees_, bytes("")); } + /// @notice Returns the fees for a request + /// @param requestCount_ The ID of the request + /// @return fees The fees data function getFees(uint40 requestCount_) external view returns (Fees memory) { return requests[requestCount_].fees; } diff --git a/contracts/protocol/payload-delivery/app-gateway/RequestQueue.sol b/contracts/protocol/payload-delivery/app-gateway/RequestQueue.sol index 5338e2bc..250e9181 100644 --- a/contracts/protocol/payload-delivery/app-gateway/RequestQueue.sol +++ b/contracts/protocol/payload-delivery/app-gateway/RequestQueue.sol @@ -1,15 +1,11 @@ // SPDX-License-Identifier: Unlicense pragma solidity ^0.8.21; -import {Ownable} from "solady/auth/Ownable.sol"; -import "solady/utils/Initializable.sol"; -import {AddressResolverUtil} from "../../utils/AddressResolverUtil.sol"; import "./DeliveryUtils.sol"; -import {PAYLOAD_SIZE_LIMIT, REQUEST_PAYLOAD_COUNT_LIMIT} from "../../utils/common/Constants.sol"; + /// @notice Abstract contract for managing asynchronous payloads abstract contract RequestQueue is DeliveryUtils { - // slots [0-108] reserved for delivery helper storage and [109-159] reserved for addr resolver util - // slots [160-209] reserved for gap + // slots [207-257] reserved for gap uint256[50] _gap_queue_async; /// @notice Clears the call parameters array @@ -20,8 +16,6 @@ abstract contract RequestQueue is DeliveryUtils { /// @notice Queues a new payload /// @param queuePayloadParams_ The call parameters function queue(QueuePayloadParams memory queuePayloadParams_) external { - if (queuePayloadParams.length > REQUEST_PAYLOAD_COUNT_LIMIT) - revert RequestPayloadCountLimitExceeded(); queuePayloadParams.push(queuePayloadParams_); } @@ -38,6 +32,27 @@ abstract contract RequestQueue is DeliveryUtils { return _batch(appGateway, auctionManager_, fees_, onCompleteData_); } + function _checkBatch( + address appGateway_, + address auctionManager_, + Fees memory fees_ + ) internal view returns (address) { + if (queuePayloadParams.length > REQUEST_PAYLOAD_COUNT_LIMIT) + revert RequestPayloadCountLimitExceeded(); + + if (!IFeesManager(addressResolver__.feesManager()).isFeesEnough(appGateway_, fees_)) + revert InsufficientFees(); + + return + auctionManager_ == address(0) + ? IAddressResolver(addressResolver__).defaultAuctionManager() + : auctionManager_; + } + + /// @notice Initiates a batch of payloads + /// @dev it checks fees, payload limits and creates the payload submit params array after assigning proper levels + /// @dev It also modifies the deploy payloads as needed by contract factory plug + /// @dev Stores request metadata and submits the request to watcher precompile function _batch( address appGateway_, address auctionManager_, @@ -45,16 +60,13 @@ abstract contract RequestQueue is DeliveryUtils { bytes memory onCompleteData_ ) internal returns (uint40 requestCount) { if (queuePayloadParams.length == 0) return 0; - if (!IFeesManager(addressResolver__.feesManager()).isFeesEnough(appGateway_, fees_)) - revert InsufficientFees(); + auctionManager_ = _checkBatch(appGateway_, auctionManager_, fees_); + // create the payload submit params array in desired format ( PayloadSubmitParams[] memory payloadSubmitParamsArray, - , bool onlyReadRequests ) = _createPayloadSubmitParamsArray(); - if (auctionManager_ == address(0)) - auctionManager_ = IAddressResolver(addressResolver__).defaultAuctionManager(); RequestMetadata memory requestMetadata = RequestMetadata({ appGateway: appGateway_, @@ -64,7 +76,7 @@ abstract contract RequestQueue is DeliveryUtils { onCompleteData: onCompleteData_, onlyReadRequests: onlyReadRequests }); - + // process and submit the queue of payloads to watcher precompile requestCount = watcherPrecompile__().submitRequest(payloadSubmitParamsArray); requests[requestCount] = requestMetadata; @@ -87,34 +99,50 @@ abstract contract RequestQueue is DeliveryUtils { /// @return payloadDetailsArray An array of payload details function _createPayloadSubmitParamsArray() internal - returns ( - PayloadSubmitParams[] memory payloadDetailsArray, - uint256 totalLevels, - bool onlyReadRequests - ) + returns (PayloadSubmitParams[] memory payloadDetailsArray, bool onlyReadRequests) { - if (queuePayloadParams.length == 0) - return (payloadDetailsArray, totalLevels, onlyReadRequests); payloadDetailsArray = new PayloadSubmitParams[](queuePayloadParams.length); - - totalLevels = 0; onlyReadRequests = queuePayloadParams[0].callType == CallType.READ; + + uint256 currentLevel = 0; for (uint256 i = 0; i < queuePayloadParams.length; i++) { if (queuePayloadParams[i].callType != CallType.READ) { onlyReadRequests = false; } - // Update level for sequential calls + // Update level for calls if (i > 0 && queuePayloadParams[i].isParallel != Parallel.ON) { - totalLevels = totalLevels + 1; + currentLevel = currentLevel + 1; } - payloadDetailsArray[i] = _createPayloadDetails(totalLevels, queuePayloadParams[i]); + payloadDetailsArray[i] = _createPayloadDetails(currentLevel, queuePayloadParams[i]); } clearQueue(); } + function _createDeployPayloadDetails( + QueuePayloadParams memory queuePayloadParams_ + ) internal returns (bytes memory payload, address target) { + bytes32 salt = keccak256( + abi.encode(queuePayloadParams_.appGateway, queuePayloadParams_.chainSlug, saltCounter++) + ); + + // app gateway is set in the plug deployed on chain + payload = abi.encodeWithSelector( + IContractFactoryPlug.deployContract.selector, + queuePayloadParams_.isPlug, + salt, + queuePayloadParams_.appGateway, + queuePayloadParams_.switchboard, + queuePayloadParams_.payload, + queuePayloadParams_.initCallData + ); + + // getting app gateway for deployer as the plug is connected to the app gateway + target = getDeliveryHelperPlugAddress(queuePayloadParams_.chainSlug); + } + /// @notice Creates the payload details for a given call parameters /// @param queuePayloadParams_ The call parameters /// @return payloadDetails The payload details @@ -122,35 +150,16 @@ abstract contract RequestQueue is DeliveryUtils { uint256 level_, QueuePayloadParams memory queuePayloadParams_ ) internal returns (PayloadSubmitParams memory) { - bytes memory payload_ = queuePayloadParams_.payload; + bytes memory payload = queuePayloadParams_.payload; address target = queuePayloadParams_.target; if (queuePayloadParams_.callType == CallType.DEPLOY) { - // getting app gateway for deployer as the plug is connected to the app gateway - bytes32 salt_ = keccak256( - abi.encode( - queuePayloadParams_.appGateway, - queuePayloadParams_.chainSlug, - saltCounter++ - ) - ); - - // app gateway is set in the plug deployed on chain - payload_ = abi.encodeWithSelector( - IContractFactoryPlug.deployContract.selector, - queuePayloadParams_.isPlug, - salt_, - queuePayloadParams_.appGateway, - queuePayloadParams_.switchboard, - payload_, - queuePayloadParams_.initCallData - ); - - target = getDeliveryHelperPlugAddress(queuePayloadParams_.chainSlug); + (payload, target) = _createDeployPayloadDetails(queuePayloadParams_); } - if (payload_.length > PAYLOAD_SIZE_LIMIT) revert PayloadTooLarge(); + if (payload.length > PAYLOAD_SIZE_LIMIT) revert PayloadTooLarge(); if (queuePayloadParams_.value > chainMaxMsgValueLimit[queuePayloadParams_.chainSlug]) revert MaxMsgValueLimitExceeded(); + return PayloadSubmitParams({ levelNumber: level_, @@ -167,7 +176,7 @@ abstract contract RequestQueue is DeliveryUtils { : queuePayloadParams_.gasLimit, value: queuePayloadParams_.value, readAt: queuePayloadParams_.readAt, - payload: payload_ + payload: payload }); } } diff --git a/contracts/protocol/socket/Socket.sol b/contracts/protocol/socket/Socket.sol index eb8313e7..83a455b6 100644 --- a/contracts/protocol/socket/Socket.sol +++ b/contracts/protocol/socket/Socket.sol @@ -1,19 +1,13 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity ^0.8.21; -import "./SocketUtils.sol"; import {LibCall} from "solady/utils/LibCall.sol"; -import {IPlug} from "../../interfaces/IPlug.sol"; -import {PlugDisconnected, InvalidAppGateway} from "../utils/common/Errors.sol"; -import {MAX_COPY_BYTES} from "../utils/common/Constants.sol"; +import "./SocketUtils.sol"; /** - * @title SocketDst - * @dev SocketDst is an abstract contract that inherits from SocketUtils and - * provides functionality for payload execution, verification. - * It manages the mapping of payload execution status - * timestamps - * It also includes functions for payload execution and verification + * @title Socket + * @dev Socket is an abstract contract that inherits from SocketUtils and SocketConfig and + * provides functionality for payload execution, verification, and management of payload execution status */ contract Socket is SocketUtils { using LibCall for address; @@ -29,14 +23,29 @@ contract Socket is SocketUtils { * @dev Error emitted when verification fails */ error VerificationFailed(); - /** * @dev Error emitted when less gas limit is provided for execution than expected */ error LowGasLimit(); + /** + * @dev Error emitted when the chain slug is invalid + */ error InvalidSlug(); + /** + * @dev Error emitted when the deadline has passed + */ error DeadlinePassed(); + /** + * @dev Error emitted when the message value is insufficient + */ error InsufficientMsgValue(); + + /** + * @notice Constructor for the Socket contract + * @param chainSlug_ The chain slug + * @param owner_ The owner of the contract + * @param version_ The version of the contract + */ constructor( uint32 chainSlug_, address owner_, @@ -50,25 +59,39 @@ contract Socket is SocketUtils { ExecuteParams memory executeParams_, bytes memory transmitterSignature_ ) external payable returns (bytes memory) { + // check if the deadline has passed if (executeParams_.deadline < block.timestamp) revert DeadlinePassed(); + PlugConfig memory plugConfig = _plugConfigs[executeParams_.target]; + // check if the plug is disconnected if (plugConfig.appGatewayId == bytes32(0)) revert PlugDisconnected(); + // check if the message value is insufficient if (msg.value < executeParams_.value) revert InsufficientMsgValue(); + + // create the payload id bytes32 payloadId = _createPayloadId(plugConfig.switchboard, executeParams_); + + // validate the execution status _validateExecutionStatus(payloadId); address transmitter = transmitterSignature_.length > 0 ? _recoverSigner(keccak256(abi.encode(address(this), payloadId)), transmitterSignature_) : address(0); + // create the digest + // transmitter, payloadId, appGateway, executeParams_ and there contents are validated using digest verification from switchboard bytes32 digest = _createDigest( transmitter, payloadId, plugConfig.appGatewayId, executeParams_ ); + + // verify the digest _verify(digest, payloadId, plugConfig.switchboard); + + // execute the payload and return the data return _execute(payloadId, executeParams_); } @@ -78,8 +101,9 @@ contract Socket is SocketUtils { function _verify(bytes32 digest_, bytes32 payloadId_, address switchboard_) internal view { if (isValidSwitchboard[switchboard_] != SwitchboardStatus.REGISTERED) revert InvalidSwitchboard(); + // NOTE: is the the first un-trusted call in the system, another one is Plug.call - if (!ISwitchboard(switchboard_).allowPacket(digest_, payloadId_)) + if (!ISwitchboard(switchboard_).allowPayload(digest_, payloadId_)) revert VerificationFailed(); } @@ -92,6 +116,7 @@ contract Socket is SocketUtils { bytes32 payloadId_, ExecuteParams memory executeParams_ ) internal returns (bytes memory) { + // check if the gas limit is sufficient if (gasleft() < executeParams_.gasLimit) revert LowGasLimit(); // NOTE: external un-trusted call @@ -102,6 +127,7 @@ contract Socket is SocketUtils { executeParams_.payload ); + // if the execution failed, set the execution status to reverted if (!success) { payloadExecuted[payloadId_] = ExecutionStatus.Reverted; emit ExecutionFailed(payloadId_, returnData); @@ -119,14 +145,14 @@ contract Socket is SocketUtils { } //////////////////////////////////////////////////////// - ////////////////////// OPERATIONS ////////////////////////// + ////////////////////// Trigger ////////////////////// //////////////////////////////////////////////////////// /** - * @notice To send message to a connected remote chain. Should only be called by a plug. + * @notice To trigger to a connected remote chain. Should only be called by a plug. * @param payload_ bytes to be delivered on EVMx * @param overrides_ a bytes param to add details for execution, for eg: fees to be paid for execution */ - function _callAppGateway( + function _triggerAppGateway( address plug_, bytes memory overrides_, bytes memory payload_ @@ -134,6 +160,7 @@ contract Socket is SocketUtils { PlugConfig memory plugConfig = _plugConfigs[plug_]; // if no sibling plug is found for the given chain slug, revert + // sends the trigger to connected app gateway if (plugConfig.appGatewayId == bytes32(0)) revert PlugDisconnected(); // creates a unique ID for the message @@ -143,8 +170,12 @@ contract Socket is SocketUtils { /// @notice Fallback function that forwards all calls to Socket's callAppGateway /// @dev The calldata is passed as-is to the gateways + /// @dev if ETH sent with the call, it will revert fallback(bytes calldata) external returns (bytes memory) { + // gets the overrides from the plug bytes memory overrides = IPlug(msg.sender).overrides(); - return abi.encode(_callAppGateway(msg.sender, overrides, msg.data)); + + // return the trigger id + return abi.encode(_triggerAppGateway(msg.sender, overrides, msg.data)); } } diff --git a/contracts/protocol/socket/SocketBatcher.sol b/contracts/protocol/socket/SocketBatcher.sol index 49eb1966..5bd605a4 100644 --- a/contracts/protocol/socket/SocketBatcher.sol +++ b/contracts/protocol/socket/SocketBatcher.sol @@ -4,9 +4,9 @@ pragma solidity ^0.8.21; import "solady/auth/Ownable.sol"; import "../../interfaces/ISocket.sol"; import "../../interfaces/ISwitchboard.sol"; +import "../../interfaces/ISocketBatcher.sol"; import "../utils/RescueFundsLib.sol"; import {ExecuteParams} from "../../protocol/utils/common/Structs.sol"; -import "../../interfaces/ISocketBatcher.sol"; /** * @title SocketBatcher @@ -17,15 +17,23 @@ contract SocketBatcher is ISocketBatcher, Ownable { ISocket public immutable socket__; /** - * @notice Initializes the TransmitManager contract - * @param socket_ The address of socket contract + * @notice Initializes the SocketBatcher contract * @param owner_ The owner of the contract with GOVERNANCE_ROLE + * @param socket_ The address of socket contract */ constructor(address owner_, ISocket socket_) { socket__ = socket_; _initializeOwner(owner_); } + /** + * @notice Attests a payload and executes it + * @param executeParams_ The execution parameters + * @param digest_ The digest of the payload + * @param proof_ The proof of the payload + * @param transmitterSignature_ The signature of the transmitter + * @return The return data after execution + */ function attestAndExecute( ExecuteParams calldata executeParams_, bytes32 digest_, @@ -36,6 +44,12 @@ contract SocketBatcher is ISocketBatcher, Ownable { return socket__.execute{value: msg.value}(executeParams_, transmitterSignature_); } + /** + * @notice Rescues funds from the contract + * @param token_ The address of the token to rescue + * @param to_ The address to rescue the funds to + * @param amount_ The amount of funds to rescue + */ function rescueFunds(address token_, address to_, uint256 amount_) external onlyOwner { RescueFundsLib._rescueFunds(token_, to_, amount_); } diff --git a/contracts/protocol/socket/SocketConfig.sol b/contracts/protocol/socket/SocketConfig.sol index 4ccc6a82..7035601e 100644 --- a/contracts/protocol/socket/SocketConfig.sol +++ b/contracts/protocol/socket/SocketConfig.sol @@ -3,34 +3,46 @@ pragma solidity ^0.8.21; import "../../interfaces/ISocket.sol"; import "../../interfaces/ISwitchboard.sol"; +import {IPlug} from "../../interfaces/IPlug.sol"; + import "../utils/AccessControl.sol"; import {GOVERNANCE_ROLE, RESCUE_ROLE} from "../utils/common/AccessRoles.sol"; import {PlugConfig, SwitchboardStatus, ExecutionStatus} from "../utils/common/Structs.sol"; +import {PlugDisconnected, InvalidAppGateway, InvalidTransmitter} from "../utils/common/Errors.sol"; +import {MAX_COPY_BYTES} from "../utils/common/Constants.sol"; /** * @title SocketConfig - * @notice An abstract contract for configuring socket connections for plugs between different chains, + * @notice An abstract contract for configuring socket connections for plugs, * manages plug configs and switchboard registrations * @dev This contract is meant to be inherited by other contracts that require socket configuration functionality */ abstract contract SocketConfig is ISocket, AccessControl { - // Error triggered when a switchboard already exists + // @notice mapping of switchboard address to its status, helps socket to block invalid switchboards mapping(address => SwitchboardStatus) public isValidSwitchboard; - // plug => (appGateway, switchboard__) + // @notice mapping of plug address to its config mapping(address => PlugConfig) internal _plugConfigs; + // @notice max copy bytes for socket uint16 public maxCopyBytes = 2048; // 2KB - // Error triggered when a connection is invalid + + // @notice error triggered when a connection is invalid error InvalidConnection(); + // @notice error triggered when a switchboard is invalid error InvalidSwitchboard(); + // @notice error triggered when a switchboard already exists error SwitchboardExists(); + // @notice error triggered when a switchboard already exists or is disabled error SwitchboardExistsOrDisabled(); - // Event triggered when a new switchboard is added + // @notice event triggered when a new switchboard is added event SwitchboardAdded(address switchboard); + // @notice event triggered when a switchboard is disabled event SwitchboardDisabled(address switchboard); + // @notice function to register a switchboard + // @dev only callable by switchboards function registerSwitchboard() external { if (isValidSwitchboard[msg.sender] != SwitchboardStatus.NOT_REGISTERED) revert SwitchboardExistsOrDisabled(); @@ -39,14 +51,17 @@ abstract contract SocketConfig is ISocket, AccessControl { emit SwitchboardAdded(msg.sender); } + // @notice function to disable a switchboard + // @dev only callable by governance role function disableSwitchboard() external onlyRole(GOVERNANCE_ROLE) { isValidSwitchboard[msg.sender] = SwitchboardStatus.DISABLED; emit SwitchboardDisabled(msg.sender); } - /** - * @notice connects Plug to Socket and sets the config for given `siblingChainSlug_` - */ + // @notice function to connect a plug to a socket + // @dev only callable by plugs (msg.sender) + // @param appGatewayId_ The app gateway id + // @param switchboard_ address of switchboard on sibling chain function connect(bytes32 appGatewayId_, address switchboard_) external override { if (isValidSwitchboard[switchboard_] != SwitchboardStatus.REGISTERED) revert InvalidSwitchboard(); @@ -59,6 +74,9 @@ abstract contract SocketConfig is ISocket, AccessControl { emit PlugConnected(msg.sender, appGatewayId_, switchboard_); } + // @notice function to set the max copy bytes for socket + // @dev only callable by governance role + // @param maxCopyBytes_ max copy bytes for socket function setMaxCopyBytes(uint16 maxCopyBytes_) external onlyRole(GOVERNANCE_ROLE) { maxCopyBytes = maxCopyBytes_; } @@ -66,6 +84,8 @@ abstract contract SocketConfig is ISocket, AccessControl { /** * @notice returns the config for given `plugAddress_` * @param plugAddress_ address of plug present at current chain + * @return appGatewayId The app gateway id + * @return switchboard The switchboard address */ function getPlugConfig( address plugAddress_ diff --git a/contracts/protocol/socket/SocketUtils.sol b/contracts/protocol/socket/SocketUtils.sol index f7d8583d..805de8d4 100644 --- a/contracts/protocol/socket/SocketUtils.sol +++ b/contracts/protocol/socket/SocketUtils.sol @@ -1,14 +1,13 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity ^0.8.21; +import {ECDSA} from "solady/utils/ECDSA.sol"; import "../utils/RescueFundsLib.sol"; import "./SocketConfig.sol"; -import {ECDSA} from "solady/utils/ECDSA.sol"; /** * @title SocketUtils - * @notice A contract that is responsible for common storage for src and dest contracts, governance - * setters and inherits SocketConfig + * @notice Utility functions for socket */ abstract contract SocketUtils is SocketConfig { //////////////////////////////////////////////////////////// @@ -20,11 +19,10 @@ abstract contract SocketUtils is SocketConfig { // ChainSlug for this deployed socket instance uint32 public immutable chainSlug; + // @notice counter for trigger id uint64 public triggerCounter; - /** - * @dev keeps track of whether a payload has been executed or not using payload id - */ + // @notice mapping of payload id to execution status mapping(bytes32 => ExecutionStatus) public payloadExecuted; /* @@ -39,15 +37,6 @@ abstract contract SocketUtils is SocketConfig { _initializeOwner(owner_); } - //////////////////////////////////////////////////////// - ////////////////////// ERRORS ////////////////////////// - //////////////////////////////////////////////////////// - - /** - * @dev Error thrown when non-transmitter tries to execute - */ - error InvalidTransmitter(); - /** * @notice creates the digest for the payload * @param transmitter_ The address of the transmitter @@ -91,7 +80,6 @@ abstract contract SocketUtils is SocketConfig { address switchboard_, ExecuteParams memory executeParams_ ) internal view returns (bytes32) { - // todo: match with watcher return keccak256( abi.encode( @@ -104,6 +92,12 @@ abstract contract SocketUtils is SocketConfig { ); } + /** + * @notice recovers the signer from the signature + * @param digest_ The digest of the payload + * @param signature_ The signature of the payload + * @return signer The address of the signer + */ function _recoverSigner( bytes32 digest_, bytes memory signature_ @@ -113,9 +107,10 @@ abstract contract SocketUtils is SocketConfig { signer = ECDSA.recover(digest, signature_); } - // Packs the local plug, local chain slug, remote chain slug and nonce - // triggerCounter++ will take care of call id overflow as well - // triggerId(256) = localChainSlug(32) | appGateway_(160) | nonce(64) + /** + * @notice Encodes the trigger ID with the chain slug, socket address and nonce + * @return The trigger ID + */ function _encodeTriggerId() internal returns (bytes32) { return bytes32( diff --git a/contracts/protocol/socket/switchboard/FastSwitchboard.sol b/contracts/protocol/socket/switchboard/FastSwitchboard.sol index 308e07f6..12e0500a 100644 --- a/contracts/protocol/socket/switchboard/FastSwitchboard.sol +++ b/contracts/protocol/socket/switchboard/FastSwitchboard.sol @@ -2,24 +2,24 @@ pragma solidity ^0.8.21; import "./SwitchboardBase.sol"; +import {WATCHER_ROLE} from "../../utils/common/AccessRoles.sol"; /** * @title FastSwitchboard contract * @dev This contract implements a fast version of the SwitchboardBase contract - * that enables packet attestations + * that enables payload attestations from watchers */ contract FastSwitchboard is SwitchboardBase { - // used to track if watcher have attested a digest - // digest => isAttested + // used to track if watcher have attested a payload + // payloadId => isAttested mapping(bytes32 => bool) public isAttested; - // Error emitted when a digest is already attested by watcher. + // Error emitted when a payload is already attested by watcher. error AlreadyAttested(); // Error emitted when watcher is not valid error WatcherNotFound(); - - // Event emitted when watcher attests a digest - event Attested(bytes32 digest_, address watcher); + // Event emitted when watcher attests a payload + event Attested(bytes32 payloadId_, address watcher); /** * @dev Constructor function for the FastSwitchboard contract @@ -34,25 +34,25 @@ contract FastSwitchboard is SwitchboardBase { ) SwitchboardBase(chainSlug_, socket_, owner_) {} /** - * @dev Function to attest a packet + * @dev Function to attest a payload * @param digest_ digest of the payload to be executed * @param proof_ proof from watcher - * @notice we are attesting a digest uniquely identified with payloadId. + * @notice we are attesting a payload uniquely identified with digest. */ function attest(bytes32 digest_, bytes calldata proof_) external { - address watcher = _recoverSigner(keccak256(abi.encode(address(this), digest_)), proof_); - if (isAttested[digest_]) revert AlreadyAttested(); + + address watcher = _recoverSigner(keccak256(abi.encode(address(this), digest_)), proof_); if (!_hasRole(WATCHER_ROLE, watcher)) revert WatcherNotFound(); - isAttested[digest_] = true; + isAttested[digest_] = true; emit Attested(digest_, watcher); } /** * @inheritdoc ISwitchboard */ - function allowPacket(bytes32 digest_, bytes32) external view returns (bool) { + function allowPayload(bytes32 digest_, bytes32) external view returns (bool) { // digest has enough attestations return isAttested[digest_]; } diff --git a/contracts/protocol/socket/switchboard/SwitchboardBase.sol b/contracts/protocol/socket/switchboard/SwitchboardBase.sol index 2f5249a5..0c99eaac 100644 --- a/contracts/protocol/socket/switchboard/SwitchboardBase.sol +++ b/contracts/protocol/socket/switchboard/SwitchboardBase.sol @@ -1,13 +1,15 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity ^0.8.21; +import {ECDSA} from "solady/utils/ECDSA.sol"; import "../../../interfaces/ISwitchboard.sol"; import "../../../interfaces/ISocket.sol"; import "../../utils/AccessControl.sol"; -import {RESCUE_ROLE, WATCHER_ROLE} from "../../utils/common/AccessRoles.sol"; import "../../utils/RescueFundsLib.sol"; -import {ECDSA} from "solady/utils/ECDSA.sol"; +import {RESCUE_ROLE} from "../../utils/common/AccessRoles.sol"; +/// @title SwitchboardBase +/// @notice Base contract for switchboards, contains common and util functions for all switchboards abstract contract SwitchboardBase is ISwitchboard, AccessControl { ISocket public immutable socket__; @@ -25,6 +27,10 @@ abstract contract SwitchboardBase is ISwitchboard, AccessControl { _initializeOwner(owner_); } + /// @notice Recovers the signer from the signature + /// @param digest_ The digest of the payload + /// @param signature_ The signature of the watcher + /// @return signer The address of the signer function _recoverSigner( bytes32 digest_, bytes memory signature_ diff --git a/contracts/protocol/utils/AddressResolverUtil.sol b/contracts/protocol/utils/AddressResolverUtil.sol index f168022c..adb8b5e5 100644 --- a/contracts/protocol/utils/AddressResolverUtil.sol +++ b/contracts/protocol/utils/AddressResolverUtil.sol @@ -26,8 +26,8 @@ abstract contract AddressResolverUtil { /// @notice Error thrown when an invalid address attempts to call the Watcher precompile or delivery helper error OnlyWatcherPrecompileOrDeliveryHelper(); - /// @notice Restricts function access to the auction house contract - /// @dev Validates that msg.sender matches the registered auction house address + /// @notice Restricts function access to the delivery helper contract + /// @dev Validates that msg.sender matches the registered delivery helper address modifier onlyDeliveryHelper() { if (msg.sender != addressResolver__.deliveryHelper()) { revert OnlyPayloadDelivery(); @@ -59,9 +59,9 @@ abstract contract AddressResolverUtil { _; } - /// @notice Gets the auction house contract interface - /// @return IMiddleware interface of the registered auction house - /// @dev Resolves and returns the auction house contract for interaction + /// @notice Gets the delivery helper contract interface + /// @return IMiddleware interface of the registered delivery helper + /// @dev Resolves and returns the delivery helper contract for interaction function deliveryHelper__() public view returns (IMiddleware) { return IMiddleware(addressResolver__.deliveryHelper()); } diff --git a/contracts/protocol/utils/FeesPlugin.sol b/contracts/protocol/utils/FeesPlugin.sol index a5320f38..5a4d7514 100644 --- a/contracts/protocol/utils/FeesPlugin.sol +++ b/contracts/protocol/utils/FeesPlugin.sol @@ -8,7 +8,7 @@ import {Fees} from "../utils/common/Structs.sol"; /// @dev Provides base functionality for fee management in the system abstract contract FeesPlugin { /// @notice Storage for the current fee configuration - /// @dev Contains fee parameters like rates, limits, and recipient addresses + /// @dev Contains fee parameters like chain slug, token address, and amount Fees public fees; /// @notice Retrieves the current fee configuration diff --git a/contracts/protocol/utils/RescueFundsLib.sol b/contracts/protocol/utils/RescueFundsLib.sol index 189f2a7b..738ec796 100644 --- a/contracts/protocol/utils/RescueFundsLib.sol +++ b/contracts/protocol/utils/RescueFundsLib.sol @@ -9,7 +9,6 @@ import {ETH_ADDRESS} from "./common/Constants.sol"; * @title RescueFundsLib * @dev A library that provides a function to rescue funds from a contract. */ - library RescueFundsLib { /** * @dev Rescues funds from a contract. diff --git a/contracts/protocol/utils/common/AccessRoles.sol b/contracts/protocol/utils/common/AccessRoles.sol index 04a7d1b7..13279b25 100644 --- a/contracts/protocol/utils/common/AccessRoles.sol +++ b/contracts/protocol/utils/common/AccessRoles.sol @@ -1,13 +1,12 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity ^0.8.21; -// contains role hashes used in socket for various different operations - +// contains role hashes used in socket for various different operation // used to rescue funds bytes32 constant RESCUE_ROLE = keccak256("RESCUE_ROLE"); // used by governance bytes32 constant GOVERNANCE_ROLE = keccak256("GOVERNANCE_ROLE"); -// used by transmitters who seal and propose packets in socket +// used by transmitters who execute payloads in socket bytes32 constant TRANSMITTER_ROLE = keccak256("TRANSMITTER_ROLE"); // used by switchboard watchers who work against transmitters bytes32 constant WATCHER_ROLE = keccak256("WATCHER_ROLE"); diff --git a/contracts/protocol/utils/common/Constants.sol b/contracts/protocol/utils/common/Constants.sol index 7790eed7..c797f72a 100644 --- a/contracts/protocol/utils/common/Constants.sol +++ b/contracts/protocol/utils/common/Constants.sol @@ -3,20 +3,13 @@ pragma solidity ^0.8.21; address constant ETH_ADDRESS = address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); -address constant ZERO_ADDRESS = address(0); - bytes32 constant FORWARD_CALL = keccak256("FORWARD_CALL"); bytes32 constant DISTRIBUTE_FEE = keccak256("DISTRIBUTE_FEE"); bytes32 constant DEPLOY = keccak256("DEPLOY"); -bytes32 constant CONFIGURE = keccak256("CONFIGURE"); -bytes32 constant CONNECT = keccak256("CONNECT"); bytes32 constant QUERY = keccak256("QUERY"); bytes32 constant FINALIZE = keccak256("FINALIZE"); bytes32 constant SCHEDULE = keccak256("SCHEDULE"); bytes32 constant FAST = keccak256("FAST"); - -uint256 constant DEPLOY_GAS_LIMIT = 5_000_000; -uint256 constant CONFIGURE_GAS_LIMIT = 1_000_000; -uint256 constant PAYLOAD_SIZE_LIMIT = 24_500; uint256 constant REQUEST_PAYLOAD_COUNT_LIMIT = 10; +uint256 constant PAYLOAD_SIZE_LIMIT = 24_500; uint16 constant MAX_COPY_BYTES = 2048; // 2KB diff --git a/contracts/protocol/utils/common/Errors.sol b/contracts/protocol/utils/common/Errors.sol index da0a0764..03078541 100644 --- a/contracts/protocol/utils/common/Errors.sol +++ b/contracts/protocol/utils/common/Errors.sol @@ -1,17 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.21; -error NotAuthorized(); -error NotBridge(); error NotSocket(); -error ConnectorUnavailable(); -error InvalidTokenContract(); -error ZeroAddressReceiver(); error ZeroAddress(); -error ZeroAmount(); -error InsufficientFunds(); -error InvalidSigner(); -error InvalidFunction(); error TimeoutDelayTooLarge(); error TimeoutAlreadyResolved(); error ResolvingTimeoutTooEarly(); @@ -22,10 +13,10 @@ error CallFailed(); error PlugDisconnected(); error InvalidAppGateway(); error AppGatewayAlreadyCalled(); +error InvalidInboxCaller(); error InvalidCallerTriggered(); error PromisesNotResolved(); error InvalidPromise(); -error InvalidIndex(); error InvalidTransmitter(); error FeesNotSet(); error InvalidTokenAddress(); @@ -40,3 +31,5 @@ error BidExceedsMaxFees(); /// @notice Error thrown if a lower bid already exists error LowerBidAlreadyExists(); error AsyncModifierNotUsed(); +error InvalidIndex(); +error RequestAlreadyExecuted(); diff --git a/contracts/protocol/utils/common/Structs.sol b/contracts/protocol/utils/common/Structs.sol index a3ffce79..b2ebe862 100644 --- a/contracts/protocol/utils/common/Structs.sol +++ b/contracts/protocol/utils/common/Structs.sol @@ -64,6 +64,7 @@ struct UpdateLimitParams { uint256 maxLimit; uint256 ratePerSecond; } + struct AppGatewayConfig { address plug; bytes32 appGatewayId; @@ -94,13 +95,7 @@ struct TimeoutRequest { bool isResolved; bytes payload; } -struct QueryResults { - address target; - uint256 queryCounter; - bytes functionSelector; - bytes returnData; - bytes callback; -} + struct ResolvedPromises { bytes32 payloadId; bytes returnData; @@ -208,9 +203,11 @@ struct PayloadParams { struct RequestParams { bool isRequestCancelled; uint40 currentBatch; + // updated while processing request uint256 currentBatchPayloadsLeft; uint256 payloadsRemaining; address middleware; + // updated after auction address transmitter; PayloadParams[] payloadParamsArray; } @@ -240,14 +237,6 @@ struct ExecuteParams { address switchboard; } -struct PayloadIdParams { - uint40 requestCount; - uint40 batchCount; - uint40 payloadCount; - address switchboard; - uint32 chainSlug; -} - /// @notice Struct containing fee amounts and status struct TokenBalance { uint256 deposited; // Amount deposited diff --git a/contracts/protocol/watcherPrecompile/WatcherPrecompileConfig.sol b/contracts/protocol/watcherPrecompile/WatcherPrecompileConfig.sol index e9b30baa..d8ad1797 100644 --- a/contracts/protocol/watcherPrecompile/WatcherPrecompileConfig.sol +++ b/contracts/protocol/watcherPrecompile/WatcherPrecompileConfig.sol @@ -1,56 +1,64 @@ // SPDX-License-Identifier: Unlicense pragma solidity ^0.8.21; -import "./WatcherPrecompileLimits.sol"; -import {ECDSA} from "solady/utils/ECDSA.sol"; import "solady/utils/Initializable.sol"; +import {ECDSA} from "solady/utils/ECDSA.sol"; +import {Ownable} from "solady/auth/Ownable.sol"; import "../../interfaces/IWatcherPrecompileConfig.sol"; -import "./WatcherPrecompileUtils.sol"; +import {AddressResolverUtil} from "../utils/AddressResolverUtil.sol"; +import {InvalidWatcherSignature, NonceUsed} from "../utils/common/Errors.sol"; +import "./core/WatcherPrecompileUtils.sol"; + /// @title WatcherPrecompileConfig /// @notice Configuration contract for the Watcher Precompile system /// @dev Handles the mapping between networks, plugs, and app gateways for payload execution contract WatcherPrecompileConfig is IWatcherPrecompileConfig, Initializable, - AccessControl, + Ownable, AddressResolverUtil, WatcherPrecompileUtils { - // slot 52: evmxSlug + // slots 0-50 (51) reserved for addr resolver util + + // slots [51-100]: gap for future storage variables + uint256[50] _gap_before; + + // slot 101: evmxSlug /// @notice The chain slug of the watcher precompile uint32 public evmxSlug; - // slot 55: _plugConfigs + // slot 102: _plugConfigs /// @notice Maps network and plug to their configuration /// @dev chainSlug => plug => PlugConfig mapping(uint32 => mapping(address => PlugConfig)) internal _plugConfigs; - // slot 56: switchboards + // slot 103: switchboards /// @notice Maps chain slug to their associated switchboard /// @dev chainSlug => sb type => switchboard address mapping(uint32 => mapping(bytes32 => address)) public switchboards; - // slot 57: sockets + // slot 104: sockets /// @notice Maps chain slug to their associated socket /// @dev chainSlug => socket address mapping(uint32 => address) public sockets; - // slot 58: contractFactoryPlug + // slot 105: contractFactoryPlug /// @notice Maps chain slug to their associated contract factory plug /// @dev chainSlug => contract factory plug address mapping(uint32 => address) public contractFactoryPlug; - // slot 59: feesPlug + // slot 106: feesPlug /// @notice Maps chain slug to their associated fees plug /// @dev chainSlug => fees plug address mapping(uint32 => address) public feesPlug; - // slot 60: isNonceUsed + // slot 107: isNonceUsed /// @notice Maps nonce to whether it has been used /// @dev signatureNonce => isValid mapping(uint256 => bool) public isNonceUsed; - // slot 61: isValidPlug + // slot 108: isValidPlug // appGateway => chainSlug => plug => isValid mapping(address => mapping(uint32 => mapping(address => bool))) public isValidPlug; @@ -80,8 +88,6 @@ contract WatcherPrecompileConfig is error InvalidGateway(); error InvalidSwitchboard(); - error NonceUsed(); - error InvalidWatcherSignature(); /// @notice Initial initialization (version 1) function initialize( @@ -95,12 +101,10 @@ contract WatcherPrecompileConfig is evmxSlug = evmxSlug_; } - /// @notice Emitted when a plug is set as valid for an app gateway - /// @notice Configures app gateways with their respective plugs and switchboards - /// @param configs_ Array of configurations containing app gateway, network, plug, and switchboard details - /// @dev Only callable by the contract owner + /// @dev Only callable by the watcher /// @dev This helps in verifying that plugs are called by respective app gateways + /// @param configs_ Array of configurations containing app gateway, network, plug, and switchboard details function setAppGateways( AppGatewayConfig[] calldata configs_, uint256 signatureNonce_, @@ -123,7 +127,7 @@ contract WatcherPrecompileConfig is } } - /// @notice Sets the switchboard for a network + /// @notice Sets the socket, contract factory plug, and fees plug for a network /// @param chainSlug_ The identifier of the network function setOnChainContracts( uint32 chainSlug_, @@ -140,6 +144,7 @@ contract WatcherPrecompileConfig is /// @notice Sets the switchboard for a network /// @param chainSlug_ The identifier of the network + /// @param sbType_ The type of switchboard, hash of a string /// @param switchboard_ The address of the switchboard function setSwitchboard( uint32 chainSlug_, @@ -150,12 +155,18 @@ contract WatcherPrecompileConfig is emit SwitchboardSet(chainSlug_, sbType_, switchboard_); } - // @dev app gateway can set the valid plugs for each chain slug + /// @notice Sets the valid plugs for an app gateway + /// @dev Only callable by the app gateway + /// @dev This helps in verifying that app gateways are called by respective plugs + /// @param chainSlug_ The identifier of the network + /// @param plug_ The address of the plug + /// @param isValid_ Whether the plug is valid function setIsValidPlug(uint32 chainSlug_, address plug_, bool isValid_) external { isValidPlug[msg.sender][chainSlug_][plug_] = isValid_; } /// @notice Retrieves the configuration for a specific plug on a network + /// @dev Returns zero addresses if configuration doesn't exist /// @param chainSlug_ The identifier of the network /// @param plug_ The address of the plug /// @return The app gateway id and switchboard address for the plug @@ -170,15 +181,24 @@ contract WatcherPrecompileConfig is ); } + /// @notice Verifies the connections between the target, app gateway, and switchboard + /// @dev Only callable by the watcher + /// @param chainSlug_ The identifier of the network + /// @param target_ The address of the target + /// @param appGateway_ The address of the app gateway + /// @param switchboard_ The address of the switchboard function verifyConnections( uint32 chainSlug_, address target_, address appGateway_, - address switchboard_ + address switchboard_, + address middleware_ ) external view { - // todo: revisit this // if target is contractFactoryPlug, return - if (target_ == contractFactoryPlug[chainSlug_]) return; + // as connection is with middleware delivery helper and not app gateway + if ( + middleware_ == address(deliveryHelper__()) && target_ == contractFactoryPlug[chainSlug_] + ) return; (bytes32 appGatewayId, address switchboard) = getPlugConfigs(chainSlug_, target_); if (appGatewayId != _encodeAppGatewayId(appGateway_)) revert InvalidGateway(); diff --git a/contracts/protocol/watcherPrecompile/WatcherPrecompileLimits.sol b/contracts/protocol/watcherPrecompile/WatcherPrecompileLimits.sol index 42f1d2b4..4fdc00b9 100644 --- a/contracts/protocol/watcherPrecompile/WatcherPrecompileLimits.sol +++ b/contracts/protocol/watcherPrecompile/WatcherPrecompileLimits.sol @@ -1,47 +1,51 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.21; -import {AccessControl} from "../utils/AccessControl.sol"; +import "solady/utils/Initializable.sol"; +import {Ownable} from "solady/auth/Ownable.sol"; import {Gauge} from "../utils/Gauge.sol"; import {AddressResolverUtil} from "../utils/AddressResolverUtil.sol"; -import {WATCHER_ROLE} from "../utils/common/AccessRoles.sol"; import "../../interfaces/IWatcherPrecompileLimits.sol"; -import "solady/utils/Initializable.sol"; import {SCHEDULE, QUERY, FINALIZE} from "../utils/common/Constants.sol"; +/// @title WatcherPrecompileLimits +/// @notice Contract for managing watcher precompile limits contract WatcherPrecompileLimits is IWatcherPrecompileLimits, Initializable, - AccessControl, + Ownable, Gauge, AddressResolverUtil { - // Slots from parent contracts: - // slot 0-118: watcher precompile storage - // 0 slots for initializable and ownable - // slots 119-169: access control (gap + 1) - // slots 170-219: gauge (gap) - // slots 220-270: address resolver util (gap + 1) - // slots 271-320: gap for future storage variables - uint256[50] _gap_watcher_precompile_limits; + // slots 0-49 (50) reserved for gauge + // slots 50-100 (51) reserved for addr resolver util + + // slots [101-150]: gap for future storage variables + uint256[50] _gap_before; + // slot 151: limitDecimals /// @notice Number of decimals used in limit calculations - uint256 public constant LIMIT_DECIMALS = 18; - // slot 50: defaultLimit + uint256 public limitDecimals; + + // slot 152: defaultLimit /// @notice Default limit value for any app gateway uint256 public defaultLimit; - // slot 51: defaultRatePerSecond + + // slot 153: defaultRatePerSecond /// @notice Rate at which limit replenishes per second uint256 public defaultRatePerSecond; - // slot 53: _limitParams + // slot 154: _limitParams // appGateway => limitType => receivingLimitParams mapping(address => mapping(bytes32 => LimitParams)) internal _limitParams; - // slot 54: _activeAppGateways + // slot 155: _activeAppGateways // Mapping to track active app gateways mapping(address => bool) internal _activeAppGateways; + /// @notice Emitted when the default limit and rate per second are set + event DefaultLimitAndRatePerSecondSet(uint256 defaultLimit, uint256 defaultRatePerSecond); + /// @notice Initial initialization (version 1) function initialize( address owner_, @@ -50,9 +54,10 @@ contract WatcherPrecompileLimits is ) public reinitializer(1) { _setAddressResolver(addressResolver_); _initializeOwner(owner_); + limitDecimals = 18; // limit per day - defaultLimit = defaultLimit_ * 10 ** LIMIT_DECIMALS; + defaultLimit = defaultLimit_ * 10 ** limitDecimals; // limit per second defaultRatePerSecond = defaultLimit / (24 * 60 * 60); } @@ -120,7 +125,7 @@ contract WatcherPrecompileLimits is ) external override onlyWatcherPrecompile { LimitParams storage limitParams = _limitParams[appGateway_][limitType_]; - // Initialize limit if not active + // Initialize limit if not active, give default limit and rate per second if (!_activeAppGateways[appGateway_]) { LimitParams memory limitParam = LimitParams({ maxLimit: defaultLimit, @@ -138,22 +143,17 @@ contract WatcherPrecompileLimits is } // Update the limit - _consumeFullLimit(consumeLimit_ * 10 ** LIMIT_DECIMALS, limitParams); + _consumeFullLimit(consumeLimit_ * 10 ** limitDecimals, limitParams); } /** * @notice Set the default limit value * @param defaultLimit_ The new default limit value */ - function setDefaultLimit(uint256 defaultLimit_) external onlyOwner { + function setDefaultLimitAndRatePerSecond(uint256 defaultLimit_) external onlyOwner { defaultLimit = defaultLimit_; - } + defaultRatePerSecond = defaultLimit / (24 * 60 * 60); - /** - * @notice Set the rate at which limit replenishes - * @param defaultRatePerSecond_ The new rate per second - */ - function setDefaultRatePerSecond(uint256 defaultRatePerSecond_) external onlyOwner { - defaultRatePerSecond = defaultRatePerSecond_; + emit DefaultLimitAndRatePerSecondSet(defaultLimit, defaultRatePerSecond); } } diff --git a/contracts/protocol/watcherPrecompile/WatcherPrecompileStorage.sol b/contracts/protocol/watcherPrecompile/WatcherPrecompileStorage.sol deleted file mode 100644 index 1dff2b6f..00000000 --- a/contracts/protocol/watcherPrecompile/WatcherPrecompileStorage.sol +++ /dev/null @@ -1,82 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import "../../interfaces/IWatcherPrecompile.sol"; -import {IAppGateway} from "../../interfaces/IAppGateway.sol"; -import {IFeesManager} from "../../interfaces/IFeesManager.sol"; -import {IPromise} from "../../interfaces/IPromise.sol"; -import "./DumpDecoder.sol"; - -import {IMiddleware} from "../../interfaces/IMiddleware.sol"; -import {QUERY, FINALIZE, SCHEDULE} from "../utils/common/Constants.sol"; -import {TimeoutDelayTooLarge, TimeoutAlreadyResolved, InvalidCallerTriggered, ResolvingTimeoutTooEarly, CallFailed, AppGatewayAlreadyCalled, InvalidWatcherSignature, NonceUsed} from "../utils/common/Errors.sol"; -import {ResolvedPromises, AppGatewayConfig, LimitParams, WriteFinality, UpdateLimitParams, PlugConfig, DigestParams, TimeoutRequest, TriggerParams, QueuePayloadParams, PayloadParams, RequestParams} from "../utils/common/Structs.sol"; - -abstract contract WatcherPrecompileStorage is IWatcherPrecompile { - // slots [0-49]: gap for future storage variables - uint256[50] _gap_before; - - // slot 50 - /// @notice The chain slug of the watcher precompile - uint32 public evmxSlug; - - /// @notice Counter for tracking payload requests - uint40 public payloadCounter; - - /// @notice Counter for tracking timeout requests - uint40 public timeoutCounter; - - uint40 public nextRequestCount; - - uint40 public nextBatchCount; - - // slot 51: maxTimeoutDelayInSeconds - uint256 public maxTimeoutDelayInSeconds; - - // slot 52: expiryTime - /// @notice The expiry time for the payload - uint256 public expiryTime; - - // slot 53: appGatewayCaller - /// @notice The address of the app gateway caller from a chain - address public appGatewayCaller; - - // slot 54: isNonceUsed - /// @notice Maps nonce to whether it has been used - /// @dev signatureNonce => isValid - mapping(uint256 => bool) public isNonceUsed; - - // slot 55: timeoutRequests - /// @notice Mapping to store timeout requests - /// @dev timeoutId => TimeoutRequest struct - mapping(bytes32 => TimeoutRequest) public timeoutRequests; - - // slot 56: watcherProofs - /// @notice Mapping to store watcher proofs - /// @dev payloadId => proof bytes - mapping(bytes32 => bytes) public watcherProofs; - - // slot 57: appGatewayCalled - /// @notice Mapping to store if appGateway has been called with trigger from on-chain plug - /// @dev triggerId => bool - mapping(bytes32 => bool) public appGatewayCalled; - - // slot 58: requestParams - mapping(uint40 => RequestParams) public requestParams; - // slot 59: batchPayloadIds - mapping(uint40 => bytes32[]) public batchPayloadIds; - // slot 60: requestBatchIds - mapping(uint40 => uint40[]) public requestBatchIds; - // slot 61: payloads - mapping(bytes32 => PayloadParams) public payloads; - // slot 62: isPromiseExecuted - mapping(bytes32 => bool) public isPromiseExecuted; - - // slot 63: watcherPrecompileLimits__ - IWatcherPrecompileLimits public watcherPrecompileLimits__; - // slot 64: watcherPrecompileConfig__ - IWatcherPrecompileConfig public watcherPrecompileConfig__; - - // slots [65-114]: gap for future storage variables - uint256[50] _gap_after; -} diff --git a/contracts/protocol/watcherPrecompile/RequestHandler.sol b/contracts/protocol/watcherPrecompile/core/RequestHandler.sol similarity index 57% rename from contracts/protocol/watcherPrecompile/RequestHandler.sol rename to contracts/protocol/watcherPrecompile/core/RequestHandler.sol index da161179..4337f634 100644 --- a/contracts/protocol/watcherPrecompile/RequestHandler.sol +++ b/contracts/protocol/watcherPrecompile/core/RequestHandler.sol @@ -3,9 +3,21 @@ pragma solidity ^0.8.21; import "./WatcherPrecompileCore.sol"; +/// @title RequestHandler +/// @notice Contract that handles request submission and processing +/// @dev This contract extends WatcherPrecompileCore to provide request handling functionality +/// @dev It manages the submission of payload requests and their processing abstract contract RequestHandler is WatcherPrecompileCore { using DumpDecoder for bytes32; + // slots [266-315] reserved for gap + uint256[50] _request_handler_gap; + + /// @notice Submits a batch of payload requests from middleware + /// @param payloadSubmitParams Array of payload submit parameters + /// @return requestCount The unique identifier for the submitted request + /// @dev This function processes a batch of payload requests and assigns them to batches + /// @dev It also consumes limits for the app gateway based on the number of reads and writes function submitRequest( PayloadSubmitParams[] calldata payloadSubmitParams ) public returns (uint40 requestCount) { @@ -22,11 +34,12 @@ abstract contract RequestHandler is WatcherPrecompileCore { for (uint256 i = 0; i < payloadSubmitParams.length; i++) { PayloadSubmitParams memory p = payloadSubmitParams[i]; - // Count reads and writes + // Count reads and writes for checking limits if (p.callType == CallType.READ) { readCount++; } else writeCount++; + // checking level number for batching if (i > 0) { if (p.levelNumber != lastP.levelNumber && p.levelNumber != lastP.levelNumber + 1) revert InvalidLevelNumber(); @@ -37,7 +50,13 @@ abstract contract RequestHandler is WatcherPrecompileCore { } uint40 localPayloadCount = payloadCounter++; - bytes32 payloadId = _createPayloadId(p, requestCount, batchCount, localPayloadCount); + bytes32 payloadId = _createPayloadId( + requestCount, + batchCount, + localPayloadCount, + p.switchboard, + p.chainSlug + ); batchPayloadIds[batchCount].push(payloadId); bytes32 dump; @@ -57,28 +76,25 @@ abstract contract RequestHandler is WatcherPrecompileCore { ? addressResolver__.deliveryHelper() : p.appGateway; payloads[payloadId].payloadId = payloadId; - payloads[payloadId].prevDigestsHash = bytes32(0); payloads[payloadId].gasLimit = p.gasLimit; payloads[payloadId].value = p.value; payloads[payloadId].readAt = p.readAt; - payloads[payloadId].deadline = 0; payloads[payloadId].payload = p.payload; - payloads[payloadId].finalizedTransmitter = address(0); requestParams[requestCount].payloadParamsArray.push(payloads[payloadId]); lastP = p; } + // Push the final batch ID to the request's batch list and increment the counter + // This is needed because the last batch in the loop above doesn't get added since there's no next level to trigger it requestBatchIds[requestCount].push(nextBatchCount++); watcherPrecompileLimits__.consumeLimit(appGateway, QUERY, readCount); watcherPrecompileLimits__.consumeLimit(appGateway, FINALIZE, writeCount); - requestParams[requestCount].isRequestCancelled = false; + requestParams[requestCount].currentBatch = currentBatch; - requestParams[requestCount].currentBatchPayloadsLeft = 0; requestParams[requestCount].payloadsRemaining = payloadSubmitParams.length; requestParams[requestCount].middleware = msg.sender; - requestParams[requestCount].transmitter = address(0); emit RequestSubmitted( msg.sender, @@ -87,22 +103,35 @@ abstract contract RequestHandler is WatcherPrecompileCore { ); } + /// @notice Checks if all app gateways in the payload submit parameters are valid and same + /// @dev It also handles special cases for the delivery helper + /// @param payloadSubmitParams Array of payload submit parameters + /// @return appGateway The core app gateway address function _checkAppGateways( PayloadSubmitParams[] calldata payloadSubmitParams ) internal view returns (address appGateway) { bool isDeliveryHelper = msg.sender == addressResolver__.deliveryHelper(); + + // Get first app gateway and use it as reference address coreAppGateway = isDeliveryHelper - ? payloadSubmitParams[0].appGateway - : _getCoreAppGateway(payloadSubmitParams[0].appGateway); - for (uint256 i = 0; i < payloadSubmitParams.length; i++) { - address callerAppGateway = isDeliveryHelper - ? payloadSubmitParams[i].appGateway - : msg.sender; - appGateway = _getCoreAppGateway(callerAppGateway); + ? _getCoreAppGateway(payloadSubmitParams[0].appGateway) + : _getCoreAppGateway(msg.sender); + + // Skip first element since we already checked it + for (uint256 i = 1; i < payloadSubmitParams.length; i++) { + appGateway = isDeliveryHelper + ? _getCoreAppGateway(payloadSubmitParams[i].appGateway) + : coreAppGateway; + if (appGateway != coreAppGateway) revert InvalidGateway(); } } + /// @notice Starts processing a request with a specified transmitter + /// @param requestCount The request count to start processing + /// @param transmitter The address of the transmitter + /// @dev This function initiates the processing of a request by a transmitter + /// @dev It verifies that the caller is the middleware and that the request hasn't been started yet function startProcessingRequest(uint40 requestCount, address transmitter) public { RequestParams storage r = requestParams[requestCount]; if (r.middleware != msg.sender) revert InvalidCaller(); @@ -113,24 +142,23 @@ abstract contract RequestHandler is WatcherPrecompileCore { r.transmitter = transmitter; r.currentBatch = batchCount; - uint256 totalPayloadsLeft = _processBatch(requestCount, batchCount); - // todo: for retry cases - r.currentBatchPayloadsLeft = totalPayloadsLeft; + _processBatch(requestCount, batchCount); } - function _processBatch( - uint40 requestCount_, - uint40 batchCount_ - ) internal returns (uint256 totalPayloadsLeft) { - RequestParams memory r = requestParams[requestCount_]; + /// @notice Processes a batch of payloads for a request + /// @param requestCount_ The request count to process + /// @param batchCount_ The batch count to process + /// @dev This function processes all payloads in a batch, either finalizing them or querying them + /// @dev It skips payloads that have already been executed + function _processBatch(uint40 requestCount_, uint40 batchCount_) internal { + RequestParams storage r = requestParams[requestCount_]; PayloadParams[] memory payloadParamsArray = _getBatch(batchCount_); - if (r.isRequestCancelled) revert RequestCancelled(); + uint256 totalPayloads = 0; for (uint40 i = 0; i < payloadParamsArray.length; i++) { - bool executed = isPromiseExecuted[payloadParamsArray[i].payloadId]; - if (executed) continue; - totalPayloadsLeft++; + if (isPromiseExecuted[payloadParamsArray[i].payloadId]) continue; + totalPayloads++; if (payloadParamsArray[i].dump.getCallType() != CallType.READ) { _finalize(payloadParamsArray[i], r.transmitter); @@ -138,8 +166,13 @@ abstract contract RequestHandler is WatcherPrecompileCore { _query(payloadParamsArray[i]); } } + + r.currentBatchPayloadsLeft = totalPayloads; } + /// @notice Gets the current request count + /// @return The current request count + /// @dev This function returns the next request count, which is the current request count function getCurrentRequestCount() external view returns (uint40) { return nextRequestCount; } diff --git a/contracts/protocol/watcherPrecompile/WatcherPrecompile.sol b/contracts/protocol/watcherPrecompile/core/WatcherPrecompile.sol similarity index 57% rename from contracts/protocol/watcherPrecompile/WatcherPrecompile.sol rename to contracts/protocol/watcherPrecompile/core/WatcherPrecompile.sol index 11def831..95bb790a 100644 --- a/contracts/protocol/watcherPrecompile/WatcherPrecompile.sol +++ b/contracts/protocol/watcherPrecompile/core/WatcherPrecompile.sol @@ -3,18 +3,28 @@ pragma solidity ^0.8.21; import "./RequestHandler.sol"; import {LibCall} from "solady/utils/LibCall.sol"; -import {MAX_COPY_BYTES} from "../utils/common/Constants.sol"; /// @title WatcherPrecompile -/// @notice Contract that handles payload verification, execution and app configurations +/// @notice Contract that handles request submission, iteration and execution +/// @dev This contract extends RequestHandler to provide the main functionality for the WatcherPrecompile system +/// @dev It handles timeout requests, finalization, queries, and promise resolution contract WatcherPrecompile is RequestHandler { using DumpDecoder for bytes32; using LibCall for address; + + /// @notice Constructor that disables initializers for the implementation constructor() { - _disableInitializers(); // disable for implementation + _disableInitializers(); } - /// @notice Initial initialization (version 1) + /// @notice Initializes the contract with the required parameters + /// @param owner_ The address of the owner + /// @param addressResolver_ The address of the address resolver + /// @param expiryTime_ The expiry time for payload execution + /// @param evmxSlug_ The EVM chain slug + /// @param watcherPrecompileLimits_ The address of the watcher precompile limits contract + /// @param watcherPrecompileConfig_ The address of the watcher precompile config contract + /// @dev This function initializes the contract with the required parameters and sets up the initial state function initialize( address owner_, address addressResolver_, @@ -37,18 +47,24 @@ contract WatcherPrecompile is RequestHandler { // ================== Timeout functions ================== /// @notice Sets a timeout for a payload execution on app gateway - /// @param payload_ The payload data - /// @param delayInSeconds_ The delay in seconds + /// @dev This function creates a timeout request that will be executed after the specified `delayInSeconds_` + /// @dev request is executed on msg.sender + /// @dev msg sender needs SCHEDULE precompile limit + /// @param delayInSeconds_ The delay in seconds before the timeout executes + /// @param payload_ The payload data to be executed after the timeout + /// @return The unique identifier for the timeout request function setTimeout( uint256 delayInSeconds_, bytes calldata payload_ ) external returns (bytes32) { - return _setTimeout(payload_, delayInSeconds_); + return _setTimeout(delayInSeconds_, payload_); } /// @notice Ends the timeouts and calls the target address with the callback payload /// @param timeoutId_ The unique identifier for the timeout - /// @dev Only callable by the contract owner + /// @param signatureNonce_ The nonce used in the watcher's signature + /// @param signature_ The watcher's signature + /// @dev It verifies if the signature is valid and the timeout hasn't been resolved yet function resolveTimeout( bytes32 timeoutId_, uint256 signatureNonce_, @@ -65,7 +81,7 @@ contract WatcherPrecompile is RequestHandler { if (timeoutRequest_.isResolved) revert TimeoutAlreadyResolved(); if (block.timestamp < timeoutRequest_.executeAt) revert ResolvingTimeoutTooEarly(); - (bool success, , ) = timeoutRequest_.target.tryCall( + (bool success, , bytes memory returnData) = timeoutRequest_.target.tryCall( 0, gasleft(), 0, // setting max_copy_bytes to 0 as not using returnData right now @@ -80,7 +96,8 @@ contract WatcherPrecompile is RequestHandler { timeoutId_, timeoutRequest_.target, timeoutRequest_.payload, - block.timestamp + block.timestamp, + returnData ); } @@ -89,16 +106,20 @@ contract WatcherPrecompile is RequestHandler { /// @notice Finalizes a payload request, requests the watcher to release the proofs to execute on chain /// @param params_ The payload parameters /// @param transmitter_ The address of the transmitter + /// @return The digest hash of the finalized payload + /// @dev This function finalizes a payload request and requests the watcher to release the proofs function finalize( PayloadParams memory params_, address transmitter_ - ) external returns (bytes32 digest) { - digest = _finalize(params_, transmitter_); + ) external returns (bytes32) { + return _finalize(params_, transmitter_); } // ================== Query functions ================== + /// @notice Creates a new query request /// @param params_ The payload parameters + /// @dev This function creates a new query request function query(PayloadParams memory params_) external { _query(params_); } @@ -108,7 +129,8 @@ contract WatcherPrecompile is RequestHandler { /// @param proof_ The watcher's proof /// @param signatureNonce_ The nonce of the signature /// @param signature_ The signature of the watcher - /// @dev Only callable by the contract owner + /// @dev This function marks a request as finalized with a proof + /// @dev It verifies that the signature is valid /// @dev Watcher signs on following digest for validation on switchboard: /// @dev keccak256(abi.encode(switchboard, digest)) function finalized( @@ -127,17 +149,26 @@ contract WatcherPrecompile is RequestHandler { emit Finalized(payloadId_, proof_); } + /// @notice Updates the transmitter for a request + /// @param requestCount The request count to update + /// @param transmitter The new transmitter address + /// @dev This function updates the transmitter for a request + /// @dev It verifies that the caller is the middleware and that the request hasn't been started yet function updateTransmitter(uint40 requestCount, address transmitter) public { RequestParams storage r = requestParams[requestCount]; if (r.isRequestCancelled) revert RequestCancelled(); + if (r.payloadsRemaining == 0) revert RequestAlreadyExecuted(); if (r.middleware != msg.sender) revert InvalidCaller(); - if (r.transmitter != address(0)) revert AlreadyStarted(); - + if (r.transmitter != address(0)) revert RequestNotProcessing(); r.transmitter = transmitter; - /// todo: recheck limits + _processBatch(requestCount, r.currentBatch); } + /// @notice Cancels a request + /// @param requestCount The request count to cancel + /// @dev This function cancels a request + /// @dev It verifies that the caller is the middleware and that the request hasn't been cancelled yet function cancelRequest(uint40 requestCount) external { RequestParams storage r = requestParams[requestCount]; if (r.isRequestCancelled) revert RequestAlreadyCancelled(); @@ -148,7 +179,11 @@ contract WatcherPrecompile is RequestHandler { /// @notice Resolves multiple promises with their return data /// @param resolvedPromises_ Array of resolved promises and their return data - /// @dev Only callable by the contract owner + /// @param signatureNonce_ The nonce of the signature + /// @param signature_ The signature of the watcher + /// @dev This function resolves multiple promises with their return data + /// @dev It verifies that the signature is valid + /// @dev It also processes the next batch if the current batch is complete function resolvePromises( ResolvedPromises[] calldata resolvedPromises_, uint256 signatureNonce_, @@ -164,38 +199,37 @@ contract WatcherPrecompile is RequestHandler { // Get the array of promise addresses for this payload PayloadParams memory payloadParams = payloads[resolvedPromises_[i].payloadId]; address asyncPromise = payloadParams.asyncPromise; - if (asyncPromise == address(0)) continue; - // Resolve each promise with its corresponding return data - bool success = IPromise(asyncPromise).markResolved( - payloadParams.dump.getRequestCount(), - resolvedPromises_[i].payloadId, - resolvedPromises_[i].returnData - ); + if (asyncPromise != address(0)) { + // Resolve each promise with its corresponding return data + bool success = IPromise(asyncPromise).markResolved( + payloadParams.dump.getRequestCount(), + resolvedPromises_[i].payloadId, + resolvedPromises_[i].returnData + ); - isPromiseExecuted[resolvedPromises_[i].payloadId] = true; - if (!success) { - emit PromiseNotResolved(resolvedPromises_[i].payloadId, asyncPromise); - continue; + if (!success) { + emit PromiseNotResolved(resolvedPromises_[i].payloadId, asyncPromise); + continue; + } } + isPromiseExecuted[resolvedPromises_[i].payloadId] = true; + RequestParams storage requestParams_ = requestParams[ payloadParams.dump.getRequestCount() ]; - requestParams_.currentBatchPayloadsLeft--; requestParams_.payloadsRemaining--; + // if all payloads of a batch are executed, process the next batch if ( requestParams_.currentBatchPayloadsLeft == 0 && requestParams_.payloadsRemaining > 0 ) { - uint256 totalPayloadsLeft = _processBatch( - payloadParams.dump.getRequestCount(), - ++requestParams_.currentBatch - ); - requestParams_.currentBatchPayloadsLeft = totalPayloadsLeft; + _processBatch(payloadParams.dump.getRequestCount(), ++requestParams_.currentBatch); } + // if all payloads of a request are executed, finish the request if (requestParams_.payloadsRemaining == 0) { IMiddleware(requestParams_.middleware).finishRequest( payloadParams.dump.getRequestCount() @@ -205,7 +239,14 @@ contract WatcherPrecompile is RequestHandler { } } - // wait till expiry time to assign fees + /// @notice Marks a request as reverting + /// @param isRevertingOnchain_ Whether the request is reverting onchain + /// @param payloadId_ The unique identifier of the payload + /// @param signatureNonce_ The nonce of the signature + /// @param signature_ The signature of the watcher + /// @dev Only valid watcher can mark a request as reverting + /// @dev This function marks a request as reverting if callback or payload is reverting on chain + /// @dev Request is marked cancelled for both cases. function markRevert( bool isRevertingOnchain_, bytes32 payloadId_, @@ -218,7 +259,7 @@ contract WatcherPrecompile is RequestHandler { signature_ ); - PayloadParams storage payloadParams = payloads[payloadId_]; + PayloadParams memory payloadParams = payloads[payloadId_]; if (payloadParams.deadline > block.timestamp) revert DeadlineNotPassedForOnChainRevert(); RequestParams storage currentRequestParams = requestParams[ @@ -226,7 +267,7 @@ contract WatcherPrecompile is RequestHandler { ]; currentRequestParams.isRequestCancelled = true; - if (isRevertingOnchain_) + if (isRevertingOnchain_ && payloadParams.asyncPromise != address(0)) IPromise(payloadParams.asyncPromise).markOnchainRevert( payloadParams.dump.getRequestCount(), payloadId_ @@ -239,12 +280,14 @@ contract WatcherPrecompile is RequestHandler { emit MarkedRevert(payloadId_, isRevertingOnchain_); } - function setMaxTimeoutDelayInSeconds(uint256 maxTimeoutDelayInSeconds_) external onlyOwner { - maxTimeoutDelayInSeconds = maxTimeoutDelayInSeconds_; - } - - // ================== On-Chain Trigger ================== + // ================== On-Chain Inbox ================== + /// @notice Calls app gateways with the specified parameters + /// @param params_ Array of call from chain parameters + /// @param signatureNonce_ The nonce of the signature + /// @param signature_ The signature of the watcher + /// @dev This function calls app gateways with the specified parameters + /// @dev It verifies that the signature is valid and that the app gateway hasn't been called yet function callAppGateways( TriggerParams[] calldata params_, uint256 signatureNonce_, @@ -289,10 +332,45 @@ contract WatcherPrecompile is RequestHandler { // ================== Helper functions ================== + /// @notice Sets the maximum timeout delay in seconds + /// @param maxTimeoutDelayInSeconds_ The maximum timeout delay in seconds + /// @dev This function sets the maximum timeout delay in seconds + /// @dev Only callable by the contract owner + function setMaxTimeoutDelayInSeconds(uint256 maxTimeoutDelayInSeconds_) external onlyOwner { + maxTimeoutDelayInSeconds = maxTimeoutDelayInSeconds_; + emit MaxTimeoutDelayInSecondsSet(maxTimeoutDelayInSeconds_); + } + + /// @notice Sets the expiry time for payload execution + /// @param expiryTime_ The expiry time in seconds + /// @dev This function sets the expiry time for payload execution + /// @dev Only callable by the contract owner function setExpiryTime(uint256 expiryTime_) external onlyOwner { expiryTime = expiryTime_; + emit ExpiryTimeSet(expiryTime_); + } + + /// @notice Sets the watcher precompile limits contract + /// @param watcherPrecompileLimits_ The address of the watcher precompile limits contract + /// @dev This function sets the watcher precompile limits contract + /// @dev Only callable by the contract owner + function setWatcherPrecompileLimits(address watcherPrecompileLimits_) external onlyOwner { + watcherPrecompileLimits__ = IWatcherPrecompileLimits(watcherPrecompileLimits_); + emit WatcherPrecompileLimitsSet(watcherPrecompileLimits_); + } + + /// @notice Sets the watcher precompile config contract + /// @param watcherPrecompileConfig_ The address of the watcher precompile config contract + /// @dev This function sets the watcher precompile config contract + /// @dev Only callable by the contract owner + function setWatcherPrecompileConfig(address watcherPrecompileConfig_) external onlyOwner { + watcherPrecompileConfig__ = IWatcherPrecompileConfig(watcherPrecompileConfig_); + emit WatcherPrecompileConfigSet(watcherPrecompileConfig_); } + /// @notice Gets the request parameters for a request + /// @param requestCount The request count to get the parameters for + /// @return The request parameters for the given request count function getRequestParams(uint40 requestCount) external view returns (RequestParams memory) { return requestParams[requestCount]; } diff --git a/contracts/protocol/watcherPrecompile/WatcherPrecompileCore.sol b/contracts/protocol/watcherPrecompile/core/WatcherPrecompileCore.sol similarity index 58% rename from contracts/protocol/watcherPrecompile/WatcherPrecompileCore.sol rename to contracts/protocol/watcherPrecompile/core/WatcherPrecompileCore.sol index f195217a..a2e201e5 100644 --- a/contracts/protocol/watcherPrecompile/WatcherPrecompileCore.sol +++ b/contracts/protocol/watcherPrecompile/core/WatcherPrecompileCore.sol @@ -3,13 +3,15 @@ pragma solidity ^0.8.21; import "./WatcherPrecompileStorage.sol"; import {ECDSA} from "solady/utils/ECDSA.sol"; -import {AccessControl} from "../utils/AccessControl.sol"; +import {AccessControl} from "../../utils/AccessControl.sol"; import "solady/utils/Initializable.sol"; -import {AddressResolverUtil} from "../utils/AddressResolverUtil.sol"; +import {AddressResolverUtil} from "../../utils/AddressResolverUtil.sol"; import "./WatcherPrecompileUtils.sol"; -/// @title WatcherPrecompile -/// @notice Contract that handles payload verification, execution and app configurations +/// @title WatcherPrecompileCore +/// @notice Core functionality for the WatcherPrecompile system +/// @dev This contract implements the core functionality for payload verification, execution, and app configurations +/// @dev It is inherited by WatcherPrecompile and provides the base implementation for request handling abstract contract WatcherPrecompileCore is IWatcherPrecompile, WatcherPrecompileStorage, @@ -20,21 +22,23 @@ abstract contract WatcherPrecompileCore is { using DumpDecoder for bytes32; + // slots [216-265] reserved for gap + uint256[50] _core_gap; + // ================== Timeout functions ================== /// @notice Sets a timeout for a payload execution on app gateway - /// @param payload_ The payload data - /// @param delayInSeconds_ The delay in seconds + /// @return timeoutId The unique identifier for the timeout request function _setTimeout( - bytes calldata payload_, - uint256 delayInSeconds_ + uint256 delayInSeconds_, + bytes calldata payload_ ) internal returns (bytes32 timeoutId) { if (delayInSeconds_ > maxTimeoutDelayInSeconds) revert TimeoutDelayTooLarge(); - // from auction manager - watcherPrecompileLimits__.consumeLimit(_getCoreAppGateway(msg.sender), SCHEDULE, 1); uint256 executeAt = block.timestamp + delayInSeconds_; - timeoutId = _encodeTimeoutId(evmxSlug, address(this)); + timeoutId = _encodeTimeoutId(); + + // stores timeout request for watcher to track and resolve when timeout is reached timeoutRequests[timeoutId] = TimeoutRequest( timeoutId, msg.sender, @@ -44,30 +48,39 @@ abstract contract WatcherPrecompileCore is false, payload_ ); + + // consumes limit for SCHEDULE precompile + watcherPrecompileLimits__.consumeLimit(_getCoreAppGateway(msg.sender), SCHEDULE, 1); + + // emits event for watcher to track timeout and resolve when timeout is reached emit TimeoutRequested(timeoutId, msg.sender, payload_, executeAt); } + /// @notice Finalizes a payload request and requests the watcher to release the proofs + /// @param params_ The payload parameters to be finalized + /// @param transmitter_ The address of the transmitter + /// @return digest The digest hash of the finalized payload + /// @dev This function verifies the app gateway configuration and creates a digest for the payload function _finalize( PayloadParams memory params_, address transmitter_ ) internal returns (bytes32 digest) { uint32 chainSlug = params_.dump.getChainSlug(); + // Verify that the app gateway is properly configured for this chain and target watcherPrecompileConfig__.verifyConnections( chainSlug, params_.target, params_.appGateway, - params_.switchboard + params_.switchboard, + requestParams[params_.dump.getRequestCount()].middleware ); uint256 deadline = block.timestamp + expiryTime; payloads[params_.payloadId].deadline = deadline; payloads[params_.payloadId].finalizedTransmitter = transmitter_; - bytes32 prevDigestsHash = _getPreviousDigestsHash( - params_.dump.getRequestCount(), - params_.dump.getBatchCount() - ); + bytes32 prevDigestsHash = _getPreviousDigestsHash(params_.dump.getBatchCount()); payloads[params_.payloadId].prevDigestsHash = prevDigestsHash; // Construct parameters for digest calculation @@ -92,31 +105,23 @@ abstract contract WatcherPrecompileCore is emit FinalizeRequested(digest, payloads[params_.payloadId]); } - function _getBatch(uint40 batchCount) internal view returns (PayloadParams[] memory) { - bytes32[] memory payloadIds = batchPayloadIds[batchCount]; - PayloadParams[] memory payloadParamsArray = new PayloadParams[](payloadIds.length); - - for (uint40 i = 0; i < payloadIds.length; i++) { - payloadParamsArray[i] = payloads[payloadIds[i]]; - } - return payloadParamsArray; - } - // ================== Query functions ================== + /// @notice Creates a new query request - /// @param params_ The payload parameters + /// @param params_ The payload parameters for the query + /// @dev This function sets up a query request and emits a QueryRequested event function _query(PayloadParams memory params_) internal { - bytes32 prevDigestsHash = _getPreviousDigestsHash( - params_.dump.getRequestCount(), - params_.dump.getBatchCount() - ); + bytes32 prevDigestsHash = _getPreviousDigestsHash(params_.dump.getBatchCount()); payloads[params_.payloadId].prevDigestsHash = prevDigestsHash; emit QueryRequested(params_); } + // ================== Helper functions ================== + /// @notice Calculates the digest hash of payload parameters - /// @param params_ The payload parameters - /// @return digest The calculated digest + /// @param params_ The payload parameters to calculate the digest for + /// @return digest The calculated digest hash + /// @dev This function creates a keccak256 hash of the payload parameters function getDigest(DigestParams memory params_) public pure returns (bytes32 digest) { digest = keccak256( abi.encode( @@ -137,22 +142,15 @@ abstract contract WatcherPrecompileCore is ); } - function _getPreviousDigestsHash( - uint40 requestCount_, - uint40 batchCount_ - ) internal view returns (bytes32) { - RequestParams memory r = requestParams[requestCount_]; - - // If this is the first batch of the request, return 0 bytes - if (batchCount_ == r.payloadParamsArray[0].dump.getBatchCount()) { - return bytes32(0); - } - - PayloadParams[] memory previousPayloads = _getBatch(batchCount_ - 1); + /// @notice Gets the hash of previous batch digests + /// @param batchCount_ The batch count to get the previous digests hash + /// @return The hash of all digests in the previous batch + function _getPreviousDigestsHash(uint40 batchCount_) internal view returns (bytes32) { + bytes32[] memory payloadIds = batchPayloadIds[batchCount_]; bytes32 prevDigestsHash = bytes32(0); - for (uint40 i = 0; i < previousPayloads.length; i++) { - PayloadParams memory p = payloads[previousPayloads[i].payloadId]; + for (uint40 i = 0; i < payloadIds.length; i++) { + PayloadParams memory p = payloads[payloadIds[i]]; DigestParams memory digestParams = DigestParams( watcherPrecompileConfig__.sockets(p.dump.getChainSlug()), p.finalizedTransmitter, @@ -173,52 +171,59 @@ abstract contract WatcherPrecompileCore is return prevDigestsHash; } - // ================== Helper functions ================== + /// @notice Gets the batch of payload parameters for a given batch count + /// @param batchCount The batch count to get the payload parameters for + /// @return An array of PayloadParams for the given batch + /// @dev This function retrieves all payload parameters for a specific batch + function _getBatch(uint40 batchCount) internal view returns (PayloadParams[] memory) { + bytes32[] memory payloadIds = batchPayloadIds[batchCount]; + PayloadParams[] memory payloadParamsArray = new PayloadParams[](payloadIds.length); - /// @notice Verifies the connection between chain slug, target, and app gateway - /// @param chainSlug_ The identifier of the chain - /// @param target_ The target address - /// @param appGateway_ The app gateway address to verify - /// @dev Internal function to validate connections - function _verifyConnections( - uint32 chainSlug_, - address target_, - address appGateway_, - address switchboard_ - ) internal view { - // todo: revisit this - // if target is contractFactoryPlug, return - if (target_ == watcherPrecompileConfig__.contractFactoryPlug(chainSlug_)) return; - - (bytes32 appGatewayId, address switchboard) = watcherPrecompileConfig__.getPlugConfigs( - chainSlug_, - target_ - ); - if (appGatewayId != _encodeAppGatewayId(appGateway_)) revert InvalidGateway(); - if (switchboard != switchboard_) revert InvalidSwitchboard(); + for (uint40 i = 0; i < payloadIds.length; i++) { + payloadParamsArray[i] = payloads[payloadIds[i]]; + } + return payloadParamsArray; } - function _encodeTimeoutId(uint32 chainSlug_, address watcher_) internal returns (bytes32) { + /// @notice Encodes an ID for a timeout or payload + /// @return The encoded ID + /// @dev This function creates a unique ID by combining the chain slug, address, and a counter + function _encodeTimeoutId() internal returns (bytes32) { // Encode timeout ID by bit-shifting and combining: - // chainSlug (32 bits) | switchboard or watcher precompile address (160 bits) | counter (64 bits) + // EVMx chainSlug (32 bits) | watcher precompile address (160 bits) | counter (64 bits) return bytes32( - (uint256(chainSlug_) << 224) | (uint256(uint160(watcher_)) << 64) | timeoutCounter++ + (uint256(evmxSlug) << 224) | + (uint256(uint160(address(this))) << 64) | + payloadCounter++ ); } + /// @notice Creates a payload ID from the given parameters + /// @param requestCount_ The request count + /// @param batchCount_ The batch count + /// @param payloadCount_ The payload count + /// @param switchboard_ The switchboard address + /// @param chainSlug_ The chain slug + /// @return The created payload ID function _createPayloadId( - PayloadSubmitParams memory p_, uint40 requestCount_, uint40 batchCount_, - uint40 payloadCount_ + uint40 payloadCount_, + address switchboard_, + uint32 chainSlug_ ) internal pure returns (bytes32) { return keccak256( - abi.encode(requestCount_, batchCount_, payloadCount_, p_.switchboard, p_.chainSlug) + abi.encode(requestCount_, batchCount_, payloadCount_, switchboard_, chainSlug_) ); } + /// @notice Verifies that a watcher signature is valid + /// @param digest_ The digest to verify + /// @param signatureNonce_ The nonce of the signature + /// @param signature_ The signature to verify + /// @dev This function verifies that the signature was created by the watcher and that the nonce has not been used before function _isWatcherSignatureValid( bytes memory digest_, uint256 signatureNonce_, @@ -235,14 +240,23 @@ abstract contract WatcherPrecompileCore is if (signer != owner()) revert InvalidWatcherSignature(); } + /// @notice Gets the batch IDs for a request + /// @param requestCount_ The request count to get the batch IDs for + /// @return An array of batch IDs for the given request function getBatches(uint40 requestCount_) external view returns (uint40[] memory) { return requestBatchIds[requestCount_]; } + /// @notice Gets the payload IDs for a batch + /// @param batchCount_ The batch count to get the payload IDs for + /// @return An array of payload IDs for the given batch function getBatchPayloadIds(uint40 batchCount_) external view returns (bytes32[] memory) { return batchPayloadIds[batchCount_]; } + /// @notice Gets the payload parameters for a payload ID + /// @param payloadId_ The payload ID to get the parameters for + /// @return The payload parameters for the given payload ID function getPayloadParams(bytes32 payloadId_) external view returns (PayloadParams memory) { return payloads[payloadId_]; } diff --git a/contracts/protocol/watcherPrecompile/core/WatcherPrecompileStorage.sol b/contracts/protocol/watcherPrecompile/core/WatcherPrecompileStorage.sol new file mode 100644 index 00000000..019d1b58 --- /dev/null +++ b/contracts/protocol/watcherPrecompile/core/WatcherPrecompileStorage.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "../../../interfaces/IWatcherPrecompile.sol"; +import {IAppGateway} from "../../../interfaces/IAppGateway.sol"; +import {IFeesManager} from "../../../interfaces/IFeesManager.sol"; +import {IPromise} from "../../../interfaces/IPromise.sol"; +import "../DumpDecoder.sol"; + +import {IMiddleware} from "../../../interfaces/IMiddleware.sol"; +import {QUERY, FINALIZE, SCHEDULE, MAX_COPY_BYTES} from "../../utils/common/Constants.sol"; +import {InvalidCallerTriggered, TimeoutDelayTooLarge, TimeoutAlreadyResolved, InvalidInboxCaller, ResolvingTimeoutTooEarly, CallFailed, AppGatewayAlreadyCalled, InvalidWatcherSignature, NonceUsed, RequestAlreadyExecuted} from "../../utils/common/Errors.sol"; +import {ResolvedPromises, AppGatewayConfig, LimitParams, WriteFinality, UpdateLimitParams, PlugConfig, DigestParams, TimeoutRequest, QueuePayloadParams, PayloadParams, RequestParams} from "../../utils/common/Structs.sol"; + +/// @title WatcherPrecompileStorage +/// @notice Storage contract for the WatcherPrecompile system +/// @dev This contract contains all the storage variables used by the WatcherPrecompile system +/// @dev It is inherited by WatcherPrecompileCore and WatcherPrecompile +abstract contract WatcherPrecompileStorage is IWatcherPrecompile { + // slots [0-49]: gap for future storage variables + uint256[50] _gap_before; + + // slot 50 + /// @notice The chain slug of the watcher precompile + uint32 public evmxSlug; + + /// @notice Counter for tracking payload requests + uint40 public payloadCounter; + + /// @notice Counter for tracking request counts + uint40 public override nextRequestCount; + + /// @notice Counter for tracking batch counts + uint40 public nextBatchCount; + + // slot 51 + /// @notice The time from finalize for the payload to be executed + /// @dev Expiry time in seconds for payload execution + uint256 public expiryTime; + + // slot 52 + /// @notice The maximum delay for a timeout + /// @dev Maximum timeout delay in seconds + uint256 public maxTimeoutDelayInSeconds; + + // slot 53 + /// @notice stores temporary address of the app gateway caller from a chain + address public appGatewayCaller; + + // slot 54 + /// @notice Maps nonce to whether it has been used + /// @dev Used to prevent replay attacks with signature nonces + /// @dev signatureNonce => isValid + mapping(uint256 => bool) public isNonceUsed; + + // slot 55 + /// @notice Mapping to store timeout requests + /// @dev Maps timeout ID to TimeoutRequest struct + /// @dev timeoutId => TimeoutRequest struct + mapping(bytes32 => TimeoutRequest) public timeoutRequests; + + // slot 56 + /// @notice Mapping to store watcher proofs + /// @dev Maps payload ID to proof bytes + /// @dev payloadId => proof bytes + mapping(bytes32 => bytes) public watcherProofs; + + // slot 57 + /// @notice Mapping to store if appGateway has been called with trigger from on-chain Inbox + /// @dev Maps call ID to boolean indicating if the appGateway has been called + /// @dev callId => bool + mapping(bytes32 => bool) public appGatewayCalled; + + // slot 58 + /// @notice Mapping to store the request parameters for each request count + mapping(uint40 => RequestParams) public requestParams; + + // slot 59 + /// @notice Mapping to store the list of payload IDs for each batch + mapping(uint40 => bytes32[]) public batchPayloadIds; + + // slot 60 + /// @notice Mapping to store the batch IDs for each request + mapping(uint40 => uint40[]) public requestBatchIds; + + // slot 61 + /// @notice Mapping to store the payload parameters for each payload ID + mapping(bytes32 => PayloadParams) public payloads; + + // slot 62 + /// @notice Mapping to store if a promise has been executed + mapping(bytes32 => bool) public isPromiseExecuted; + + // slot 63 + IWatcherPrecompileLimits public watcherPrecompileLimits__; + + // slot 64 + IWatcherPrecompileConfig public watcherPrecompileConfig__; + + // slots [65-114]: gap for future storage variables + uint256[50] _gap_after; + + // slots 115-165 (51) reserved for access control + // slots 166-216 (51) reserved for addr resolver util +} diff --git a/contracts/protocol/watcherPrecompile/WatcherPrecompileUtils.sol b/contracts/protocol/watcherPrecompile/core/WatcherPrecompileUtils.sol similarity index 100% rename from contracts/protocol/watcherPrecompile/WatcherPrecompileUtils.sol rename to contracts/protocol/watcherPrecompile/core/WatcherPrecompileUtils.sol diff --git a/script/admin/UpdateAppEVMxLimits.s.sol b/script/admin/UpdateAppEVMxLimits.s.sol index 4389c1be..7c650dd7 100644 --- a/script/admin/UpdateAppEVMxLimits.s.sol +++ b/script/admin/UpdateAppEVMxLimits.s.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.13; import "forge-std/Script.sol"; -import {WatcherPrecompile} from "../../contracts/protocol/watcherPrecompile/WatcherPrecompile.sol"; +import {WatcherPrecompile} from "../../contracts/protocol/watcherPrecompile/core/WatcherPrecompile.sol"; import {UpdateLimitParams} from "../../contracts/protocol/utils/common/Structs.sol"; import {SCHEDULE, QUERY, FINALIZE} from "../../contracts/protocol/utils/common/Constants.sol"; diff --git a/script/helpers/CheckAppEVMxLimits.s.sol b/script/helpers/CheckAppEVMxLimits.s.sol index 7c1f16a0..601dca82 100644 --- a/script/helpers/CheckAppEVMxLimits.s.sol +++ b/script/helpers/CheckAppEVMxLimits.s.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.13; import "forge-std/Script.sol"; -import {WatcherPrecompile} from "../../contracts/protocol/watcherPrecompile/WatcherPrecompile.sol"; +import {WatcherPrecompile} from "../../contracts/protocol/watcherPrecompile/core/WatcherPrecompile.sol"; import {LimitParams} from "../../contracts/protocol/utils/common/Structs.sol"; import {SCHEDULE, QUERY, FINALIZE} from "../../contracts/protocol/utils/common/Constants.sol"; diff --git a/test/Inbox.t.sol b/test/Inbox.t.sol index d1763190..1361cb9f 100644 --- a/test/Inbox.t.sol +++ b/test/Inbox.t.sol @@ -62,7 +62,7 @@ contract TriggerTest is DeliveryHelperTest { // Simulate a message from another chain through the watcher uint256 incrementValue = 5; - bytes32 triggerId = _encodeTriggerId(address(gateway), arbChainSlug); + bytes32 triggerId = _encodeTriggerId(address(arbConfig.socket), arbChainSlug); bytes memory payload = abi.encodeWithSelector( CounterAppGateway.increase.selector, incrementValue diff --git a/test/Migration.t.sol b/test/Migration.t.sol index 0632d583..aed1bf3e 100644 --- a/test/Migration.t.sol +++ b/test/Migration.t.sol @@ -3,272 +3,332 @@ pragma solidity ^0.8.0; import "./SetupTest.t.sol"; import "../contracts/protocol/AddressResolver.sol"; -import "../contracts/protocol/watcherPrecompile/WatcherPrecompile.sol"; +import "../contracts/protocol/watcherPrecompile/core/WatcherPrecompile.sol"; +import "../contracts/protocol/watcherPrecompile/WatcherPrecompileLimits.sol"; +import "../contracts/protocol/watcherPrecompile/WatcherPrecompileConfig.sol"; import "../contracts/protocol/Forwarder.sol"; import "../contracts/protocol/AsyncPromise.sol"; import "./mock/MockWatcherPrecompileImpl.sol"; -// contract MigrationTest is SetupTest { -// // ERC1967Factory emits this event with both proxy and implementation addresses -// event Upgraded(address indexed proxy, address indexed implementation); -// event ImplementationUpdated(string contractName, address newImplementation); - -// // ERC1967 implementation slot -// bytes32 internal constant _IMPLEMENTATION_SLOT = -// 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; - -// // Beacon implementation slot -// uint256 internal constant _BEACON_IMPLEMENTATION_SLOT = 0x911c5a209f08d5ec5e; - -// // Beacon slot in ERC1967 -// bytes32 internal constant _BEACON_SLOT = -// 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50; - -// // Error selector for Unauthorized error -// bytes4 internal constant UNAUTHORIZED_SELECTOR = 0x82b42900; // bytes4(keccak256("Unauthorized()")) - -// function setUp() public { -// deployEVMxCore(); -// } - -// function getImplementation(address proxy) internal view returns (address) { -// bytes32 value = vm.load(proxy, _IMPLEMENTATION_SLOT); -// return address(uint160(uint256(value))); -// } - -// function getBeaconImplementation(address beacon) internal view returns (address) { -// bytes32 value = vm.load(beacon, bytes32(_BEACON_IMPLEMENTATION_SLOT)); -// return address(uint160(uint256(value))); -// } - -// function getBeacon(address proxy) internal view returns (address) { -// bytes32 value = vm.load(proxy, _BEACON_SLOT); -// return address(uint160(uint256(value))); -// } - -// function testAddressResolverUpgrade() public { -// // Deploy new implementation -// AddressResolver newImpl = new AddressResolver(); - -// // Store old implementation address -// address oldImpl = getImplementation(address(addressResolver)); - -// // Upgrade proxy to new implementation -// vm.startPrank(watcherEOA); -// vm.expectEmit(true, true, true, true, address(proxyFactory)); -// emit Upgraded(address(addressResolver), address(newImpl)); -// proxyFactory.upgradeAndCall(address(addressResolver), address(newImpl), ""); -// vm.stopPrank(); - -// // Verify upgrade was successful -// address newImplAddr = getImplementation(address(addressResolver)); -// assertNotEq(oldImpl, newImplAddr, "Implementation should have changed"); -// assertEq(newImplAddr, address(newImpl), "New implementation not set correctly"); - -// // Verify state is preserved -// assertEq(addressResolver.owner(), watcherEOA, "Owner should be preserved after upgrade"); -// assertEq( -// address(addressResolver.watcherPrecompile__()), -// address(watcherPrecompile), -// "WatcherPrecompile address should be preserved" -// ); -// } - -// function testWatcherPrecompileUpgrade() public { -// // Deploy new implementation -// WatcherPrecompile newImpl = new WatcherPrecompile(); - -// // Store old implementation address -// address oldImpl = getImplementation(address(watcherPrecompile)); - -// // Upgrade proxy to new implementation -// vm.startPrank(watcherEOA); -// vm.expectEmit(true, true, true, true, address(proxyFactory)); -// emit Upgraded(address(watcherPrecompile), address(newImpl)); -// proxyFactory.upgradeAndCall(address(watcherPrecompile), address(newImpl), ""); -// vm.stopPrank(); - -// // Verify upgrade was successful -// address newImplAddr = getImplementation(address(watcherPrecompile)); -// assertNotEq(oldImpl, newImplAddr, "Implementation should have changed"); -// assertEq(newImplAddr, address(newImpl), "New implementation not set correctly"); - -// // Verify state is preserved -// assertEq(watcherPrecompile.owner(), watcherEOA, "Owner should be preserved after upgrade"); -// assertEq( -// address(watcherPrecompile.addressResolver__()), -// address(addressResolver), -// "AddressResolver should be preserved" -// ); -// assertEq( -// watcherPrecompile.defaultLimit(), -// defaultLimit * 10 ** 18, -// "DefaultLimit should be preserved" -// ); -// } - -// function testUpgradeWithInitializationData() public { -// // Deploy new implementation -// MockWatcherPrecompileImpl newImpl = new MockWatcherPrecompileImpl(); - -// // Store old implementation address for verification -// address oldImpl = getImplementation(address(watcherPrecompile)); - -// // Prepare initialization data with new defaultLimit -// uint256 newDefaultLimit = 2000; -// bytes memory initData = abi.encodeWithSelector( -// MockWatcherPrecompileImpl.mockReinitialize.selector, -// watcherEOA, -// address(addressResolver), -// newDefaultLimit -// ); - -// // Upgrade proxy with initialization data -// vm.startPrank(watcherEOA); -// vm.expectEmit(true, true, true, true, address(proxyFactory)); -// emit Upgraded(address(watcherPrecompile), address(newImpl)); -// proxyFactory.upgradeAndCall(address(watcherPrecompile), address(newImpl), initData); -// vm.stopPrank(); - -// // Verify upgrade and initialization was successful -// address newImplAddr = getImplementation(address(watcherPrecompile)); -// assertNotEq(oldImpl, newImplAddr, "Implementation should have changed"); -// assertEq(newImplAddr, address(newImpl), "New implementation not set correctly"); -// assertEq( -// watcherPrecompile.defaultLimit(), -// newDefaultLimit * 10 ** 18, -// "DefaultLimit should be updated" -// ); -// } - -// function testUnauthorizedUpgrade() public { -// // Deploy new implementation -// WatcherPrecompile newImpl = new WatcherPrecompile(); - -// // Try to upgrade from unauthorized account -// address unauthorizedUser = address(0xBEEF); -// vm.startPrank(unauthorizedUser); -// vm.expectRevert(UNAUTHORIZED_SELECTOR); -// proxyFactory.upgradeAndCall(address(watcherPrecompile), address(newImpl), ""); -// vm.stopPrank(); - -// // Verify implementation was not changed -// assertEq( -// getImplementation(address(watcherPrecompile)), -// address(watcherPrecompileImpl), -// "Implementation should not have changed" -// ); -// } - -// function testForwarderBeaconUpgrade() public { -// // Deploy new implementation -// Forwarder newImpl = new Forwarder(); - -// // Get current implementation from beacon -// address oldImpl = getBeaconImplementation(address(addressResolver.forwarderBeacon())); - -// // Upgrade beacon to new implementation -// vm.startPrank(watcherEOA); -// vm.expectEmit(true, true, true, true, address(addressResolver)); -// emit ImplementationUpdated("Forwarder", address(newImpl)); -// addressResolver.setForwarderImplementation(address(newImpl)); -// vm.stopPrank(); - -// // Verify upgrade was successful -// address newImplAddr = getBeaconImplementation(address(addressResolver.forwarderBeacon())); -// assertNotEq(oldImpl, newImplAddr, "Implementation should have changed"); -// assertEq(newImplAddr, address(newImpl), "New implementation not set correctly"); - -// // Deploy a new forwarder and verify it uses the correct beacon -// address newForwarder = addressResolver.getOrDeployForwarderContract( -// address(this), -// address(0x123), -// 1 -// ); -// address beacon = getBeacon(newForwarder); -// assertEq( -// beacon, -// address(addressResolver.forwarderBeacon()), -// "Beacon address not set correctly" -// ); - -// // Get implementation from beacon and verify it matches -// address implFromBeacon = getBeaconImplementation(beacon); -// assertEq( -// implFromBeacon, -// address(newImpl), -// "Beacon implementation should match new implementation" -// ); -// } - -// function testAsyncPromiseBeaconUpgrade() public { -// // Deploy new implementation -// AsyncPromise newImpl = new AsyncPromise(); - -// // Get current implementation from beacon -// address oldImpl = getBeaconImplementation(address(addressResolver.asyncPromiseBeacon())); - -// // Upgrade beacon to new implementation -// vm.startPrank(watcherEOA); -// vm.expectEmit(true, true, true, true, address(addressResolver)); -// emit ImplementationUpdated("AsyncPromise", address(newImpl)); -// addressResolver.setAsyncPromiseImplementation(address(newImpl)); -// vm.stopPrank(); - -// // Verify upgrade was successful -// address newImplAddr = getBeaconImplementation( -// address(addressResolver.asyncPromiseBeacon()) -// ); -// assertNotEq(oldImpl, newImplAddr, "Implementation should have changed"); -// assertEq(newImplAddr, address(newImpl), "New implementation not set correctly"); - -// // Deploy a new async promise and verify it uses the correct beacon -// address newPromise = addressResolver.deployAsyncPromiseContract(address(this)); -// address beacon = getBeacon(newPromise); -// assertEq( -// beacon, -// address(addressResolver.asyncPromiseBeacon()), -// "Beacon address not set correctly" -// ); - -// // Get implementation from beacon and verify it matches -// address implFromBeacon = getBeaconImplementation(beacon); -// assertEq( -// implFromBeacon, -// address(newImpl), -// "Beacon implementation should match new implementation" -// ); -// } - -// function testUnauthorizedBeaconUpgrade() public { -// // Deploy new implementations -// Forwarder newForwarderImpl = new Forwarder(); -// AsyncPromise newAsyncPromiseImpl = new AsyncPromise(); - -// // Try to upgrade from unauthorized account -// address unauthorizedUser = address(0xBEEF); - -// vm.startPrank(unauthorizedUser); -// // Try upgrading forwarder beacon -// vm.expectRevert(abi.encodeWithSelector(Ownable.Unauthorized.selector)); -// addressResolver.setForwarderImplementation(address(newForwarderImpl)); - -// // Try upgrading async promise beacon -// vm.expectRevert(abi.encodeWithSelector(Ownable.Unauthorized.selector)); -// addressResolver.setAsyncPromiseImplementation(address(newAsyncPromiseImpl)); - -// vm.stopPrank(); - -// // Verify implementations were not changed -// assertNotEq( -// getBeaconImplementation(address(addressResolver.forwarderBeacon())), -// address(newForwarderImpl), -// "Forwarder implementation should not have changed" -// ); -// assertNotEq( -// getBeaconImplementation(address(addressResolver.asyncPromiseBeacon())), -// address(newAsyncPromiseImpl), -// "AsyncPromise implementation should not have changed" -// ); -// } -// } +contract MigrationTest is SetupTest { + // ERC1967Factory emits this event with both proxy and implementation addresses + event Upgraded(address indexed proxy, address indexed implementation); + event ImplementationUpdated(string contractName, address newImplementation); + + // ERC1967 implementation slot + bytes32 internal constant _IMPLEMENTATION_SLOT = + 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + + // Beacon implementation slot + uint256 internal constant _BEACON_IMPLEMENTATION_SLOT = 0x911c5a209f08d5ec5e; + + // Beacon slot in ERC1967 + bytes32 internal constant _BEACON_SLOT = + 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50; + + // Error selector for Unauthorized error + bytes4 internal constant UNAUTHORIZED_SELECTOR = 0x82b42900; // bytes4(keccak256("Unauthorized()")) + + function setUp() public { + deployEVMxCore(); + } + + function getImplementation(address proxy) internal view returns (address) { + bytes32 value = vm.load(proxy, _IMPLEMENTATION_SLOT); + return address(uint160(uint256(value))); + } + + function getBeaconImplementation(address beacon) internal view returns (address) { + bytes32 value = vm.load(beacon, bytes32(_BEACON_IMPLEMENTATION_SLOT)); + return address(uint160(uint256(value))); + } + + function getBeacon(address proxy) internal view returns (address) { + bytes32 value = vm.load(proxy, _BEACON_SLOT); + return address(uint160(uint256(value))); + } + + function testAddressResolverUpgrade() public { + // Deploy new implementation + AddressResolver newImpl = new AddressResolver(); + + // Store old implementation address + address oldImpl = getImplementation(address(addressResolver)); + + // Upgrade proxy to new implementation + vm.startPrank(watcherEOA); + vm.expectEmit(true, true, true, true, address(proxyFactory)); + emit Upgraded(address(addressResolver), address(newImpl)); + proxyFactory.upgradeAndCall(address(addressResolver), address(newImpl), ""); + vm.stopPrank(); + + // Verify upgrade was successful + address newImplAddr = getImplementation(address(addressResolver)); + assertNotEq(oldImpl, newImplAddr, "Implementation should have changed"); + assertEq(newImplAddr, address(newImpl), "New implementation not set correctly"); + + // Verify state is preserved + assertEq(addressResolver.owner(), watcherEOA, "Owner should be preserved after upgrade"); + assertEq( + address(addressResolver.watcherPrecompile__()), + address(watcherPrecompile), + "WatcherPrecompile address should be preserved" + ); + } + + function testWatcherPrecompileUpgrade() public { + // Deploy new implementation + WatcherPrecompile newImpl = new WatcherPrecompile(); + + // Store old implementation address + address oldImpl = getImplementation(address(watcherPrecompile)); + + // Upgrade proxy to new implementation + vm.startPrank(watcherEOA); + vm.expectEmit(true, true, true, true, address(proxyFactory)); + emit Upgraded(address(watcherPrecompile), address(newImpl)); + proxyFactory.upgradeAndCall(address(watcherPrecompile), address(newImpl), ""); + vm.stopPrank(); + + // Verify upgrade was successful + address newImplAddr = getImplementation(address(watcherPrecompile)); + assertNotEq(oldImpl, newImplAddr, "Implementation should have changed"); + assertEq(newImplAddr, address(newImpl), "New implementation not set correctly"); + + // Verify state is preserved + assertEq(watcherPrecompile.owner(), watcherEOA, "Owner should be preserved after upgrade"); + assertEq( + address(watcherPrecompile.watcherPrecompileConfig__()), + address(watcherPrecompileConfig), + "WatcherPrecompileConfig should be preserved" + ); + assertEq(watcherPrecompile.evmxSlug(), evmxSlug, "EvmxSlug should be preserved"); + } + + function testWatcherPrecompileLimitsUpgrade() public { + // Deploy new implementation + WatcherPrecompileLimits newImpl = new WatcherPrecompileLimits(); + + // Store old implementation address + address oldImpl = getImplementation(address(watcherPrecompileLimits)); + + // Upgrade proxy to new implementation + vm.startPrank(watcherEOA); + vm.expectEmit(true, true, true, true, address(proxyFactory)); + emit Upgraded(address(watcherPrecompileLimits), address(newImpl)); + proxyFactory.upgradeAndCall(address(watcherPrecompileLimits), address(newImpl), ""); + vm.stopPrank(); + + // Verify upgrade was successful + address newImplAddr = getImplementation(address(watcherPrecompileLimits)); + assertNotEq(oldImpl, newImplAddr, "Implementation should have changed"); + assertEq(newImplAddr, address(newImpl), "New implementation not set correctly"); + + // Verify state is preserved + assertEq(watcherPrecompile.owner(), watcherEOA, "Owner should be preserved after upgrade"); + assertEq( + address(watcherPrecompileLimits.addressResolver__()), + address(addressResolver), + "AddressResolver should be preserved" + ); + assertEq( + watcherPrecompileLimits.defaultLimit(), + defaultLimit * 10 ** 18, + "DefaultLimit should be preserved" + ); + } + + function testWatcherPrecompileConfigUpgrade() public { + // Deploy new implementation + WatcherPrecompileConfig newImpl = new WatcherPrecompileConfig(); + + // Store old implementation address + address oldImpl = getImplementation(address(watcherPrecompileConfig)); + + // Upgrade proxy to new implementation + vm.startPrank(watcherEOA); + vm.expectEmit(true, true, true, true, address(proxyFactory)); + emit Upgraded(address(watcherPrecompileConfig), address(newImpl)); + proxyFactory.upgradeAndCall(address(watcherPrecompileConfig), address(newImpl), ""); + vm.stopPrank(); + + // Verify upgrade was successful + address newImplAddr = getImplementation(address(watcherPrecompileConfig)); + assertNotEq(oldImpl, newImplAddr, "Implementation should have changed"); + assertEq(newImplAddr, address(newImpl), "New implementation not set correctly"); + + // Verify state is preserved + assertEq( + watcherPrecompileConfig.owner(), + watcherEOA, + "Owner should be preserved after upgrade" + ); + assertEq( + address(watcherPrecompileConfig.addressResolver__()), + address(addressResolver), + "AddressResolver should be preserved" + ); + assertEq(watcherPrecompileConfig.evmxSlug(), 1, "EvmxSlug should be preserved"); + } + + function testUpgradeWithInitializationData() public { + // Deploy new implementation + MockWatcherPrecompileImpl newImpl = new MockWatcherPrecompileImpl(); + + // Store old implementation address for verification + address oldImpl = getImplementation(address(watcherPrecompile)); + + // Prepare initialization data with new defaultLimit + uint256 newDefaultLimit = 2000; + bytes memory initData = abi.encodeWithSelector( + MockWatcherPrecompileImpl.mockReinitialize.selector, + watcherEOA, + address(addressResolver), + newDefaultLimit + ); + + // Upgrade proxy with initialization data + vm.startPrank(watcherEOA); + vm.expectEmit(true, true, true, true, address(proxyFactory)); + emit Upgraded(address(watcherPrecompile), address(newImpl)); + proxyFactory.upgradeAndCall(address(watcherPrecompile), address(newImpl), initData); + vm.stopPrank(); + + // Verify upgrade and initialization was successful + address newImplAddr = getImplementation(address(watcherPrecompile)); + assertNotEq(oldImpl, newImplAddr, "Implementation should have changed"); + assertEq(newImplAddr, address(newImpl), "New implementation not set correctly"); + assertEq(watcherPrecompile.evmxSlug(), evmxSlug, "EvmxSlug should be preserved"); + } + + function testUnauthorizedUpgrade() public { + // Deploy new implementation + WatcherPrecompile newImpl = new WatcherPrecompile(); + + // Try to upgrade from unauthorized account + address unauthorizedUser = address(0xBEEF); + vm.startPrank(unauthorizedUser); + vm.expectRevert(UNAUTHORIZED_SELECTOR); + proxyFactory.upgradeAndCall(address(watcherPrecompile), address(newImpl), ""); + vm.stopPrank(); + + // Verify implementation was not changed + assertEq( + getImplementation(address(watcherPrecompile)), + address(watcherPrecompileImpl), + "Implementation should not have changed" + ); + } + + function testForwarderBeaconUpgrade() public { + // Deploy new implementation + Forwarder newImpl = new Forwarder(); + + // Get current implementation from beacon + address oldImpl = getBeaconImplementation(address(addressResolver.forwarderBeacon())); + + // Upgrade beacon to new implementation + vm.startPrank(watcherEOA); + vm.expectEmit(true, true, true, true, address(addressResolver)); + emit ImplementationUpdated("Forwarder", address(newImpl)); + addressResolver.setForwarderImplementation(address(newImpl)); + vm.stopPrank(); + + // Verify upgrade was successful + address newImplAddr = getBeaconImplementation(address(addressResolver.forwarderBeacon())); + assertNotEq(oldImpl, newImplAddr, "Implementation should have changed"); + assertEq(newImplAddr, address(newImpl), "New implementation not set correctly"); + + // Deploy a new forwarder and verify it uses the correct beacon + address newForwarder = addressResolver.getOrDeployForwarderContract( + address(this), + address(0x123), + 1 + ); + address beacon = getBeacon(newForwarder); + assertEq( + beacon, + address(addressResolver.forwarderBeacon()), + "Beacon address not set correctly" + ); + + // Get implementation from beacon and verify it matches + address implFromBeacon = getBeaconImplementation(beacon); + assertEq( + implFromBeacon, + address(newImpl), + "Beacon implementation should match new implementation" + ); + } + + function testAsyncPromiseBeaconUpgrade() public { + // Deploy new implementation + AsyncPromise newImpl = new AsyncPromise(); + + // Get current implementation from beacon + address oldImpl = getBeaconImplementation(address(addressResolver.asyncPromiseBeacon())); + + // Upgrade beacon to new implementation + vm.startPrank(watcherEOA); + vm.expectEmit(true, true, true, true, address(addressResolver)); + emit ImplementationUpdated("AsyncPromise", address(newImpl)); + addressResolver.setAsyncPromiseImplementation(address(newImpl)); + vm.stopPrank(); + + // Verify upgrade was successful + address newImplAddr = getBeaconImplementation( + address(addressResolver.asyncPromiseBeacon()) + ); + assertNotEq(oldImpl, newImplAddr, "Implementation should have changed"); + assertEq(newImplAddr, address(newImpl), "New implementation not set correctly"); + + // Deploy a new async promise and verify it uses the correct beacon + address newPromise = addressResolver.deployAsyncPromiseContract(address(this)); + address beacon = getBeacon(newPromise); + assertEq( + beacon, + address(addressResolver.asyncPromiseBeacon()), + "Beacon address not set correctly" + ); + + // Get implementation from beacon and verify it matches + address implFromBeacon = getBeaconImplementation(beacon); + assertEq( + implFromBeacon, + address(newImpl), + "Beacon implementation should match new implementation" + ); + } + + function testUnauthorizedBeaconUpgrade() public { + // Deploy new implementations + Forwarder newForwarderImpl = new Forwarder(); + AsyncPromise newAsyncPromiseImpl = new AsyncPromise(); + + // Try to upgrade from unauthorized account + address unauthorizedUser = address(0xBEEF); + + vm.startPrank(unauthorizedUser); + // Try upgrading forwarder beacon + vm.expectRevert(abi.encodeWithSelector(Ownable.Unauthorized.selector)); + addressResolver.setForwarderImplementation(address(newForwarderImpl)); + + // Try upgrading async promise beacon + vm.expectRevert(abi.encodeWithSelector(Ownable.Unauthorized.selector)); + addressResolver.setAsyncPromiseImplementation(address(newAsyncPromiseImpl)); + + vm.stopPrank(); + + // Verify implementations were not changed + assertNotEq( + getBeaconImplementation(address(addressResolver.forwarderBeacon())), + address(newForwarderImpl), + "Forwarder implementation should not have changed" + ); + assertNotEq( + getBeaconImplementation(address(addressResolver.asyncPromiseBeacon())), + address(newAsyncPromiseImpl), + "AsyncPromise implementation should not have changed" + ); + } +} diff --git a/test/SetupTest.t.sol b/test/SetupTest.t.sol index b752a6c7..b9407b81 100644 --- a/test/SetupTest.t.sol +++ b/test/SetupTest.t.sol @@ -5,7 +5,7 @@ import "forge-std/Test.sol"; import "../contracts/protocol/utils/common/Structs.sol"; import "../contracts/protocol/utils/common/Errors.sol"; import "../contracts/protocol/utils/common/Constants.sol"; -import "../contracts/protocol/watcherPrecompile/WatcherPrecompile.sol"; +import "../contracts/protocol/watcherPrecompile/core/WatcherPrecompile.sol"; import "../contracts/protocol/watcherPrecompile/WatcherPrecompileConfig.sol"; import "../contracts/protocol/watcherPrecompile/WatcherPrecompileLimits.sol"; import "../contracts/protocol/watcherPrecompile/DumpDecoder.sol"; diff --git a/test/mock/MockSocket.sol b/test/mock/MockSocket.sol index 26650c35..5489ea08 100644 --- a/test/mock/MockSocket.sol +++ b/test/mock/MockSocket.sol @@ -53,9 +53,6 @@ contract MockSocket is ISocket { * @dev Error emitted when verification fails */ error VerificationFailed(); - /** - * @dev Error emitted when source slugs deduced from packet id and msg id don't match - */ /** * @dev Error emitted when less gas limit is provided for execution than expected */ @@ -127,7 +124,7 @@ contract MockSocket is ISocket { ISwitchboard switchboard__ ) internal view { // NOTE: is the the first un-trusted call in the system, another one is Plug.call - if (!switchboard__.allowPacket(digest_, payloadId_)) revert VerificationFailed(); + if (!switchboard__.allowPayload(digest_, payloadId_)) revert VerificationFailed(); } /** @@ -148,7 +145,7 @@ contract MockSocket is ISocket { /** * @dev Decodes the switchboard address from a given payload id. - * @param id_ The ID of the msg to decode the switchboard from. + * @param id_ The ID of the payload to decode the switchboard from. * @return switchboard_ The address of switchboard decoded from the payload ID. */ function _decodeSwitchboard(bytes32 id_) internal pure returns (address switchboard_) { @@ -156,9 +153,9 @@ contract MockSocket is ISocket { } /** - * @dev Decodes the chain ID from a given packet/payload ID. - * @param id_ The ID of the packet/msg to decode the chain slug from. - * @return chainSlug_ The chain slug decoded from the packet/payload ID. + * @dev Decodes the chain ID from a given payload ID. + * @param id_ The ID of the payload to decode the chain slug from. + * @return chainSlug_ The chain slug decoded from the payload ID. */ function _decodeChainSlug(bytes32 id_) internal pure returns (uint32 chainSlug_) { chainSlug_ = uint32(uint256(id_) >> 224); diff --git a/test/mock/MockWatcherPrecompile.sol b/test/mock/MockWatcherPrecompile.sol index 12c4c720..8710cafc 100644 --- a/test/mock/MockWatcherPrecompile.sol +++ b/test/mock/MockWatcherPrecompile.sol @@ -16,8 +16,6 @@ contract MockWatcherPrecompile { uint256 public maxTimeoutDelayInSeconds = 24 * 60 * 60; // 24 hours /// @notice Counter for tracking payload execution requests uint256 public payloadCounter; - /// @notice Counter for tracking timeout requests - uint256 public timeoutCounter; /// @notice Mapping to store timeout requests /// @dev timeoutId => TimeoutRequest struct mapping(bytes32 => TimeoutRequest) public timeoutRequests; @@ -26,7 +24,6 @@ contract MockWatcherPrecompile { /// @notice Error thrown when an invalid chain slug is provided error InvalidChainSlug(); - error InvalidTransmitter(); event CalledAppGateway(bytes32 triggerId); @@ -74,7 +71,7 @@ contract MockWatcherPrecompile { /// @param delayInSeconds_ The delay in seconds function setTimeout(bytes calldata payload_, uint256 delayInSeconds_) external { uint256 executeAt = block.timestamp + delayInSeconds_; - bytes32 timeoutId = _encodeTimeoutId(timeoutCounter++); + bytes32 timeoutId = _encodeTimeoutId(); timeoutRequests[timeoutId] = TimeoutRequest( timeoutId, msg.sender, @@ -177,9 +174,9 @@ contract MockWatcherPrecompile { ); } - function _encodeTimeoutId(uint256 timeoutCounter_) internal view returns (bytes32) { + function _encodeTimeoutId() internal returns (bytes32) { // watcher address (160 bits) | counter (64 bits) - return bytes32((uint256(uint160(address(this))) << 64) | timeoutCounter_); + return bytes32((uint256(uint160(address(this))) << 64) | payloadCounter++); } /// @notice Retrieves the configuration for a specific plug on a network diff --git a/test/mock/MockWatcherPrecompileImpl.sol b/test/mock/MockWatcherPrecompileImpl.sol index e888a9bb..567189f3 100644 --- a/test/mock/MockWatcherPrecompileImpl.sol +++ b/test/mock/MockWatcherPrecompileImpl.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import "../../contracts/protocol/watcherPrecompile/WatcherPrecompile.sol"; +import "../../contracts/protocol/watcherPrecompile/core/WatcherPrecompile.sol"; contract MockWatcherPrecompileImpl is WatcherPrecompile { // Mock function to test reinitialization with version 2