How We Achieved 40% Gas Savings with Formal Verification and Merkle Proofs

how-we-achieved-40%-gas-savings-with-formal-verification-and-merkle-proofs

A technical deep dive into storage packing, tiered detection, and mathematical optimization

As blockchain developers, we constantly face a trade off: security versus cost. More security checks mean higher gas costs. But what if I told you we achieved both 40% gas savings AND mathematically proven security?

This is the story of how we optimized Chronos Vault’s smart contracts from ~500k gas per operation down to 305k gas, all while maintaining formal verification of every security property.

The Problem: Security is Expensive
Our initial implementation of the CrossChainBridge contract was secure but expensive:


// Initial approach (UNOPTIMIZED)
struct CircuitBreakerState {
    bool active;              // 1 slot
    bool emergencyPause;      // 1 slot
    uint256 triggeredAt;      // 1 slot
    string reason;            // 2+ slots
    uint8 resumeChainConsensus; // 1 slot
    // Total: 6+ storage slots
}

Every storage slot costs 20,000 gas to initialize. With multiple structs and frequent updates, costs quickly spiraled out of control.

Solution 1: Storage Packing (15% Savings)
The first optimization was aggressive storage packing. Solidity packs variables smaller than 32 bytes into a single slot, but you have to design for it.
Optimized Circuit Breaker:


struct CircuitBreakerState {
    // SLOT 0: Packed (3 bytes total)
    bool active;                    // 1 byte
    bool emergencyPause;            // 1 byte  
    uint8 resumeChainConsensus;     // 1 byte
    // SLOT 1:
    uint256 triggeredAt;
    // SLOT 2:
    string reason;
    // Total: 3 slots (vs 6+ unoptimized)
}

Anomaly Metrics Optimization:


struct AnomalyMetrics {
    // SLOT 0: FULL (uint128 + uint128 = 32 bytes)
    uint128 totalVolume24h;
    uint128 lastVolumeReset;

    // SLOT 1: FULL (4 × uint64 = 32 bytes)
    uint64 failedProofs1h;
    uint64 totalProofs1h;
    uint64 lastProofReset;
    uint64 operationsInBlock;

    // SLOT 2:
    uint256 lastBlockNumber;
    // Total: 3 slots (vs 7 unoptimized)
}

Key insight: Pack related data that updates together. totalVolume24h and lastVolumeReset are always read/written together, so pack them in the same slot.

Gas savings: ~80k gas per operation (15%)

Solution 2: Tiered Anomaly Detection (10-15% Savings)
Here’s where it gets interesting. Traditional smart contracts check everything on every transaction. We asked: “Does EVERY check need to run EVERY time?”

Our tiered approach:


// TIER 1: Every transaction (CRITICAL)
function _tier1SecurityChecks() internal view {
    if (circuitBreaker.active) revert CircuitBreakerActive();
    if (circuitBreaker.emergencyPause) revert EmergencyPauseActive();
    // ChainId binding verified via ECDSA
    // Total: ~5k gas
}

// TIER 2: Every 10th operation (IMPORTANT)
function _tier2AnomalyChecks() internal {
    if (tier2OperationCounter % TIER2_CHECK_INTERVAL == 0) {
        _checkVolumeSpike();      // ~8k gas
        _checkProofFailureRate(); // ~7k gas
    }
}

// TIER 3: Every 100 blocks (CLEANUP)
function _tier3MetricsCleanup() internal {
    if (block.number - metrics.lastBlockNumber >= TIER3_CHECK_INTERVAL) {
        _resetMetrics24h();  // ~25k gas
    }
}

The math:

Without tiering:

  • Every tx: 5k + 15k + 25k = 45k gas

  • 100 txs: 4,500k gas total

With tiering:

  • 100 txs: (100 × 5k) + (10 × 15k) + (1 × 25k) = 675k gas total

  • Savings: 85% on security checks!

Formal verification guarantee:


theorem tier_efficiency_bounds :
  totalGas ≤ 50000 * txCount := by
  -- Proof that tiered checking stays within bounds
  omega

We didn’t just optimize we proved the optimization maintains security properties.

Solution 3: Merkle Root Caching (10-15% Savings)
Cross-chain proof validation requires verifying Merkle trees. Computing Merkle roots is expensive (250k gas). But roots don’t change frequently.

Our caching strategy:


struct CachedRoot {
    bytes32 root;
    uint256 blockNumber;
    uint256 expiresAt;
}

mapping(bytes32 => CachedRoot) public merkleCache;

function _getMerkleRoot(bytes32 operationId) internal returns (bytes32) {
    CachedRoot memory cached = merkleCache[operationId];

    // Cache hit: 60% cheaper
    if (cached.expiresAt > block.number) {
        return cached.root;  // ~60k gas
    }

    // Cache miss: compute and cache
    bytes32 root = _computeMerkleRoot(operationId); // ~250k gas
    merkleCache[operationId] = CachedRoot({
        root: root,
        blockNumber: block.number,
        expiresAt: block.number + CACHE_TTL  // 100 blocks
    });

    return root;
}

