Building a Storage Proof Powered Bridge

WIP!!!!

For a more comprehensive, step-by-step guide with specific implementation details, please refer to our Ethereum <> Starknet Yet Another Bridge in-depth tutorial at: https://github.com/HerodotusDev/yab-herodotus/tree/main

This tutorial provides an overview of using Storage Proofs to create a secure bridge between EVM-compatible blockchain networks. We'll cover the core concepts and provide code examples for building a bridge using Storage Proofs and the FactsRegistry.

Prerequisites

  • Understanding of smart contracts and blockchain concepts

  • Familiarity with Solidity and JavaScript/TypeScript

  • Access to the Storage Proof API

Step 1: Identify the Contract Storage Layout

First, determine the storage layout of the contract you want to prove. Let's say we're interested in a transfers mapping:

mapping(bytes32 => struct Transfer) public transfers;

To obtain the storage layout:

  1. Use a Solidity storage layout tool or plugin

  2. Or use an online service like storage.herodotus.dev

Note the slot number for the transfers mapping from the output.

Step 2: Mapping Slot Index

For mappings, we need to calculate the exact storage slot for a specific key. Here's a generic JavaScript function to do this:

const ethers = require('ethers');

function getSlot(mappingSlot, key) {
  return ethers.utils.keccak256(
    ethers.utils.defaultAbiCoder.encode(
      ['bytes32', 'uint256'],
      [key, mappingSlot]
    )
  );
}

Usage example:

const TRANSFERS_SLOT = 5; // obtained from storage layout
const key = '0x1234...'; // your specific key
const slot = getSlot(TRANSFERS_SLOT, key);
console.log('Calculated slot:', slot);

Step 3: Request a Storage Proof

With the correct storage slot, request a Storage Proof. Here's a generic example using fetch:

async function requestStorageProof(chainId, contractAddress, slot, blockNumber, apiKey) {
  const response = await fetch('https://api.herodotus.cloud/submit-batch-query', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${apiKey}`
    },
    body: JSON.stringify({
      chainId,
      contractAddress,
      slots: [slot],
      blockNumber
    })
  });

  return await response.json();
}

// Usage
const proofRequest = await requestStorageProof(
  1, // Ethereum mainnet
  '0x1234...', // Your contract address
  slot,
  14000000, // Block number
  'YOUR_API_KEY'
);
console.log('Proof request:', proofRequest);

Step 4: Access Proven Slots

Once the Storage Proof is generated, access the proven data:

async function getProvenData(taskId, apiKey) {
  const response = await fetch(`https://api.herodotus.cloud/batch-query-status/${taskId}`, {
    headers: {
      'Authorization': `Bearer ${apiKey}`
    }
  });

  const proofData = await response.json();
  
  if (proofData.status === 'DONE') {
    const slotValue = proofData.slots[0].value;
    console.log('Proven slot value:', slotValue);
    return slotValue;
  } else {
    console.log('Proof not yet finalized');
    return null;
  }
}

// Usage
const provenValue = await getProvenData(proofRequest.taskId, 'YOUR_API_KEY');

Step 5: Implement On-Chain Verification

To use the Storage Proof in your bridge contract, implement a verification function using the FactsRegistry. Here's a Solidity example:

pragma solidity ^0.8.0;

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

interface IFactsRegistry {
    function verifyStorage(
        address account,
        uint256 blockNumber,
        bytes32 slot,
        bytes memory storageSlotTrieProof
    ) external view returns (bytes32 slotValue);
}

contract Bridge {
    IFactsRegistry public factsRegistry;
    IERC20 public token;
    uint256 public sourceChainId;
    address public sourceTokenContract;

    constructor(address _factsRegistry, address _token, uint256 _sourceChainId, address _sourceTokenContract) {
        factsRegistry = IFactsRegistry(_factsRegistry);
        token = IERC20(_token);
        sourceChainId = _sourceChainId;
        sourceTokenContract = _sourceTokenContract;
    }

    function bridgeTransfer(
        bytes32 transferId,
        uint256 blockNumber,
        bytes memory storageSlotTrieProof
    ) external {
        bytes32 slot = keccak256(abi.encode(transferId, uint256(5))); // Assuming transfers mapping is at slot 5
        
        bytes32 slotValue = factsRegistry.verifyStorage(
            sourceTokenContract,
            blockNumber,
            slot,
            storageSlotTrieProof
        );

        (address recipient, uint256 amount) = decodeSlotValue(slotValue);

        require(recipient != address(0), "Invalid recipient");
        require(amount > 0, "Invalid amount");

        // Perform bridge logic
        token.transfer(recipient, amount);

        emit TransferBridged(transferId, recipient, amount);
    }

    function decodeSlotValue(bytes32 slotValue) internal pure returns (address recipient, uint256 amount) {
        // Implement decoding logic based on your data structure
        // This is a simplified example
        recipient = address(uint160(uint256(slotValue)));
        amount = uint256(slotValue) >> 160;
    }

    event TransferBridged(bytes32 indexed transferId, address indexed recipient, uint256 amount);
}

This implementation uses the verifyStorage function from the FactsRegistry to verify the Storage Proof on-chain. The bridgeTransfer function takes the necessary parameters to verify the storage slot and execute the bridge logic.

Conclusion

By following these steps and using the FactsRegistry, you can create a bridge that uses Storage Proofs to securely transfer and verify data between EVM-compatible chains. This approach enhances security by cryptographically proving the state of one chain on another, reducing trust assumptions compared to traditional oracle-based bridges.

Remember to adapt these examples to your specific environment, contract structure, and chosen libraries. Always thoroughly test your implementation and consider potential edge cases specific to your use case.

Last updated