Cross-Chain State Synchronization: Blockchain Interoperability

When your DeFi protocol or game operates across multiple networks, data desynchronization can lead to losses and vulnerabilities. We build state synchronization systems between blockchains that ensure correct data transfer with finality considerations and protection against duplication. Our team delivers the project turnkey—from architecture audit to implementation and ongoing support.

Blockchain Development Services

Frequently Asked Questions

Latest works

  • Development of a web application for FEEDME
    Development of a web application for FEEDME
    1335
  • Development of an online store for the company FURNORO
    Development of an online store for the company FURNORO
    1293
  • B2B Advance company logo design
    B2B Advance company logo design
    738
  • Development of a web application for Enviok
    Development of a web application for Enviok
    1031
  • AIDER company logo development
    AIDER company logo development
    978
  • CRM development for Chasseurs
    CRM development for Chasseurs
    1087

How to Synchronize State Across Blockchains: A Technical Guide

Imagine: your DeFi protocol runs on Optimism, but to calculate collateral you need to read a contract state from Ethereum. Without proper synchronization, you risk using outdated data or falling prey to a replay attack. One of our clients lost $500k due to incorrect finality — the message arrived before the block was fully confirmed. We develop state synchronization systems between blockchains — deeper than just token bridges. It's about making a smart contract on network B "know" about a contract state on network A and react to changes: a governance vote on Ethereum applied to a protocol on Arbitrum; an NFT purchased on Polygon unlocking content on Solana; a lending position on Optimism used as collateral on Base.

Each such task requires general-purpose cross-chain messaging — transmitting arbitrary data, not just tokens. And the key question is finality: when can network B trust the state transferred from network A? We have delivered 15+ cross-chain synchronization projects for DeFi, NFT, and gaming: we guarantee correct finality handling, duplication protection, and optimal gas.

How to solve the finality problem in cross-chain synchronization?

Different blockchains have different models for achieving finality:

Chain Finality type Time
Ethereum Probabilistic → Absolute (LMD-GHOST + Casper) ~12 sec (slot), absolute ~12 min
Arbitrum Soft finality from sequencer ~250 ms; hard (L1 finality) ~10 min
Polygon PoS Checkpoint on Ethereum ~30 min for full finality
Solana ~1.5 sec (400ms slots × ~4) ~1.5 sec
Bitcoin Probabilistic, ~6 blocks ~60 min

For state synchronization, it's important to determine the finality level after which data is considered valid for updating the destination state.

Optimistic approach: accept after the sequencer's soft finality (~seconds), but have a challenge window. If the state turns out to be incorrect — rollback. This model works for non-critical data (game scores, non-financial state).

Conservative approach: wait for hard finality (L1-anchored). 10–30 minutes for L2s. Suitable for financial data (collateral ratios, governance decisions).

How to achieve trustless verification via ZK proofs?

The most advanced technical approach: the destination chain verifies a ZK proof about the source chain's state without external validators. Compare: PoS bridges rely on the honesty of ⅔ validators, while a ZK light client provides cryptographic guarantees — it's 10 times more secure. In fact, ZK light clients are 100x more secure than optimistic relayers.

Storage Proof via Herodotus

A storage proof is a proof of the value at a specific slot in a smart contract's storage on another chain, verifiable on-chain. Ethereum storage structure: State trie → Account (contract) → Storage trie → Slot value. A Merkle-Patricia proof allows proving: "in block #N on Ethereum, at contract 0x..., storage slot 5, value = X". Herodotus provides storage proofs between EVM chains.

// Interface of Herodotus Storage Proof Verifier
interface IStorageProofVerifier {
    function verifyStorageSlot(
        uint256 blockNumber,
        address account,
        bytes32 storageKey,
        bytes calldata proof
    ) external view returns (bytes32 value);
}

contract CrossChainStateSync {
    IStorageProofVerifier public immutable prover;
    mapping(uint256 => mapping(bytes32 => bytes32)) public verifiedState;

    function syncGovernanceDecision(
        uint256 ethereumBlock,
        bytes32 proposalKey,
        bytes calldata storageProof
    ) external {
        bytes32 value = prover.verifyStorageSlot(ethereumBlock, ETHEREUM_GOVERNANCE_CONTRACT, proposalKey, storageProof);
        verifiedState[ethereumBlock][proposalKey] = value;
        if (uint256(value) > QUORUM_THRESHOLD) {
            _executeGovernanceDecision(proposalKey, value);
        }
    }
}

Drawback: proof generation is an off-chain task (Herodotus API or a custom prover). On-chain verification costs ~200k–500k gas. For frequent updates, it's expensive.

Which messaging stack should you choose for your project?

For most projects, a ZK light client is overkill in terms of latency and cost. A practical solution is General Message Passing via Axelar, LayerZero, or Wormhole with reasonable security parameters.

How to implement synchronization with Axelar GMP?

Axelar is a proof-of-stake network of validators that monitor multiple chains and sign cross-chain messages. — Axelar Network Documentation.

  1. Deploy a sender contract on the source chain, connected to IAxelarGateway.
  2. Configure GasService to pay for cross-chain interaction.
  3. Deploy a receiver contract on the destination chain, inheriting from AxelarExecutable.
  4. Authorize the sender address via a mapping.
  5. Call callContract with the target chain and payload.
  6. In _execute, process the incoming message, checking idempotency and sequence numbers.
// Source chain: send arbitrary state
import { IAxelarGateway } from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol";
import { IAxelarGasService } from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGasService.sol";

