Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 23 additions & 4 deletions contracts/interfaces/IWatcherPrecompile.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions contracts/protocol/utils/common/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,5 @@ error InvalidIndex();
error InvalidTransmitter();
error FeesNotSet();
error InvalidTokenAddress();
error InvalidWatcherSignature();
error NonceUsed();
66 changes: 53 additions & 13 deletions contracts/protocol/watcherPrecompile/WatcherPrecompile.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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_);
}
Expand All @@ -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];
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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])
Expand Down
33 changes: 32 additions & 1 deletion contracts/protocol/watcherPrecompile/WatcherPrecompileConfig.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;

Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;

Expand Down
4 changes: 1 addition & 3 deletions script/counter-inbox/Increment.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 0 additions & 1 deletion script/parallel-counter/incrementCounters.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
19 changes: 13 additions & 6 deletions test/DeliveryHelper.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
13 changes: 9 additions & 4 deletions test/Inbox.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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,
Expand All @@ -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");
}
Expand Down
26 changes: 24 additions & 2 deletions test/SetupTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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(
Expand Down
Loading