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
6 changes: 5 additions & 1 deletion contracts/protocol/SocketBatcher.sol
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,11 @@ contract SocketBatcher is ISocketBatcher, Ownable {
bytes calldata proof_
) external payable returns (bool, bytes memory) {
// Attest digest on FastSwitchboard
IFastSwitchboard(socket__.switchboardAddresses(switchboardId_)).attest(executionParams_.payloadId, digest_, proof_);
IFastSwitchboard(socket__.switchboardAddresses(switchboardId_)).attest(
executionParams_.payloadId,
digest_,
proof_
);
// Execute payload on socket
return socket__.execute{value: msg.value}(executionParams_, transmissionParams_);
}
Expand Down
140 changes: 80 additions & 60 deletions contracts/protocol/switchboard/EVMxSwitchboard.sol
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ contract EVMxSwitchboard is SwitchboardBase {
bytes payload
);

struct AssignTransmitterParams {
DigestParams digestParams;
address oldTransmitter;
address newTransmitter;
uint256 nonce;
bytes[] signatures; // must be totalWatchers length
}

// --- Constructor ---

constructor(
Expand All @@ -86,7 +94,6 @@ contract EVMxSwitchboard is SwitchboardBase {
}

// --- External Functions ---

/**
* @notice Attests a payload digest with watcher signature
* @param digest_ The digest of the payload to be executed
Expand All @@ -95,23 +102,52 @@ contract EVMxSwitchboard is SwitchboardBase {
* Payload is uniquely identified by digest. Once attested, payload can be executed.
*/
function attest(bytes32 payloadId_, bytes32 digest_, bytes calldata proof_) public virtual {
address watcher = _recoverSigner(
keccak256(
abi.encodePacked(toBytes32Format(address(this)), chainSlug, payloadId_, digest_)
),
proof_
bytes32 messageHash = keccak256(
abi.encodePacked(toBytes32Format(address(this)), chainSlug, payloadId_, digest_)
);
address watcher = _validateSignature(messageHash, proof_, WATCHER_ROLE);

_processAttestation(payloadId_, digest_, watcher);
}

/**
* @notice Batch attests a payload digest with multiple watcher signatures
* @param payloadId_ The payload ID to attest
* @param digest_ The digest of the payload to be executed
* @param proofs_ Array of watcher signature proofs
* @dev Processes multiple attestations in a single transaction to save gas.
* Reverts if any watcher is not authorized or has already attested.
* Can be called by any third party as authorization happens through signatures.
*/
function batchAttest(
bytes32 payloadId_,
bytes32 digest_,
bytes[] memory proofs_
) public virtual {
bytes32 messageHash = keccak256(
abi.encodePacked(toBytes32Format(address(this)), chainSlug, payloadId_, digest_)
);
if (!_hasRole(WATCHER_ROLE, watcher)) revert WatcherNotFound();
address[] memory watchers = _validateBatchSignatures(messageHash, proofs_, WATCHER_ROLE);

// Prevent double attestation
if (isAttestedByWatcher[watcher][payloadId_][digest_]) revert AlreadyAttested();
isAttestedByWatcher[watcher][payloadId_][digest_] = true;
for (uint256 i = 0; i < watchers.length; i++) {
_processAttestation(payloadId_, digest_, watchers[i]);
}
}

/**
* @notice Processes a single watcher attestation
* @param payloadId_ The payload ID
* @param digest_ The digest of the payload
* @param watcher_ The watcher address
* @dev Checks for double attestation, updates state, and emits event
*/
function _processAttestation(bytes32 payloadId_, bytes32 digest_, address watcher_) internal {
if (isAttestedByWatcher[watcher_][payloadId_][digest_]) revert AlreadyAttested();
isAttestedByWatcher[watcher_][payloadId_][digest_] = true;
attestations[payloadId_][digest_]++;

// Mark digest as valid if enough attestations are reached
if (attestations[payloadId_][digest_] >= totalWatchers) isValid[payloadId_][digest_] = true;

emit Attested(payloadId_, digest_, watcher);
emit Attested(payloadId_, digest_, watcher_);
}

/**
Expand Down Expand Up @@ -212,17 +248,20 @@ contract EVMxSwitchboard is SwitchboardBase {
}

/**
* @notice Sets reverting status for a payload
* @param payloadId_ The payload ID to mark
* @param isReverting_ True if payload should be marked as reverting
* @dev Only callable by owner. Used to mark payloads that are known to revert.
* @notice Sets reverting payload status with watcher signatures
* @param payloadId_ payload ID to mark
* @param isReverting_ reverting status flag
* @param nonce_ nonce to prevent replay attacks
* @param signatures_ watcher signature
* @dev Processes multiple payloads in a single transaction. Each payload requires exactly totalWatchers signatures.
*/
function setRevertingPayload(
bytes32 payloadId_,
bool isReverting_,
uint256 nonce_,
bytes calldata signature_
bytes[] memory signatures_
) external {
if (signatures_.length != totalWatchers) revert ArrayLengthMismatch();
bytes32 digest = keccak256(
abi.encodePacked(
toBytes32Format(address(this)),
Expand All @@ -233,54 +272,35 @@ contract EVMxSwitchboard is SwitchboardBase {
)
);

address watcher = _recoverSigner(digest, signature_);
if (!_hasRole(WATCHER_ROLE, watcher)) revert WatcherNotFound();
if (usedNonces[watcher][nonce_]) revert NonceAlreadyUsed();
usedNonces[watcher][nonce_] = true;
for (uint256 k = 0; k < totalWatchers; k++) {
address watcher = _validateSignature(digest, signatures_[k], WATCHER_ROLE);
_validateAndUseNonce(this.setRevertingPayload.selector, watcher, nonce_);
}

revertingPayloadIds[payloadId_] = isReverting_;
emit RevertingPayloadIdset(payloadId_, isReverting_);
emit RevertingPayloadIdSet(payloadId_, isReverting_);
}

/**
* @notice Gets the transmitter address for payload execution
* @param digestParams_ The digest parameters
* @param signature_ The watcher signature
* @dev Only callable by watcher. Used to assign the transmitter address for payload execution.
*/
function assignTransmitter(
DigestParams memory digestParams_,
address oldTransmitter_,
address newTransmitter_,
uint256 nonce_,
bytes calldata signature_
) external {
bytes32 oldTransmitterBytes32 = toBytes32Format(oldTransmitter_);
digestParams_.transmitter = oldTransmitterBytes32;
bytes32 oldDigest = createDigest(digestParams_);

if (payloadIdToDigest[digestParams_.payloadId] != oldDigest) revert InvalidDigest();

digestParams_.transmitter = toBytes32Format(newTransmitter_);
bytes32 newDigest = createDigest(digestParams_);

address watcher = _recoverSigner(
keccak256(
abi.encodePacked(
toBytes32Format(address(this)),
chainSlug,
oldDigest,
newDigest,
nonce_
)
),
signature_
function assignTransmitter(AssignTransmitterParams memory params_) external {
if (params_.signatures.length != totalWatchers) revert ArrayLengthMismatch();

DigestParams memory dp = params_.digestParams;
dp.transmitter = toBytes32Format(params_.oldTransmitter);
bytes32 oldDigest = createDigest(dp);
if (payloadIdToDigest[dp.payloadId] != oldDigest) revert InvalidDigest();

dp.transmitter = toBytes32Format(params_.newTransmitter);
bytes32 newDigest = createDigest(dp);
bytes32 messageHash = keccak256(
abi.encodePacked(toBytes32Format(address(this)), chainSlug, oldDigest, newDigest, params_.nonce)
);
if (!_hasRole(WATCHER_ROLE, watcher)) revert WatcherNotFound();
_validateAndUseNonce(this.assignTransmitter.selector, watcher, nonce_);
for (uint256 k = 0; k < totalWatchers; k++) {
address watcher = _validateSignature(messageHash, params_.signatures[k], WATCHER_ROLE);
_validateAndUseNonce(this.assignTransmitter.selector, watcher, params_.nonce);
}

payloadIdToDigest[digestParams_.payloadId] = newDigest;
emit TransmitterAssigned(digestParams_.payloadId, newTransmitter_);
payloadIdToDigest[dp.payloadId] = newDigest;
emit TransmitterAssigned(dp.payloadId, params_.newTransmitter);
}
Comment on lines 284 to 304
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

assignTransmitter should also guard against totalWatchers == 0

assignTransmitter has the same pattern as setRevertingPayload: length check then a loop over totalWatchers. When totalWatchers == 0, an empty signatures array will:

  • pass the length check,
  • skip the loop (no _validateSignature / _validateAndUseNonce),
  • and still update payloadIdToDigest plus emit TransmitterAssigned.

That effectively allows unauthenticated transmitter reassignment in a “no watchers configured” state. This should be guarded the same way:

function assignTransmitter(AssignTransmitterParams memory params_) external {
-    if (params_.signatures.length != totalWatchers) revert ArrayLengthMismatch();
+    if (totalWatchers == 0) revert WatcherNotFound();
+    if (params_.signatures.length != totalWatchers) revert ArrayLengthMismatch();
    ...
}

This keeps the semantics “no watcher set => no multisig operations” instead of “no watcher set => zero-signature operations”.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function assignTransmitter(AssignTransmitterParams memory params_) external {
if (params_.signatures.length != totalWatchers) revert ArrayLengthMismatch();
DigestParams memory dp = params_.digestParams;
dp.transmitter = toBytes32Format(params_.oldTransmitter);
bytes32 oldDigest = createDigest(dp);
if (payloadIdToDigest[dp.payloadId] != oldDigest) revert InvalidDigest();
dp.transmitter = toBytes32Format(params_.newTransmitter);
bytes32 newDigest = createDigest(dp);
bytes32 messageHash = keccak256(
abi.encodePacked(toBytes32Format(address(this)), chainSlug, oldDigest, newDigest)
);
if (!_hasRole(WATCHER_ROLE, watcher)) revert WatcherNotFound();
_validateAndUseNonce(this.assignTransmitter.selector, watcher, nonce_);
for (uint256 k = 0; k < totalWatchers; k++) {
address watcher = _validateSignature(messageHash, params_.signatures[k], WATCHER_ROLE);
_validateAndUseNonce(this.assignTransmitter.selector, watcher, params_.nonce);
}
payloadIdToDigest[digestParams_.payloadId] = newDigest;
emit TransmitterAssigned(digestParams_.payloadId, newTransmitter_);
payloadIdToDigest[dp.payloadId] = newDigest;
emit TransmitterAssigned(dp.payloadId, params_.newTransmitter);
}
function assignTransmitter(AssignTransmitterParams memory params_) external {
if (totalWatchers == 0) revert WatcherNotFound();
if (params_.signatures.length != totalWatchers) revert ArrayLengthMismatch();
DigestParams memory dp = params_.digestParams;
dp.transmitter = toBytes32Format(params_.oldTransmitter);
bytes32 oldDigest = createDigest(dp);
if (payloadIdToDigest[dp.payloadId] != oldDigest) revert InvalidDigest();
dp.transmitter = toBytes32Format(params_.newTransmitter);
bytes32 newDigest = createDigest(dp);
bytes32 messageHash = keccak256(
abi.encodePacked(toBytes32Format(address(this)), chainSlug, oldDigest, newDigest)
);
for (uint256 k = 0; k < totalWatchers; k++) {
address watcher = _validateSignature(messageHash, params_.signatures[k], WATCHER_ROLE);
_validateAndUseNonce(this.assignTransmitter.selector, watcher, params_.nonce);
}
payloadIdToDigest[dp.payloadId] = newDigest;
emit TransmitterAssigned(dp.payloadId, params_.newTransmitter);
}
🤖 Prompt for AI Agents
In contracts/protocol/switchboard/EVMxSwitchboard.sol around lines 279 to 299,
add a guard that reverts when totalWatchers == 0 before allowing assignment to
prevent unauthenticated zero-signature operations; specifically,
require(totalWatchers > 0) (or revert with an appropriate error) immediately
before/alongside the existing signatures length check so an empty signatures
array cannot pass when no watchers are configured, ensuring the function always
validates signatures/nonces when watchers exist and denies multisig operations
when none are set.


function markIsValid(bytes32 payloadId_, bytes32 digest_) external {
Expand Down
Loading