Skip to content

scrvUSD Verifier

The two verifier contracts work together to securely update and maintain the scrvUSD oracle using on-chain state proofs.

ScrvusdVerifierV1 extracts scrvUSD vault parameters (such as total debt, idle funds, supply, and profit unlocking metrics) from state proofs. It validates these parameters by verifying the block header or state root against the BlockHashOracle and then updates the scrvUSD oracle’s price via its update_price (see oracle documentation) function.

ScrvusdVerifierV2 focuses specifically on updating the profit unlocking duration (profit_max_unlock_time). It uses similar state proof techniques—verifying either an RLP-encoded block header or a state root—to extract the period value. This period is then sent to the scrvUSD oracle via the update_profit_max_unlock_time function.

Together, these contracts ensure that the scrvUSD oracle remains accurate by securely integrating verified on-chain data.


scrvUSD Verifier V1

ScrvusdVerifierV1.sol

The source code for the ScrvusdVerifierV1 contract is available on GitHub. The contract is written in Solidity version 0.8.18.

verifyScrvusdByBlockHash

ScrvusdVerifierV1.verifyScrvusdByBlockHash(_block_header_rlp: bytes, _proof_rlp: bytes) -> uint256

This function verifies scrvUSD parameters using an RLP-encoded block header and a corresponding state proof. It parses the block header to ensure the BlockHash is valid and matches the expected value from the BlockHashOracle, then extracts the scrvUSD vault parameters from the state proof. It then updates the scrvUSD oracle with these parameters, returning the absolute relative price change scaled to \(10^18\) precision.

Returns: absolute relative price change of the scrvUSD price.

Input Type Description
_block_header_rlp bytes RLP-encoded block header containing block details
_proof_rlp bytes RLP-encoded state proof for the scrvUSD parameters
Source code
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;

import {RLPReader} from "hamdiallam/Solidity-RLP@2.0.7/contracts/RLPReader.sol";
import {StateProofVerifier as Verifier} from "../../xdao/contracts/libs/StateProofVerifier.sol";

uint256 constant PARAM_CNT = 2 + 5;
uint256 constant PROOF_CNT = 1 + PARAM_CNT;

interface IScrvusdOracle {
    function update_price(
        uint256[PARAM_CNT] memory _parameters,
        uint256 _ts,
        uint256 _block_number
    ) external returns (uint256);
}

interface IBlockHashOracle {
    function get_block_hash(uint256 _number) external view returns (bytes32);
    function get_state_root(uint256 _number) external view returns (bytes32);
}

contract ScrvusdVerifierV1 {
    using RLPReader for bytes;
    using RLPReader for RLPReader.RLPItem;

    // Common constants
    address constant SCRVUSD = 0x0655977FEb2f289A4aB78af67BAB0d17aAb84367;
    bytes32 constant SCRVUSD_HASH = keccak256(abi.encodePacked(SCRVUSD));

    // Storage slots of parameters
    uint256[PROOF_CNT] internal PARAM_SLOTS = [
        uint256(0), // filler for account proof
        uint256(21), // total_debt
        uint256(22), // total_idle
        uint256(20), // totalSupply
        uint256(38), // full_profit_unlock_date
        uint256(39), // profit_unlocking_rate
        uint256(40), // last_profit_update
        uint256(keccak256(abi.encode(18, SCRVUSD))) // balanceOf(self)
    ];

    address public immutable SCRVUSD_ORACLE;
    address public immutable BLOCK_HASH_ORACLE;

    constructor(address _block_hash_oracle, address _scrvusd_oracle)
    {
        BLOCK_HASH_ORACLE = _block_hash_oracle;
        SCRVUSD_ORACLE = _scrvusd_oracle;
    }

    /// @param _block_header_rlp The RLP-encoded block header
    /// @param _proof_rlp The state proof of the parameters
    function verifyScrvusdByBlockHash(
        bytes memory _block_header_rlp,
        bytes memory _proof_rlp
    ) external returns (uint256) {
        Verifier.BlockHeader memory block_header = Verifier.parseBlockHeader(_block_header_rlp);
        require(block_header.hash != bytes32(0), "Invalid blockhash");
        require(
            block_header.hash == IBlockHashOracle(BLOCK_HASH_ORACLE).get_block_hash(block_header.number),
            "Blockhash mismatch"
        );

        uint256[PARAM_CNT] memory params = _extractParametersFromProof(block_header.stateRootHash, _proof_rlp);
        return _updatePrice(params, block_header.timestamp, block_header.number);
    }

    /// @dev Extract parameters from the state proof using the given state root.
    function _extractParametersFromProof(
        bytes32 stateRoot,
        bytes memory proofRlp
    ) internal view returns (uint256[PARAM_CNT] memory) {
        RLPReader.RLPItem[] memory proofs = proofRlp.toRlpItem().toList();
        require(proofs.length == PROOF_CNT, "Invalid number of proofs");

        // Extract account proof
        Verifier.Account memory account = Verifier.extractAccountFromProof(
            SCRVUSD_HASH,
            stateRoot,
            proofs[0].toList()
        );
        require(account.exists, "scrvUSD account does not exist");

        // Extract slot values
        uint256[PARAM_CNT] memory params;
        for (uint256 i = 1; i < PROOF_CNT; i++) {
            Verifier.SlotValue memory slot = Verifier.extractSlotValueFromProof(
                keccak256(abi.encode(PARAM_SLOTS[i])),
                account.storageRoot,
                proofs[i].toList()
            );
            // Slots might not exist, but typically we just read them.
            params[i - 1] = slot.value;
        }

        return params;
    }

    /// @dev Calls the oracle to update the price parameters.
    ///      Both child contracts use the same oracle call, differing only in how they obtain the timestamp.
    function _updatePrice(
        uint256[PARAM_CNT] memory params,
        uint256 ts,
        uint256 number
    ) internal returns (uint256) {
        return IScrvusdOracle(SCRVUSD_ORACLE).update_price(params, ts, number);
    }
}

