diff --git a/contracts/interfaces/IWatcherPrecompile.sol b/contracts/interfaces/IWatcherPrecompile.sol index 1e6f510b..0a54fa6a 100644 --- a/contracts/interfaces/IWatcherPrecompile.sol +++ b/contracts/interfaces/IWatcherPrecompile.sol @@ -9,8 +9,14 @@ import {PayloadDetails, AsyncRequest, FinalizeParams, PayloadDigestParams, AppGa interface IWatcherPrecompile { /// @notice Sets up app gateway configurations /// @param configs_ Array of app gateway configurations + /// @param signatureNonce_ The nonce of the signature + /// @param signature_ The signature of the watcher /// @dev Only callable by authorized addresses - function setAppGateways(AppGatewayConfig[] calldata configs_) external; + function setAppGateways( + AppGatewayConfig[] calldata configs_, + uint256 signatureNonce_, + bytes calldata signature_ + ) external; /// @notice Sets up on-chain contract configurations /// @dev Only callable by authorized addresses @@ -59,7 +65,12 @@ interface IWatcherPrecompile { /// @notice Marks a request as finalized with a proof /// @param payloadId_ The unique identifier of the request /// @param proof_ The watcher's proof - function finalized(bytes32 payloadId_, bytes calldata proof_) external; + function finalized( + bytes32 payloadId_, + bytes calldata proof_, + uint256 signatureNonce_, + bytes calldata signature_ + ) external; /// @notice Finalizes multiple payload execution requests with a new transmitter /// @param payloadId_ The unique identifier of the request @@ -68,7 +79,11 @@ interface IWatcherPrecompile { /// @notice Resolves multiple promises with their return data /// @param resolvedPromises_ Array of resolved promises and their return data - function resolvePromises(ResolvedPromises[] calldata resolvedPromises_) external; + function resolvePromises( + ResolvedPromises[] calldata resolvedPromises_, + uint256 signatureNonce_, + bytes calldata signature_ + ) external; /// @notice Sets a timeout for payload execution /// @param payload_ The payload data @@ -81,7 +96,11 @@ interface IWatcherPrecompile { /// @notice Resolves a timeout by executing the payload /// @param timeoutId_ The unique identifier for the timeout - function resolveTimeout(bytes32 timeoutId_) external; + function resolveTimeout( + bytes32 timeoutId_, + uint256 signatureNonce_, + bytes calldata signature_ + ) external; /// @notice Calculates the Digest hash for payload parameters /// @param params_ The payload parameters used to calculate the digest diff --git a/contracts/protocol/utils/common/Errors.sol b/contracts/protocol/utils/common/Errors.sol index 11c15386..f129a2a2 100644 --- a/contracts/protocol/utils/common/Errors.sol +++ b/contracts/protocol/utils/common/Errors.sol @@ -29,3 +29,5 @@ error InvalidIndex(); error InvalidTransmitter(); error FeesNotSet(); error InvalidTokenAddress(); +error InvalidWatcherSignature(); +error NonceUsed(); diff --git a/contracts/protocol/watcherPrecompile/WatcherPrecompile.sol b/contracts/protocol/watcherPrecompile/WatcherPrecompile.sol index 576d3c31..966e5342 100644 --- a/contracts/protocol/watcherPrecompile/WatcherPrecompile.sol +++ b/contracts/protocol/watcherPrecompile/WatcherPrecompile.sol @@ -7,7 +7,6 @@ import "../../interfaces/IPromise.sol"; import "../../interfaces/IFeesManager.sol"; import "solady/utils/Initializable.sol"; import {PayloadDigestParams, AsyncRequest, FinalizeParams, TimeoutRequest, CallFromChainParams} from "../utils/common/Structs.sol"; -import {TimeoutDelayTooLarge, TimeoutAlreadyResolved, InvalidInboxCaller, ResolvingTimeoutTooEarly, CallFailed, AppGatewayAlreadyCalled} from "../utils/common/Errors.sol"; /// @title WatcherPrecompile /// @notice Contract that handles payload verification, execution and app configurations @@ -18,9 +17,6 @@ contract WatcherPrecompile is WatcherPrecompileConfig, Initializable { /// @notice The expiry time for the payload uint256 public expiryTime; - /// @notice The chain slug of the watcher precompile - uint32 public evmxSlug; - /// @notice Mapping to store async requests /// @dev payloadId => AsyncRequest struct mapping(bytes32 => AsyncRequest) public asyncRequests; @@ -153,9 +149,18 @@ contract WatcherPrecompile is WatcherPrecompileConfig, Initializable { /// @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 - function resolveTimeout(bytes32 timeoutId_) external onlyRole(WATCHER_ROLE) { - TimeoutRequest storage timeoutRequest_ = timeoutRequests[timeoutId_]; + function resolveTimeout( + bytes32 timeoutId_, + uint256 signatureNonce_, + bytes calldata signature_ + ) external { + _isWatcherSignatureValid( + abi.encode(this.resolveTimeout.selector, timeoutId_), + signatureNonce_, + signature_ + ); + TimeoutRequest storage timeoutRequest_ = timeoutRequests[timeoutId_]; if (timeoutRequest_.target == address(0)) revert InvalidTimeoutRequest(); if (timeoutRequest_.isResolved) revert TimeoutAlreadyResolved(); if (block.timestamp < timeoutRequest_.executeAt) revert ResolvingTimeoutTooEarly(); @@ -300,7 +305,18 @@ contract WatcherPrecompile is WatcherPrecompileConfig, Initializable { /// @dev Only callable by the contract owner /// @dev Watcher signs on following digest for validation on switchboard: /// @dev keccak256(abi.encode(switchboard, digest)) - function finalized(bytes32 payloadId_, bytes calldata proof_) external onlyRole(WATCHER_ROLE) { + function finalized( + bytes32 payloadId_, + bytes calldata proof_, + uint256 signatureNonce_, + bytes calldata signature_ + ) external { + _isWatcherSignatureValid( + abi.encode(this.finalized.selector, payloadId_, proof_), + signatureNonce_, + signature_ + ); + watcherProofs[payloadId_] = proof_; emit Finalized(payloadId_, asyncRequests[payloadId_], proof_); } @@ -309,8 +325,16 @@ contract WatcherPrecompile is WatcherPrecompileConfig, Initializable { /// @param resolvedPromises_ Array of resolved promises and their return data /// @dev Only callable by the contract owner function resolvePromises( - ResolvedPromises[] calldata resolvedPromises_ - ) external onlyRole(WATCHER_ROLE) { + ResolvedPromises[] calldata resolvedPromises_, + uint256 signatureNonce_, + bytes calldata signature_ + ) external { + _isWatcherSignatureValid( + abi.encode(this.resolvePromises.selector, resolvedPromises_), + signatureNonce_, + signature_ + ); + for (uint256 i = 0; i < resolvedPromises_.length; i++) { // Get the array of promise addresses for this payload AsyncRequest memory asyncRequest_ = asyncRequests[resolvedPromises_[i].payloadId]; @@ -338,9 +362,17 @@ contract WatcherPrecompile is WatcherPrecompileConfig, Initializable { // wait till expiry time to assign fees function markRevert( + bool isRevertingOnchain_, bytes32 payloadId_, - bool isRevertingOnchain_ - ) external onlyRole(WATCHER_ROLE) { + uint256 signatureNonce_, + bytes calldata signature_ + ) external { + _isWatcherSignatureValid( + abi.encode(this.markRevert.selector, isRevertingOnchain_, payloadId_), + signatureNonce_, + signature_ + ); + AsyncRequest memory asyncRequest_ = asyncRequests[payloadId_]; address[] memory next = asyncRequest_.next; @@ -384,8 +416,16 @@ contract WatcherPrecompile is WatcherPrecompileConfig, Initializable { // ================== On-Chain Inbox ================== function callAppGateways( - CallFromChainParams[] calldata params_ - ) external onlyRole(WATCHER_ROLE) { + CallFromChainParams[] calldata params_, + uint256 signatureNonce_, + bytes calldata signature_ + ) external { + _isWatcherSignatureValid( + abi.encode(this.callAppGateways.selector, params_), + signatureNonce_, + signature_ + ); + for (uint256 i = 0; i < params_.length; i++) { if (appGatewayCalled[params_[i].callId]) revert AppGatewayAlreadyCalled(); if (!isValidPlug[params_[i].appGateway][params_[i].chainSlug][params_[i].plug]) diff --git a/contracts/protocol/watcherPrecompile/WatcherPrecompileConfig.sol b/contracts/protocol/watcherPrecompile/WatcherPrecompileConfig.sol index 89928c7d..0f855adb 100644 --- a/contracts/protocol/watcherPrecompile/WatcherPrecompileConfig.sol +++ b/contracts/protocol/watcherPrecompile/WatcherPrecompileConfig.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.21; import "./WatcherPrecompileLimits.sol"; +import {ECDSA} from "solady/utils/ECDSA.sol"; /// @title WatcherPrecompileConfig /// @notice Configuration contract for the Watcher Precompile system @@ -27,6 +28,10 @@ abstract contract WatcherPrecompileConfig is WatcherPrecompileLimits { /// @dev chainSlug => fees plug address mapping(uint32 => address) public feesPlug; + /// @notice Maps nonce to whether it has been used + /// @dev signatureNonce => isValid + mapping(uint256 => bool) public isNonceUsed; + // appGateway => chainSlug => plug => isValid mapping(address => mapping(uint32 => mapping(address => bool))) public isValidPlug; @@ -62,7 +67,17 @@ abstract contract WatcherPrecompileConfig is WatcherPrecompileLimits { /// @param configs_ Array of configurations containing app gateway, network, plug, and switchboard details /// @dev Only callable by the contract owner /// @dev This helps in verifying that plugs are called by respective app gateways - function setAppGateways(AppGatewayConfig[] calldata configs_) external onlyRole(WATCHER_ROLE) { + function setAppGateways( + AppGatewayConfig[] calldata configs_, + uint256 signatureNonce_, + bytes calldata signature_ + ) external { + _isWatcherSignatureValid( + abi.encode(this.setAppGateways.selector, configs_), + signatureNonce_, + signature_ + ); + for (uint256 i = 0; i < configs_.length; i++) { // Store the plug configuration for this network and plug _plugConfigs[configs_[i].chainSlug][configs_[i].plug] = PlugConfig({ @@ -120,5 +135,21 @@ abstract contract WatcherPrecompileConfig is WatcherPrecompileLimits { ); } + function _isWatcherSignatureValid( + bytes memory digest_, + uint256 signatureNonce_, + bytes memory signature_ + ) internal { + if (isNonceUsed[signatureNonce_]) revert NonceUsed(); + isNonceUsed[signatureNonce_] = true; + + bytes32 digest = keccak256(abi.encode(address(this), evmxSlug, signatureNonce_, digest_)); + digest = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest)); + + // recovered signer is checked for the valid roles later + address signer = ECDSA.recover(digest, signature_); + if (signer != owner()) revert InvalidWatcherSignature(); + } + uint256[49] __gap_config; } diff --git a/contracts/protocol/watcherPrecompile/WatcherPrecompileLimits.sol b/contracts/protocol/watcherPrecompile/WatcherPrecompileLimits.sol index d5acb4a2..8bc10bc9 100644 --- a/contracts/protocol/watcherPrecompile/WatcherPrecompileLimits.sol +++ b/contracts/protocol/watcherPrecompile/WatcherPrecompileLimits.sol @@ -8,6 +8,7 @@ import {AddressResolverUtil} from "../utils/AddressResolverUtil.sol"; import {QUERY, FINALIZE, SCHEDULE} from "../utils/common/Constants.sol"; import "../../interfaces/IWatcherPrecompile.sol"; import {WATCHER_ROLE} from "../utils/common/AccessRoles.sol"; +import {TimeoutDelayTooLarge, TimeoutAlreadyResolved, InvalidInboxCaller, ResolvingTimeoutTooEarly, CallFailed, AppGatewayAlreadyCalled, InvalidWatcherSignature, NonceUsed} from "../utils/common/Errors.sol"; abstract contract WatcherPrecompileLimits is Gauge, @@ -22,6 +23,10 @@ abstract contract WatcherPrecompileLimits is uint256 public defaultLimit; /// @notice Rate at which limit replenishes per second uint256 public defaultRatePerSecond; + + /// @notice The chain slug of the watcher precompile + uint32 public evmxSlug; + // appGateway => limitType => receivingLimitParams mapping(address => mapping(bytes32 => LimitParams)) internal _limitParams; diff --git a/script/counter-inbox/Increment.s.sol b/script/counter-inbox/Increment.s.sol index 37b56733..95477943 100644 --- a/script/counter-inbox/Increment.s.sol +++ b/script/counter-inbox/Increment.s.sol @@ -10,10 +10,8 @@ import {ETH_ADDRESS, FAST} from "../../contracts/protocol/utils/common/Constants contract Increment is Script { function run() external { - address gateway = vm.envAddress("APP_GATEWAY"); - address socket = vm.envAddress("SOCKET"); - address switchboard = vm.envAddress("SWITCHBOARD"); string memory arbRpc = vm.envString("ARBITRUM_SEPOLIA_RPC"); + vm.createSelectFork(arbRpc); uint256 arbDeployerPrivateKey = vm.envUint("SPONSOR_KEY"); vm.startBroadcast(arbDeployerPrivateKey); diff --git a/script/parallel-counter/incrementCounters.s.sol b/script/parallel-counter/incrementCounters.s.sol index 70036545..5c4fc7e5 100644 --- a/script/parallel-counter/incrementCounters.s.sol +++ b/script/parallel-counter/incrementCounters.s.sol @@ -9,7 +9,6 @@ import {CounterAppGateway} from "../../contracts/apps//counter/CounterAppGateway contract IncrementCounters is Script { function run() external { string memory socketRPC = vm.envString("EVMX_RPC"); - uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); vm.createSelectFork(socketRPC); diff --git a/test/DeliveryHelper.t.sol b/test/DeliveryHelper.t.sol index 04a0fc3e..74aa39a9 100644 --- a/test/DeliveryHelper.t.sol +++ b/test/DeliveryHelper.t.sol @@ -161,8 +161,11 @@ contract DeliveryHelperTest is SetupTest { switchboard: address(optConfig.switchboard) }); - hoax(watcherEOA); - watcherPrecompile.setAppGateways(gateways); + bytes memory watcherSignature = _createWatcherSignature( + abi.encode(IWatcherPrecompile.setAppGateways.selector, gateways) + ); + + watcherPrecompile.setAppGateways(gateways, signatureNonce++, watcherSignature); } function setLimit(address appGateway_) internal { @@ -351,8 +354,10 @@ contract DeliveryHelperTest is SetupTest { }); } - hoax(watcherEOA); - watcherPrecompile.setAppGateways(gateways); + bytes memory watcherSignature = _createWatcherSignature( + abi.encode(IWatcherPrecompile.setAppGateways.selector, gateways) + ); + watcherPrecompile.setAppGateways(gateways, signatureNonce++, watcherSignature); } function _executeReadBatchSingleChain( @@ -487,8 +492,10 @@ contract DeliveryHelperTest is SetupTest { if (auctionEndDelaySeconds == 0) return; bytes32 timeoutId = _encodeId(evmxSlug, address(watcherPrecompile), payloadIdCounter++); - hoax(watcherEOA); - watcherPrecompile.resolveTimeout(timeoutId); + bytes memory watcherSignature = _createWatcherSignature( + abi.encode(IWatcherPrecompile.resolveTimeout.selector, timeoutId) + ); + watcherPrecompile.resolveTimeout(timeoutId, signatureNonce++, watcherSignature); } function finalize( diff --git a/test/Inbox.t.sol b/test/Inbox.t.sol index 9072689d..9e8da63d 100644 --- a/test/Inbox.t.sol +++ b/test/Inbox.t.sol @@ -42,8 +42,10 @@ contract InboxTest is DeliveryHelperTest { switchboard: address(arbConfig.switchboard) }); - hoax(watcherEOA); - watcherPrecompile.setAppGateways(gateways); + bytes memory watcherSignature = _createWatcherSignature( + abi.encode(IWatcherPrecompile.setAppGateways.selector, gateways) + ); + watcherPrecompile.setAppGateways(gateways, signatureNonce++, watcherSignature); hoax(watcherEOA); watcherPrecompile.setIsValidPlug(arbChainSlug, address(inbox), true); @@ -58,7 +60,6 @@ contract InboxTest is DeliveryHelperTest { bytes32 callId = inbox.increaseOnGateway(incrementValue); - hoax(watcherEOA); CallFromChainParams[] memory params = new CallFromChainParams[](1); params[0] = CallFromChainParams({ callId: callId, @@ -68,7 +69,11 @@ contract InboxTest is DeliveryHelperTest { payload: abi.encode(incrementValue), params: bytes32(0) }); - watcherPrecompile.callAppGateways(params); + + bytes memory watcherSignature = _createWatcherSignature( + abi.encode(WatcherPrecompile.callAppGateways.selector, params) + ); + watcherPrecompile.callAppGateways(params, signatureNonce++, watcherSignature); // Check counter was incremented assertEq(gateway.counter(), incrementValue, "Gateway counter should be incremented"); } diff --git a/test/SetupTest.t.sol b/test/SetupTest.t.sol index 65594d3c..8103bbd6 100644 --- a/test/SetupTest.t.sol +++ b/test/SetupTest.t.sol @@ -35,6 +35,7 @@ contract SetupTest is Test { uint32 evmxSlug = 1; uint256 expiryTime = 10000000; + uint256 public signatureNonce = 0; uint256 public payloadIdCounter = 0; uint256 public defaultLimit = 1000; @@ -215,8 +216,29 @@ contract SetupTest is Test { returnDatas[0] = returnData; resolvedPromises[0] = ResolvedPromises({payloadId: payloadId, returnData: returnDatas}); - vm.prank(watcherEOA); - watcherPrecompile.resolvePromises(resolvedPromises); + + bytes memory watcherSignature = _createWatcherSignature( + abi.encode(WatcherPrecompile.resolvePromises.selector, resolvedPromises) + ); + watcherPrecompile.resolvePromises(resolvedPromises, signatureNonce++, watcherSignature); + } + + function _createWatcherSignature( + bytes memory params_ + ) internal view returns (bytes memory sig) { + bytes32 digest = keccak256( + abi.encode(address(watcherPrecompile), evmxSlug, signatureNonce, params_) + ); + digest = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest)); + (uint8 sigV, bytes32 sigR, bytes32 sigS) = vm.sign(watcherPrivateKey, digest); + sig = new bytes(65); + bytes1 v32 = bytes1(sigV); + + assembly { + mstore(add(sig, 96), v32) + mstore(add(sig, 32), sigR) + mstore(add(sig, 64), sigS) + } } function getWritePayloadId(