Independent Relayer vs. Validator State Reconciliation: Detecting Phantom Deposits Before Minting

In January 2022, Qubit Finance’s cross-chain lending protocol suffered an attack that caused more than $80 million in losses. Postmortems from SlowMist and PeckShield exposed a troubling failure for smart-contract developers:

The attacker exploited insufficient revert handling for transfers to address(0) in Qubit’s deposit contract and submitted fake deposits to the source-chain contract. Its internal safeTransferFrom failed in practice, or transferred no real assets at all. A logic flaw nevertheless allowed the contract’s final line to emit a standard Deposit(address, address, uint256) event.

The off-chain relayer used a textbook event-driven architecture. As soon as it received the Deposit event over WebSocket, it assumed that the user had fully funded the source-chain deposit. Within milliseconds it called the destination minting contract and created 77,162 qXETH for the attacker without backing.

Without spending anything on the source chain, the attacker used fabricated event logs to withdraw $80 million in real assets on the destination chain.

This critical flaw—relayers trusting event logs without verifying actual asset increases—is called a phantom deposit attack. In cross-chain infrastructure, an event log is a notification, never the final authority for asset accounting. Drawing on the production-grade practices in Passage Bridge, this article explains how to build an independent relayer and vault balance-invariant cross-reconciliation engine.


1. Why Listening Only to Event Logs Is Dangerous

Independent Relayer vs. Validator State Reconciliation: Detecting Phantom Deposits Before Minting

In traditional Web2 systems, message queues such as Kafka and RabbitMQ are often trusted infrastructure. In public distributed ledgers and the EVM, however, event logs have three significant avenues for deception:

+----------------------------------------------------------------------------------------------------+
|                              How Phantom Deposit Vulnerabilities Arise                             |
+----------------------------------------------------------------------------------------------------+

   [ On-Chain Exploit Contract ]                         [ Vulnerable Off-Chain Relayer ]
                 │                                                          │
   (1) Call deposit() with abnormal arguments                                │
       - Supply a fake token address with the same name                      │
       - Or swallow errors through internal delegatecall/fallback            │
       - Actual increase in vault assets: 0                                  │
                 │                                                          │
   (2) A contract logic flaw allows an event to fire:                         │
       emit Deposit(User, 100,000 USDC)                                     │
                 │                                                          │
                 ├──────────────── WebSocket Event Log push ───────────────>│
                 │                                                          │
                 │                                            (3) Only parse ABI and assemble signatures
                 │                                                - Source contract address matches [PASS]
                 │                                                - Destination address format valid [PASS]
                 │                                                - Assume the user actually paid!
                 │                                                          │
                 │                                            (4) Broadcast mint() to destination
                 │                                                          │
                 ▼                                                          ▼
   [ Source vault reserves: no funds received ]                  [ Destination vault: mint 100,000 unbacked USDC ]
+----------------------------------------------------------------------------------------------------+
  1. Emitting an event does not mean state changed successfully (Event Decoupling): In complex DeFi compositions or proxy upgrades, an intermediate contract may catch a lower-level revert without propagating it, or still dispatch the standard completion event inside a catch block.
  2. Same-name fake tokens and fraudulent transfers (Token Spoofing): If a source vault supports multiple staked assets but logs only amount, and token-address validation has a backdoor or case-sensitivity bypass, attackers can transfer a fake ERC-20 they deployed and emit an event for the real token.
  3. Reentrancy and flash-loan manipulation (Transient Balance Manipulation): An attacker can use a flash loan to deposit and borrow out funds within the same block. An event fires, but the vault’s net asset increase at the end of the block may be zero.

2. The Core Defense: A Strict Balance-Delta Invariant

To stop phantom deposits, the bridge’s relay-signing layer must enforce one rule: every cross-chain mint or release instruction requires corroboration from two fully independent proof dimensions:

ValidDeposit ⇔ (ValidEventLog) ∧ (ΔPhysicalVaultBalance ≥ ClaimedAmount)

After receiving an event, the relayer must not immediately sign a cross-chain message. It must query historical state through the underlying JSON-RPC within the same block-height context:

// passage-bridge/src/relayer/balance-invariant-auditor.ts
import { PublicClient, parseAbiItem, getAddress } from 'viem';

export interface DepositEventPayload {
  messageId: `0x${string}`;
  depositor: `0x${string}`;
  token: `0x${string}`;
  amount: bigint;
  blockNumber: bigint;
  txHash: `0x${string}`;
}