verifyScrvusdByStateRoot

ScrvusdVerifierV1.verifyScrvusdByStateRoot(_block_number: uint256, _proof_rlp: bytes) -> uint256

This function verifies scrvUSD parameters by retrieving the state root for a given block number from the block hash oracle and then extracting the scrvUSD vault parameters using a state proof. The extracted parameters are used to update the scrvUSD oracle, returning the absolute relative price change scaled to 10^18 precision.

Returns: The absolute relative price change of the scrvUSD price.

Input Type Description
_block_number uint256 Block number for which to retrieve the state root
_proof_rlp bytes RLP-encoded state proof for the scrvUSD parameters
Source code
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;

import {RLPReader} from "hamdiallam/Solidity-RLP@2.0.7/contracts/RLPReader.sol";
import {StateProofVerifier as Verifier} from "../../xdao/contracts/libs/StateProofVerifier.sol";

uint256 constant PARAM_CNT = 2 + 5;
uint256 constant PROOF_CNT = 1 + PARAM_CNT;

interface IScrvusdOracle {
    function update_price(
        uint256[PARAM_CNT] memory _parameters,
        uint256 _ts,
        uint256 _block_number
    ) external returns (uint256);
}

interface IBlockHashOracle {
    function get_block_hash(uint256 _number) external view returns (bytes32);
    function get_state_root(uint256 _number) external view returns (bytes32);
}