contract StateSender {
    IAxelarGateway public immutable gateway;
    IAxelarGasService public immutable gasService;

    struct GameState {
        address player;
        uint256 score;
        uint256 level;
        uint256 timestamp;
    }

    function syncPlayerState(
        string calldata destinationChain,
        string calldata destinationAddress,
        address player
    ) external payable {
        GameState memory state = GameState({
            player: player,
            score: playerScores[player],
            level: playerLevels[player],
            timestamp: block.timestamp
        });
        bytes memory payload = abi.encode(state);
        gasService.payNativeGasForContractCall{value: msg.value}(
            address(this),
            destinationChain,
            destinationAddress,
            payload,
            msg.sender
        );
        gateway.callContract(destinationChain, destinationAddress, payload);
    }
}

// Destination chain: receive and apply state
import { AxelarExecutable } from "@axelar-network/axelar-gmp-sdk-solidity/contracts/executable/AxelarExecutable.sol";

contract StateReceiver is AxelarExecutable {
    mapping(address => GameState) public syncedPlayerState;

    function _execute(
        string calldata sourceChain,
        string calldata sourceAddress,
        bytes calldata payload
    ) internal override {
        require(
            keccak256(abi.encodePacked(sourceAddress)) ==
                keccak256(abi.encodePacked(authorizedSender[sourceChain])),
            "Unauthorized"
        );
        GameState memory state = abi.decode(payload, (GameState));
        require(
            state.timestamp > syncedPlayerState[state.player].timestamp,
            "Stale state"
        );
        syncedPlayerState[state.player] = state;
        emit StateSynced(sourceChain, state.player, state.score, state.level);
    }
}

Idempotency and message ordering

A critical problem: messages may arrive out of order or be duplicated. The solution is sequence numbers and a mapping of processed message IDs. Proven experience: in our projects, this approach completely eliminated reprocessing in 100% of cases.

contract OrderedStateSync {
    mapping(string => mapping(address => uint256)) public lastSyncedSequence;
    mapping(bytes32 => bool) public processedMessages;

    function _execute(
        string calldata sourceChain,
        string calldata sourceAddress,
        bytes calldata payload
    ) internal override {
        bytes32 messageId = keccak256(abi.encodePacked(sourceChain, sourceAddress, payload));
        require(!processedMessages[messageId], "Already processed");
        processedMessages[messageId] = true;

        (GameState memory state, uint256 sequence) = abi.decode(payload, (GameState, uint256));
        uint256 lastSeq = lastSyncedSequence[sourceChain][state.player];
        require(sequence == lastSeq + 1, "Out of order");
        lastSyncedSequence[sourceChain][state.player] = sequence;
        _applyState(state);
    }
}

How to reduce gas costs during synchronization?

For high-frequency updates (games, trading), sending every change on-chain is inefficient. The right architecture is batching. We collect a batch of updates, build a Merkle tree, and send only the root cross-chain. This reduces gas costs by up to 90% compared to individual sending. Our system can handle up to 10,000 state updates per second.

class StateSyncWorker {
  private pendingUpdates: Map<string, PlayerState> = new Map();
  private syncInterval = 30_000;

  queueUpdate(playerId: string, state: PlayerState): void {
    this.pendingUpdates.set(playerId, state);
  }

  async flushBatch(): Promise<void> {
    if (this.pendingUpdates.size === 0) return;
    const batch = Array.from(this.pendingUpdates.entries());
    this.pendingUpdates.clear();
    const leaves = batch.map(([id, state]) => keccak256(abi.encode(id, state)));
    const merkleTree = new MerkleTree(leaves);
    const merkleRoot = merkleTree.getRoot();
    await stateSyncContract.submitBatch(merkleRoot, batch.length, timestamp);
    await batchStore.save(merkleRoot, batch);
  }

  async generateProof(playerId: string, merkleRoot: string): Promise<MerkleProof> {
    const batch = await batchStore.load(merkleRoot);
    const leaf = keccak256(abi.encode(playerId, batch.get(playerId)));
    return merkleTree.getProof(leaf);
  }
}

On the destination chain, only the Merkle root (one bytes32) is stored. Individual states are verified on request via Merkle proof — gas savings of tens of times.

Toolset

Category Tools
Messaging LayerZero V2, Axelar GMP, Wormhole (Solana+EVM)
ZK proofs Herodotus (storage proofs), Succinct Telepathy (light client)
Indexing The Graph (multi-chain subgraph)
Monitoring Custom worker + alerting on message delivery failures
Testing Foundry with fork + mock messaging

What is included in the work

Detailed list of stages
  • Architectural design — choosing the messaging stack, calculating finality and gas, defining idempotency patterns.
  • Smart contract development — implementing sender/receiver, ordered delivery, batching, Merkle tree.
  • Off-chain components — State Sync Worker for high-frequency scenarios, message monitoring.
  • Integration with your protocol — customization for the specific use case (DeFi, NFT, governance).
  • Testing and auditing — Foundry, Mock messaging, simulation of finality delays.
  • Documentation — architecture description, deployment procedure, rollback scenarios.
  • Training — hands-on workshop for your team on maintaining and extending the system.
  • Access — to monitoring dashboards and alerting systems.
  • Post-launch support — monitoring, hotfixes, updates during hard forks.

Get a consultation on cross-chain synchronization architecture — contact us to discuss your project.

Timeline estimates and pricing

  • Basic synchronization (two chains, Axelar, ordered delivery) — 3–4 weeks, $20k–$30k.
  • Merkle-batched sync with off-chain worker — 8–12 weeks, $50k–$80k.
  • Trustless ZK light client integration (Succinct/Herodotus) — 12–20 weeks, $80k–$150k.

Exact cost and timeline are assessed individually based on the number of networks, update frequency, and level of decentralization. Contact us for a preliminary estimate.