export class BalanceInvariantAuditor {
  private client: PublicClient;
  private vaultAddress: `0x${string}`;

  constructor(client: PublicClient, vaultAddress: `0x${string}`) {
    this.client = client;
    this.vaultAddress = getAddress(vaultAddress);
  }

  /**
   * @notice Cross-check the actual asset balance difference before and after the event's transaction
   */
  public async verifyDepositInvariant(event: DepositEventPayload): Promise<{ valid: boolean; reason?: string }> {
    const targetToken = getAddress(event.token);

    // 1. Fetch the receipt and confirm that the transaction itself succeeded on-chain
    const receipt = await this.client.getTransactionReceipt({ hash: event.txHash });
    if (receipt.status !== 'success') {
      return { valid: false, reason: `Transaction ${event.txHash} reverted on-chain.` };
    }

    // 2. Confirm that the receipt contains a Deposit event from this vault address (prevent forged log origins)
    const isLogOriginValid = receipt.logs.some(
      log => getAddress(log.address) === this.vaultAddress && log.transactionHash === event.txHash
    );
    if (!isLogOriginValid) {
      return { valid: false, reason: `Event log was not emitted by official vault contract ${this.vaultAddress}.` };
    }

    // 3. Core conservation check: read actual balances at the previous and current blocks through Ethereum RPC
    // Read the balance at block N containing the transaction
    const balanceAfter = await this.client.readContract({
      address: targetToken,
      abi: parseAbiItem('function balanceOf(address) view returns (uint256)'),
      functionName: 'balanceOf',
      args: [this.vaultAddress],
      blockNumber: event.blockNumber,
    });

    // Read the balance at the previous block N-1
    const balanceBefore = await this.client.readContract({
      address: targetToken,
      abi: parseAbiItem('function balanceOf(address) view returns (uint256)'),
      functionName: 'balanceOf',
      args: [this.vaultAddress],
      blockNumber: event.blockNumber - 1n,
    });

    // 4. Assert the balance-delta invariant: balance at N must be >= balance at N-1 + claimed deposit amount
    // (With multiple deposits per block at extreme frequency, balanceAfter - balanceBefore must be >= the sum of actual deposits in that block)
    if (balanceAfter < balanceBefore) {
      return {
        valid: false,
        reason: `CRITICAL INVARIANT VIOLATION: Vault balance decreased! Before: ${balanceBefore}, After: ${balanceAfter}`
      };
    }

    const netDelta = balanceAfter - balanceBefore;
    if (netDelta < event.amount) {
      return {
        valid: false,
        reason: `PHANTOM DEPOSIT DETECTED: Physical vault gain (${netDelta}) is less than claimed event amount (${event.amount})`
      };
    }

    return { valid: true };
  }
}

3. Designing Passage Bridge’s On-Chain Vault Self-Checks

Alongside off-chain relay review, the vault contract itself must enforce strict balance checks before and after crediting a deposit, eliminating the possibility of false logs at the source.

// passage-bridge/contracts/src/core/VerifiedSourceVault.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

error InvariantDepositFailed(uint256 expectedDelta, uint256 actualDelta);
error ZeroAddressNotAllowed();
error UnsupportedToken(address token);

contract VerifiedSourceVault is ReentrancyGuard {
    using SafeERC20 for IERC20;

    mapping(address => bool) public supportedTokens;
    uint256 public nextNonce;

    event CrossChainDeposit(
        bytes32 indexed messageId,
        address indexed depositor,
        address token,
        uint256 amount,
        uint256 destChainId,
        address recipient,
        uint256 nonce
    );

    constructor(address[] memory _initialTokens) {
        for (uint256 i = 0; i < _initialTokens.length; i++) {
            supportedTokens[_initialTokens[i]] = true;
        }
    }

    /**
     * @notice Production-grade deposit entry: assert actual balance growth instead of trusting transfer arguments
     */
    function depositForCrossChain(
        address token,
        uint256 amount,
        uint256 destChainId,
        address recipient
    ) external nonReentrant returns (bytes32 messageId) {
        if (!supportedTokens[token]) revert UnsupportedToken(token);
        if (recipient == address(0)) revert ZeroAddressNotAllowed();
        require(amount > 0, "INVALID_AMOUNT");

        // 1. Before transfer: record the contract's actual underlying token balance precisely
        uint256 balanceBefore = IERC20(token).balanceOf(address(this));

        // 2. Execute the external transfer (supports fee-on-transfer / rebasing tokens)
        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);

        // 3. After transfer: read the actual balance again
        uint256 balanceAfter = IERC20(token).balanceOf(address(this));

        // 4. Enforce the balance-delta invariant (Invariant Check)
        uint256 actualDelta = balanceAfter - balanceBefore;
        if (actualDelta < amount) {
            // If transfer tax or fraudulent transfer logic exists, refuse to credit the original amount
            revert InvariantDepositFailed(amount, actualDelta);
        }

        uint256 currentNonce = nextNonce++;
        messageId = keccak256(abi.encode(
            block.chainid,
            destChainId,
            currentNonce,
            msg.sender,
            recipient,
            token,
            actualDelta
        ));

        // 5. Emit the event only after confirming the actual balance increased by actualDelta
        emit CrossChainDeposit(
            messageId,
            msg.sender,
            token,
            actualDelta,
            destChainId,
            recipient,
            currentNonce
        );
    }
}