contract ScrvusdVerifierV1 {
    using RLPReader for bytes;
    using RLPReader for RLPReader.RLPItem;

    // Common constants
    address constant SCRVUSD = 0x0655977FEb2f289A4aB78af67BAB0d17aAb84367;
    bytes32 constant SCRVUSD_HASH = keccak256(abi.encodePacked(SCRVUSD));

    // Storage slots of parameters
    uint256[PROOF_CNT] internal PARAM_SLOTS = [
        uint256(0), // filler for account proof
        uint256(21), // total_debt
        uint256(22), // total_idle
        uint256(20), // totalSupply
        uint256(38), // full_profit_unlock_date
        uint256(39), // profit_unlocking_rate
        uint256(40), // last_profit_update
        uint256(keccak256(abi.encode(18, SCRVUSD))) // balanceOf(self)
    ];

    address public immutable SCRVUSD_ORACLE;
    address public immutable BLOCK_HASH_ORACLE;

    constructor(address _block_hash_oracle, address _scrvusd_oracle)
    {
        BLOCK_HASH_ORACLE = _block_hash_oracle;
        SCRVUSD_ORACLE = _scrvusd_oracle;
    }

    /// @param _block_number Number of the block to use state root hash
    /// @param _proof_rlp The state proof of the parameters
    function verifyScrvusdByStateRoot(
        uint256 _block_number,
        bytes memory _proof_rlp
    ) external returns (uint256) {
        bytes32 state_root = IBlockHashOracle(BLOCK_HASH_ORACLE).get_state_root(_block_number);

        uint256[PARAM_CNT] memory params = _extractParametersFromProof(state_root, _proof_rlp);
        // Use last_profit_update as the timestamp surrogate
        return _updatePrice(params, params[5], _block_number);
    }

    /// @dev Extract parameters from the state proof using the given state root.
    function _extractParametersFromProof(
        bytes32 stateRoot,
        bytes memory proofRlp
    ) internal view returns (uint256[PARAM_CNT] memory) {
        RLPReader.RLPItem[] memory proofs = proofRlp.toRlpItem().toList();
        require(proofs.length == PROOF_CNT, "Invalid number of proofs");

        // Extract account proof
        Verifier.Account memory account = Verifier.extractAccountFromProof(
            SCRVUSD_HASH,
            stateRoot,
            proofs[0].toList()
        );
        require(account.exists, "scrvUSD account does not exist");

        // Extract slot values
        uint256[PARAM_CNT] memory params;
        for (uint256 i = 1; i < PROOF_CNT; i++) {
            Verifier.SlotValue memory slot = Verifier.extractSlotValueFromProof(
                keccak256(abi.encode(PARAM_SLOTS[i])),
                account.storageRoot,
                proofs[i].toList()
            );
            // Slots might not exist, but typically we just read them.
            params[i - 1] = slot.value;
        }

        return params;
    }

    /// @dev Calls the oracle to update the price parameters.
    ///      Both child contracts use the same oracle call, differing only in how they obtain the timestamp.
    function _updatePrice(
        uint256[PARAM_CNT] memory params,
        uint256 ts,
        uint256 number
    ) internal returns (uint256) {
        return IScrvusdOracle(SCRVUSD_ORACLE).update_price(params, ts, number);
    }
}

scrvUSD Verifier V2

ScrvusdVerifierV2.sol

The source code for the ScrvusdVerifierV2 contract is available on GitHub. The contract is written in Solidity version 0.8.18.

verifyPeriodByBlockHash

ScrvusdVerifierV2.verifyPeriodByBlockHash(_block_header_rlp: bytes, _proof_rlp: bytes) -> bool

This function verifies the period using an RLP-encoded block header and a corresponding state proof. It parses the block header to ensure the block hash is valid and matches the expected value from the block hash oracle, then extracts the period from the state proof. Finally, it uses the extracted period to update the scrvUSD oracle's profit_max_unlock_time.

Returns: a boolean indicating whether the update was successful.

Input Type Description
_block_header_rlp bytes RLP-encoded block header containing block information
_proof_rlp bytes RLP-encoded state proof for the period
Source code
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;

import {ScrvusdVerifierV1, IBlockHashOracle} from "./ScrvusdVerifierV1.sol";
import {RLPReader} from "hamdiallam/Solidity-RLP@2.0.7/contracts/RLPReader.sol";
import {StateProofVerifier as Verifier} from "../../xdao/contracts/libs/StateProofVerifier.sol";

interface IScrvusdOracleV2 {
    function update_profit_max_unlock_time(
        uint256 _profit_max_unlock_time,
        uint256 _block_number
    ) external returns (bool);
}

