Fraxtal Merkle Proof Oracles

Fraxtal Merkle Oracles (Fraxtal-MOs) are a class of oracles that utilize the eth_getProof rpc method to proof L1 State on Ethereum L2s such as Fraxtal.

Overview

Fraxtal-MOs utilize 4 Key contracts in order to verify and transport state.

FraxchainL1Block

Predeploy contract which serves as a registry of L1 block hashes on the L2.

StateRootOracle

Contract which verifies a given blockheader against the L1 Blockhash relayed via the FraxchainL1Block contract. This contract is responsible for storing the stateroot hash as well as the L1 timestamp.

MerkleProofPriceSource

Contract which performs the state root verification. Given a merkle proof, constructed off chain via the eth_getProof rpc method. This contract will verify the and extract the storage slot values "proofed" onto the L2 blockchain. These "proofed" values are then relayed to the oracle itself.

The process of relaying these "proofed" values is trustless in the sense that anyone may submit a valid merkle proof corresponding to the pre-approved slots for a given Ethereum L1 address.

FraxtalERC4626TransportOracle

Contract which accepts "proofed" L1 data from the MerkleProofPriceSoure contract.

In the case of sFrax and other ERC4626 vaults. These "proofed" values define the current slope of the value accrual function of the vault token on the L1.

These oracles expose the following funtions in order to allow the user to query the price of the asset in question:

    /// @dev Adheres to chainlink's AggregatorV3Interface `latestRoundData()`
    /// @return _roundId The l1Block corresponding to the last time the oracle was proofed
    /// @return _answer The price of Sfrax in frax
    /// @return _startedAt The L1 timestamp corresponding to most recent proof
    /// @return _updatedAt Equivalent to `_startedAt`
    /// @return _answeredInRound Equivalent to `_roundId`
    function latestRoundData()
        external
        view
    returns (
        uint80 _roundId, 
        int256 _answer, 
        uint256 _startedAt, 
        uint256 _updatedAt, 
        uint80 _answeredInRound
    )
/// @return _pricePerShare The current exchange rate of the vault token 
///                        denominated in the underlying vault asset
function pricePerShare() public view returns (uint256 _pricePerShare);

Architecture

The Process of transporting/proving the L1 data onto the L2:

Step 1: Prove the L1 blockHeader on the L2

Screenshot 2024-09-23 at 11.04.48 AM

Step 2: Submit the Storage proof for a predefined L1 address and Slots

Screenshot 2024-09-23 at 11.05.00 AM

Demo Client

The following code should serve as an example of how to generate the function arguments that are accepted by the fraxtal smart contracts detailed above.

Documentation surrounding the RPC methods: eth_getBlockByNumber && eth_getProof

To generate the L1 Block Header

async function getHeaderFromBlock(provider, blockL1) {
    let block = await provider.send("eth_getBlockByNumber", [blockL1, false])
    let headerFields = [];
    headerFields.push(block.parentHash);
    headerFields.push(block.sha3Uncles);
    headerFields.push(block.miner);
    headerFields.push(block.stateRoot);
    headerFields.push(block.transactionsRoot);
    headerFields.push(block.receiptsRoot);
    headerFields.push(block.logsBloom);
    headerFields.push(block.difficulty);
    headerFields.push(block.number);
    headerFields.push(block.gasLimit);
    headerFields.push(block.gasUsed);
    headerFields.push(block.timestamp);
    headerFields.push(block.extraData);
    headerFields.push(block.mixHash);
    headerFields.push(block.nonce);
    headerFields.push(block.baseFeePerGas);
    if (block.withdrawalsRoot) {
        headerFields.push(block.withdrawalsRoot);
    }
    if (block.blobGasUsed) {
        headerFields.push(block.blobGasUsed);
    }
    if (block.excessBlobGas) {
        headerFields.push(block.excessBlobGas);
    }
    if (block.parentBeaconBlockRoot) {
        headerFields.push(block.parentBeaconBlockRoot);
    }
    convertHeaderFields(headerFields);
    let header = ethers.utils.RLP.encode(headerFields);
    return header
}

To generate a storage proof

        let blockToProof = "0x"+blockL1.toHexString().substring(2).replace(/^0+/, "");

        let sfrax_proof = await mainnetProvider.send("eth_getProof", 
        [
            // L1 address to generate proofs for
            SFRAX_MAINNET,
            // Slots to proof 
            [
                "0x0000000000000000000000000000000000000000000000000000000000000002",
                "0x0000000000000000000000000000000000000000000000000000000000000009",
                "0x0000000000000000000000000000000000000000000000000000000000000008",
                "0x0000000000000000000000000000000000000000000000000000000000000006",
                "0x0000000000000000000000000000000000000000000000000000000000000007"
            ], 
            blockToProof
        ]);
    
        // Format the proof info returned from `eth_getProof`
        let proof: Proof = {} as Proof;
        proof._accountProofSfrax = sfrax_proof.accountProof;
        proof._storageProofTotalSupply = sfrax_proof.storageProof[0].proof;
        proof._storageProofTotalAssets = sfrax_proof.storageProof[1].proof;
        proof._storageProofLastDist = sfrax_proof.storageProof[2].proof;
        proof._storageProofRewardsPacked = sfrax_proof.storageProof[3].proof;
        proof._storageProofRewardsCycleAmount = sfrax_proof.storageProof[4].proof;

        let txn = await proover.addRoundDataSfrax(
            SFRAX_L2_ORACLE,
            blockL1.toString(),
            proof
        )

Deployed Contracts

Last updated