4. Cross-Reconciliation Between Two Relayers and Threshold Signing

Even a node that checks asset conservation can be bypassed if an insider gains server privileges or its RPC cache is poisoned. The Passage Bridge architecture requires cross-endorsement from at least two relayer nodes independently operated by different organizations:

+----------------------------------------------------------------------------------------------------+
|                         Independent Dual-Relayer Verification and Multisig Release                  |
+----------------------------------------------------------------------------------------------------+

   [ Source-chain contract emits Deposit ]
               │
               ├───────────────────────┬───────────────────────┐
               ▼                                               ▼
   [ Relay Node A (Fone official node) ]          [ Relay Node B (independent validator organization) ]
   - Fetch Deposit event                         - Fetch Deposit event
   - Verify balanceOf delta using independent RPC - Verify invariant using private independent archive node
   - Verify block has 64 confirmations            - Verify block has 64 confirmations
               │                                               │
               ▼ (verified)                                    ▼ (verified)
   Generate EIP-712 signature A                   Generate EIP-712 signature B
               │                                               │
               └───────────────────────┬───────────────────────┘
                                       │
                                       ▼ Aggregate both signatures (Threshold = 2/2)
                         [ Destination Release Verification Gateway ]
                                       │
                                       ├── Verify both signature A and signature B
                                       ├── Check nonce has not been replayed
                                       └── Check outflow is within the rolling-window limit
                                       │
                                       ▼ Safely release funds to the user!
+----------------------------------------------------------------------------------------------------+

This architecture eliminates single-node compromise as a point of failure. Even if an attacker buys one relayer’s private key, without forging the other independent node’s signature attesting to actual asset growth, the destination contract throws InsufficientSignatures and blocks release.


5. Comparing Event Listening with Dual Asset-Invariant Verification

The following table shows why modern cross-chain systems must move beyond purely event-driven architectures:

Dimension Traditional: purely event-driven This approach: dual asset-invariant reconciliation
Fake-deposit defense No protection (an emitted event is treated as proof of receipt) Complete immunity (must read actual balanceOf differences)
Transfer-tax tokens Destination payouts can exceed source receipts, steadily draining the pool Automatically captures actualDelta and bridges the net amount actually received
RPC poisoning tolerance None (compromising one node allows forged events and unbacked minting) Very high (two independent nodes cross-check and require both signatures)
Extreme block reorganizations Soft confirmations can be mistaken for real deposits Requires waiting for the finalized epoch before checking balance differences
Production audit and reconciliation costs The pool may already be empty when ledger/asset mismatches are discovered Real-time off-chain reconciliation dashboard, with 0-second alerts and circuit breaking on anomalies

6. Integrating Circuit Breakers and Replay Protection

Asset-conservation verification protects the bridge’s input side. Together with the defensive components discussed in earlier chapters, it forms an impenetrable defense matrix:

  1. Integrate the on-chain rolling-window rate limiter: Even if an unknown cryptographic zero-day defeats reconciliation in an extreme case, the Cross-Chain Vault Rolling-Window Rate Limiter and Circuit Breaker still caps hourly outflow at a safe threshold, protecting at least 95% of deposited funds.
  2. Integrate unordered bitmap replay markers: Every cross-verified messageId is atomically marked during destination release using EIP-712 Domain Separation and Bitmap Nonces, preventing replay of previously valid credentials across forks or a second execution.

Consulting and Source-Code Delivery

If you are planning heterogeneous-chain support, a high-throughput asset bridge, or cross-chain liquidity vaults, and want financial-grade protection against phantom deposits, replay attacks, and compromised nodes: