Batch Submitter Reverts: Empty Seal Vulnerability Explained

by Hugo van Dijk 60 views

Hey guys! Let's dive into this Soft Black Seahorse issue where calls in the batch submitter can revert. This is a sherlock-audit finding from 2025-07-malda-judging, categorized as a Medium severity issue. We're going to break down what's happening, why it's a problem, and how it can be fixed. So, buckle up!

Calls in Batch Submitter Will Revert

Summary

The proof forwarder has the capability to initiate multiple transactions by submitting valid journal data and seal data for verification. However, a critical flaw exists: these calls will revert. This means that the intended functionality of processing multiple transactions in a batch is broken, leading to a denial of service.

Root Cause

To understand why this happens, let's look at the code snippet from the BatchSubmitter.sol contract:

function batchProcess(BatchProcessMsg calldata data) external {
 if (!rolesOperator.isAllowedFor(msg.sender, rolesOperator.PROOF_FORWARDER())) {
 revert BatchSubmitter_CallerNotAllowed();
 }

 _verifyProof(data.journalData, data.seal);

 bytes[] memory journals = abi.decode(data.journalData, (bytes[]));

 ...
 try ImErc20Host(data.mTokens[i]).mintExternal(
 encodedJournal, "", singleAmount, singleMinAmounts, data.receivers[i]
 ) {
 ...
 } else if (selector == REPAY_SELECTOR) {
 try ImErc20Host(data.mTokens[i]).repayExternal(encodedJournal, "", singleAmount, data.receivers[i]) {
 ...
 } else if (selector == OUT_HERE_SELECTOR) {
 try ImTokenGateway(data.mTokens[i]).outHere(encodedJournal, "", singleAmount, data.receivers[i]) {
 ...
 }

In this batchProcess function, the proof forwarder is expected to provide necessary message data such as journal data, seal, and receivers. The proof is then verified using the _verifyProof function. The core issue lies in how the seal parameter is used in the subsequent external function calls. Specifically, when functions with “external verification” are called (like mintExternal, repayExternal, or outHere), the seal parameter is passed as an empty byte string "". Let's dig deeper into why this is a problem.

Looking at the mintExternal function in ImErc20Host.sol:

function mintExternal(
 bytes calldata journalData,
 bytes calldata seal,
 uint256[] calldata mintAmount,
 uint256[] calldata minAmountsOut,
 address receiver
 ) external override {
 if (!_isAllowedFor(msg.sender, _getBatchProofForwarderRole())) {
 _verifyProof(journalData, seal);
 }

function _verifyProof(bytes calldata journalData, bytes calldata seal) internal view {
 ...
 // verify it using the IZkVerifier contract
 verifier.verifyInput(journalData, seal);
}

function verifyInput(bytes calldata journalEntry, bytes calldata seal) external view {
 ...
 // verify input
 __verify(journalEntry, seal);
}

function __verify(bytes calldata journalEntry, bytes calldata seal) private view {
 verifier.verify(seal, imageId, sha256(journalEntry));
}

The _verifyProof function calls verifier.verifyInput, which eventually leads to a call to the verifier's verify function. Now, let's examine the verify function in RiscZeroGroth16Verifier.sol:

function verify(bytes calldata seal, bytes32 imageId, bytes32 journalDigest) external view {
 _verifyIntegrity(seal, ReceiptClaimLib.ok(imageId, journalDigest).digest());
}

/// @notice internal implementation of verifyIntegrity, factored to avoid copying calldata bytes to memory.
function _verifyIntegrity(bytes calldata seal, bytes32 claimDigest) internal view {
 // Check that the seal has a matching selector. Mismatch generally indicates that the
 // prover and this verifier are using different parameters, and so the verification
 // will not succeed.
 if (SELECTOR != bytes4(seal[:4])) {
 revert SelectorMismatch({received: bytes4(seal[:4]), expected: SELECTOR});
 }

This is where the major issue comes to light. The verify function attempts to extract the first 4 bytes from the seal parameter to check against a selector. However, since the seal is an empty byte string (""), this operation will fail and revert due to an out-of-bounds access. The code tries to read the first four bytes from an empty byte array, which is a recipe for disaster. This means any batch submission using an empty seal will always revert, effectively halting the batch processing functionality.

Internal Pre-conditions

There are no specific internal pre-conditions identified for this issue.

External Pre-conditions

There are no specific external pre-conditions identified for this issue. The vulnerability is inherent in the code logic itself.

Attack Path

The attack path is straightforward: a proof forwarder can call the batchProcess function with a seal value set to an empty byte string. This will cause the transaction to revert within the verifier's verify function due to the out-of-bounds access when trying to read the first 4 bytes of the empty seal.

Impact

The impact of this vulnerability is a Denial of Service (DoS). By submitting a batch process with an empty seal, an attacker can prevent any batch transactions from being processed. This can stall critical operations and disrupt the normal functioning of the lending platform. Think of it like trying to start a car without a key – it’s just not going to happen!

PoC

Unfortunately, no Proof of Concept (PoC) was provided in the original audit finding. However, the root cause analysis clearly demonstrates how the vulnerability can be triggered.

Mitigation

The mitigation for this issue is relatively simple but crucial. The seal parameter passed to the external verification functions (mintExternal, repayExternal, outHere) in the BatchSubmitter.sol contract should not be an empty byte string. The correct seal data needs to be passed to ensure that the verification process can proceed without errors. This might involve ensuring that the proof generation process always produces a valid seal or modifying the batch processing logic to handle cases where the seal is missing or invalid. It’s like making sure you always have the right key before you try to start the car!

Improving Our Understanding: Keywords and Rephrasing

Key Terms and Concepts

Let's clarify some key terms related to this vulnerability:

  • Batch Submitter: The contract or component responsible for processing multiple transactions in a single batch.
  • Proof Forwarder: An entity or role authorized to submit proofs for verification.
  • Seal: Cryptographic data used to verify the integrity and authenticity of a transaction or a set of transactions.
  • Journal Data: Data related to the transaction, often including logs or state changes.
  • Verifier: A contract or component responsible for verifying the validity of a proof.
  • Denial of Service (DoS): An attack that prevents legitimate users from accessing a service or system.
  • Out-of-Bounds Access: An error that occurs when a program tries to access memory outside the bounds of an array or other data structure.

Clarifying the Core Problem

To make sure we're all on the same page, let's rephrase the central issue:

Original: Calls in batch submitter will revert.

Revised: Why do transactions in the batch submitter revert when an empty seal is used?

This revised question is much more direct and helps to focus on the root cause of the problem. It encourages a deeper understanding of why the reverts occur, rather than just stating the fact that they do. We need to understand the "why" behind the problem to truly grasp its impact and how to fix it.

Diving Deeper into the Code

To further illustrate the issue, let's break down the critical parts of the code flow:

  1. batchProcess Function: The proof forwarder calls this function to submit a batch of transactions. This function receives the journalData and seal as input.
  2. _verifyProof Function: This internal function is called to verify the proof. It takes the journalData and seal as parameters.
  3. verifier.verifyInput Function: This function is part of the IZkVerifier contract and is responsible for verifying the proof's integrity. It receives the journalEntry and seal.
  4. verifier.verify Function: This is the critical function where the vulnerability lies. It attempts to read the first 4 bytes of the seal to check the selector. If the seal is empty, this operation will cause a revert.
  5. _verifyIntegrity Function: This internal function within the verifier checks if the selector matches the expected value. If the seal is empty, the bytes4(seal[:4]) operation will trigger an out-of-bounds access and cause the transaction to revert.

By tracing this flow, we can see precisely where and why the vulnerability occurs. The empty seal is the key culprit, leading to the out-of-bounds access in the verifier's verify function. This detailed understanding is essential for developing a robust mitigation strategy.

The Broader Context: Security Implications

Understanding this vulnerability is not just about fixing a single bug; it’s about understanding the broader security implications. In this case, the vulnerability allows an attacker to cause a Denial of Service. This is a significant concern because it can prevent legitimate users from using the platform. Imagine a scenario where the attacker repeatedly submits batch transactions with empty seals, effectively freezing the system and preventing any valid transactions from being processed.

The consequences of a DoS attack can be severe. It can disrupt normal operations, lead to financial losses, and damage the reputation of the platform. Therefore, it’s crucial to address this vulnerability promptly and effectively. This highlights the importance of thorough code audits and security reviews to identify and mitigate potential vulnerabilities before they can be exploited.

Long-Term Prevention: Best Practices

Beyond fixing this specific issue, it’s important to consider long-term prevention strategies. Here are some best practices to help prevent similar vulnerabilities in the future:

  1. Input Validation: Always validate input data to ensure it meets the expected format and constraints. In this case, the seal parameter should be checked to ensure it is not empty before being passed to the verifier.
  2. Defensive Coding: Write code that anticipates potential errors and handles them gracefully. For example, the verifier could check the length of the seal before attempting to read its bytes.
  3. Code Audits: Conduct regular code audits and security reviews to identify potential vulnerabilities. This helps to catch issues early in the development process, before they can be exploited.
  4. Fuzzing and Testing: Use fuzzing and other testing techniques to uncover edge cases and unexpected behavior. This can help to identify vulnerabilities that might not be apparent through manual code review.
  5. Formal Verification: Consider using formal verification techniques to mathematically prove the correctness of your code. This can provide a high degree of assurance that your code is free from vulnerabilities.

By implementing these best practices, you can significantly reduce the risk of security vulnerabilities in your smart contracts and ensure the long-term security and reliability of your platform. Remember, security is not just a one-time fix; it’s an ongoing process that requires constant vigilance and attention.

In conclusion, the "Soft Black Seahorse" vulnerability highlights the importance of careful input validation and defensive coding practices. By understanding the root cause of the issue and implementing appropriate mitigations, we can protect our systems from Denial of Service attacks and ensure the continued operation of our platforms. Keep those seals valid, guys!