contract ScrvusdVerifierV2 is ScrvusdVerifierV1 {
    using RLPReader for bytes;
    using RLPReader for RLPReader.RLPItem;

    uint256 internal PERIOD_SLOT = 37; // profit_max_unlock_time

    constructor(address _block_hash_oracle, address _scrvusd_oracle)
        ScrvusdVerifierV1(_block_hash_oracle, _scrvusd_oracle) {}

    /// @param _block_header_rlp The RLP-encoded block header
    /// @param _proof_rlp The state proof of the period
    function verifyPeriodByBlockHash(
        bytes memory _block_header_rlp,
        bytes memory _proof_rlp
    ) external returns (bool) {
        Verifier.BlockHeader memory block_header = Verifier.parseBlockHeader(_block_header_rlp);
        require(block_header.hash != bytes32(0), "Invalid blockhash");
        require(
            block_header.hash == IBlockHashOracle(ScrvusdVerifierV1.BLOCK_HASH_ORACLE).get_block_hash(block_header.number),
            "Blockhash mismatch"
        );

        uint256 period = _extractPeriodFromProof(block_header.stateRootHash, _proof_rlp);
        return IScrvusdOracleV2(SCRVUSD_ORACLE).update_profit_max_unlock_time(period, block_header.number);
    }

    /// @dev Extract period from the state proof using the given state root.
    function _extractPeriodFromProof(
        bytes32 stateRoot,
        bytes memory proofRlp
    ) internal view returns (uint256) {
        RLPReader.RLPItem[] memory proofs = proofRlp.toRlpItem().toList();
        require(proofs.length == 2, "Invalid number of proofs");

        // Extract account proof
        Verifier.Account memory account = Verifier.extractAccountFromProof(
            ScrvusdVerifierV1.SCRVUSD_HASH,
            stateRoot,
            proofs[0].toList()
        );
        require(account.exists, "scrvUSD account does not exist");

        Verifier.SlotValue memory slot = Verifier.extractSlotValueFromProof(
            keccak256(abi.encode(PERIOD_SLOT)),
            account.storageRoot,
            proofs[1].toList()
        );
        require(slot.exists);

        return slot.value;
    }
}

verifyPeriodByStateRoot

ScrvusdVerifierV2.verifyPeriodByStateRoot(_block_number: uint256, _proof_rlp: bytes) -> bool

This function verifies the period by retrieving the state root for a given block number from the block hash oracle and then using a state proof to extract the period. The extracted period is used to update the scrvUSD oracle's profit_max_unlock_time.

Returns: a boolean indicating whether the update was successful.

Input Type Description
_block_number uint256 Block number for which to retrieve the state root
_proof_rlp bytes RLP-encoded state proof for the period
Source code
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;

import {ScrvusdVerifierV1, IBlockHashOracle} from "./ScrvusdVerifierV1.sol";
import {RLPReader} from "hamdiallam/Solidity-RLP@2.0.7/contracts/RLPReader.sol";
import {StateProofVerifier as Verifier} from "../../xdao/contracts/libs/StateProofVerifier.sol";

interface IScrvusdOracleV2 {
    function update_profit_max_unlock_time(
        uint256 _profit_max_unlock_time,
        uint256 _block_number
    ) external returns (bool);
}

contract ScrvusdVerifierV2 is ScrvusdVerifierV1 {
    using RLPReader for bytes;
    using RLPReader for RLPReader.RLPItem;

    uint256 internal PERIOD_SLOT = 37; // profit_max_unlock_time

    constructor(address _block_hash_oracle, address _scrvusd_oracle)
        ScrvusdVerifierV1(_block_hash_oracle, _scrvusd_oracle) {}

    /// @param _block_number Number of the block to use state root hash
    /// @param _proof_rlp The state proof of the period
    function verifyPeriodByStateRoot(
        uint256 _block_number,
        bytes memory _proof_rlp
    ) external returns (bool) {
        bytes32 state_root = IBlockHashOracle(ScrvusdVerifierV1.BLOCK_HASH_ORACLE).get_state_root(_block_number);

        uint256 period = _extractPeriodFromProof(state_root, _proof_rlp);
        return IScrvusdOracleV2(SCRVUSD_ORACLE).update_profit_max_unlock_time(period, _block_number);
    }

    /// @dev Extract period from the state proof using the given state root.
    function _extractPeriodFromProof(
        bytes32 stateRoot,
        bytes memory proofRlp
    ) internal view returns (uint256) {
        RLPReader.RLPItem[] memory proofs = proofRlp.toRlpItem().toList();
        require(proofs.length == 2, "Invalid number of proofs");

        // Extract account proof
        Verifier.Account memory account = Verifier.extractAccountFromProof(
            ScrvusdVerifierV1.SCRVUSD_HASH,
            stateRoot,
            proofs[0].toList()
        );
        require(account.exists, "scrvUSD account does not exist");

        Verifier.SlotValue memory slot = Verifier.extractSlotValueFromProof(
            keccak256(abi.encode(PERIOD_SLOT)),
            account.storageRoot,
            proofs[1].toList()
        );
        require(slot.exists);

        return slot.value;
    }
}