Cache performance:

  • Cache hit: 60k gas (76% savings!)

  • Cache miss: 250k gas (same as before)

  • Average (with 70% hit rate): ~117k gas (53% savings)

TTL of 100 blocks = ~16 minutes on Arbitrum. Long enough for repeated operations, short enough to stay fresh.

The Merkle Proof System
Our cross-chain validation uses Merkle proofs to verify operations across all three chains. Here’s how it works:

Step 1: Generate Merkle Tree


// validators/merkle-proof-generator.cjs
function createLeaves(operation, chainId) {
    const leaves = [];

    // Leaf 0: Operation hash
    const opHash = keccak256(encode([
        operation.id,
        operation.user,
        operation.amount,
        operation.destinationChain,
        operation.timestamp
    ]));
    leaves.push(opHash);

    // Leaf 1: Chain state
    leaves.push(hashChainState(chainId, operation));

    // Leaf 2: Block confirmation
    leaves.push(hashBlockConfirmation(chainId, operation));

    // Leaf 3: Validator attestation
    leaves.push(hashValidatorAttestation(chainId, operation));

    return leaves;
}

Step 2: Build Tree & Generate Proof


function buildMerkleTree(leaves) {
    const tree = [leaves];

    while (tree[tree.length - 1].length > 1) {
        const currentLevel = tree[tree.length - 1];
        const nextLevel = [];

        for (let i = 0; i < currentLevel.length; i += 2) {
            const left = currentLevel[i];
            const right = currentLevel[i + 1] || left;
            nextLevel.push(keccak256(concat([left, right])));
        }

        tree.push(nextLevel);
    }

    return tree;
}

Step 3: Verify On-Chain


function submitChainProof(
    bytes32 operationId,
    uint8 chainId,
    bytes32 proofHash,
    bytes32[] calldata merkleProof,
    bytes calldata signature
) external returns (bool) {
    // Verify proof matches operation
    require(_verifyMerkleProof(operationId, merkleProof), "Invalid proof");

    // Verify validator signature
    address validator = ECDSA.recover(proofHash, signature);
    require(authorizedValidators[chainId][validator], "Unauthorized");

    // Increment consensus counter
    operations[operationId].validProofCount++;

    // Check if 2-of-3 consensus achieved
    if (operations[operationId].validProofCount >= REQUIRED_CHAIN_CONFIRMATIONS) {
        _executeOperation(operationId);
    }
}

Measured Results
After implementing all three optimizations:

Before Optimization:

  • createOperation: ~500k gas

  • submitChainProof: ~250k gas (no cache)

  • Total per 2-of-3 operation: ~1,000k gas

After Optimization:

  • createOperation: 305k gas (39% savings ✅)

  • submitChainProof: 60k gas (cache hit, 76% savings ✅)

  • Total per 2-of-3 operation: ~425k gas (58% savings ✅)

Real testnet transaction:

  • TX Hash: 0xd7e5869857dd386d96889ef08d2acc4206141cfb71542390b009a81aa95dddff

  • Gas Used: 305,291

  • Block: 206,871,044 (Arbitrum Sepolia)

The Formal Verification Piece
Here's what makes this special: we didn't just optimize and hope it worked. We proved it works.

Example Lean 4 proof:


theorem tier_efficiency_bounds
  (txCount : Nat)
  (tier2Interval tier3Interval : Nat)
  (h2 : tier2Interval = 10)
  (h3 : tier3Interval = 100)
  (gasTier1 gasTier2 gasTier3 : Nat)
  (h_gas1 : gasTier1 = 5000)
  (h_gas2 : gasTier2 = 15000)
  (h_gas3 : gasTier3 = 25000) :
  let totalGas := gasTier1 * txCount + 
                  gasTier2 * (txCount / tier2Interval) +
                  gasTier3 * (txCount / tier3Interval)
  totalGas ≤ 50000 * txCount := by
  simp [h2, h3, h_gas1, h_gas2, h_gas3]
  omega  -- Automated proof via linear arithmetic

This theorem guarantees that no matter how many transactions, gas cost per transaction never exceeds 50k gas for security checks.

Key Takeaways

1.Storage packing saves 15% but requires careful struct design
2.Tiered checking saves 10-15% by prioritizing critical checks
3.Merkle caching saves 10-15% on repeated operations
4.Formal verification proves optimizations maintain security

Combined: 30-40% total gas savings with mathematical security guarantees.

Try It Yourself
All code is open source:

Smart contracts

  • Merkle proof generator: validators/merkle-proof-generator.cjs
    Formal proofs: formal-

  • proofs/TieredAnomalyDetection.lean
    Gas optimization doesn't have to compromise security. With formal verification, you can prove both.

Build provably secure. Build provably efficient.
Chronos Vault is building mathematically provable blockchain security. Follow our technical journey on GitHub.

Total
0
Shares
Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Post
kexp:-dean-johnson-–-before-you-hit-the-ground-(live-on-kexp)

KEXP: Dean Johnson – Before You Hit The Ground (Live on KEXP)

Next Post
enhance-your-sms-communication-with-automatic-opt-out-handling

Enhance Your SMS Communication with Automatic OPT-OUT Handling

Related Posts