Skip to main content

L2 veCRV Verifiers

L2 verifier contracts are used to securely synchronize veCRV and related state from Ethereum mainnet (L1) to supported L2s. They validate Merkle proofs and block data from L1, allowing trust-minimized updates of veCRV balances, total supply, and delegation state on L2.


veCRV Verifier

The VecrvVerifier contract is used to verify and update the total supply and individual balances of veCRV on L2s by validating state proofs from L1. It enables trust-minimized synchronization of veCRV state by accepting Merkle proofs and block data, and updating the canonical veCRV oracle with supply and balance changes. This contract is typically called by relayers or bridges to reflect L1 veCRV state on L2.

VecrvVerifier.sol

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

The VecrvVerifier contract is deployed at the following addresses:

{ }Contract ABI
[{"inputs":[{"internalType":"address","name":"_block_hash_oracle","type":"address"},{"internalType":"address","name":"_vecrv_oracle","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"BLOCK_HASH_ORACLE","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MIN_SLOPE_CHANGES_CNT","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"VE_ORACLE","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_user","type":"address"},{"internalType":"bytes","name":"_block_header_rlp","type":"bytes"},{"internalType":"bytes","name":"_proof_rlp","type":"bytes"}],"name":"verifyBalanceByBlockHash","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_user","type":"address"},{"internalType":"uint256","name":"_block_number","type":"uint256"},{"internalType":"bytes","name":"_proof_rlp","type":"bytes"}],"name":"verifyBalanceByStateRoot","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes","name":"_block_header_rlp","type":"bytes"},{"internalType":"bytes","name":"_proof_rlp","type":"bytes"}],"name":"verifyTotalByBlockHash","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_block_number","type":"uint256"},{"internalType":"bytes","name":"_proof_rlp","type":"bytes"}],"name":"verifyTotalByStateRoot","outputs":[],"stateMutability":"nonpayable","type":"function"}]

BLOCK_HASH_ORACLE

VecrvVerifier.BLOCK_HASH_ORACLE() -> address: view

Getter for the block hash oracle contract, which is used to retrieve block hashes and state roots for verification.

Returns: block hash oracle (address).

<>Source code
VecrvVerifier.sol
address public immutable BLOCK_HASH_ORACLE;
Example

This example returns the block hash oracle on Optimism.

>>> VecrvVerifier.BLOCK_HASH_ORACLE()
'0xeB896fB7D1AaE921d586B0E5a037496aFd3E2412'

MIN_SLOPE_CHANGES_CNT

VecrvVerifier.MIN_SLOPE_CHANGES_CNT() -> uint256: view

Returns the minimum number of slope changes required for a valid proof. This is set to 4, corresponding to 1 month (assuming 1 week per slope change).

Returns: minimum slope changes count (uint256).

<>Source code
VecrvVerifierCore.sol
uint256 public constant MIN_SLOPE_CHANGES_CNT = 4; // 1 month
Example

This example returns the minimum slope change count.

>>> VecrvVerifier.MIN_SLOPE_CHANGES_CNT()
4

VE_ORACLE

VecrvVerifier.VE_ORACLE() -> address: view

Getter for the veCRV oracle contract, which is called to update the total supply and user balances after verification.

Returns: veCRV oracle (address).

<>Source code
VecrvVerifierCore.sol
address public immutable VE_ORACLE;

constructor(address _ve_oracle) {
VE_ORACLE = _ve_oracle;
}
Example

This example returns the VE_ORACLE address on Optimism.

>>> VecrvVerifier.VE_ORACLE()
'0xF1946D4879646e0FCD8F5bb32a5636ED8055176D'

verifyBalanceByBlockHash

VecrvVerifier.verifyBalanceByBlockHash(address _user, bytes memory _block_header_rlp, bytes memory _proof_rlp) external

Verifies a user's veCRV balance and updates the total veCRV supply using a block hash. This function is intended for use with RLP-encoded block headers and state proofs.

InputTypeDescription
_useraddressUser to verify the balance for
_block_header_rlpbytesRLP-encoded block header
_proof_rlpbytesState proof of the parameters
<>Source code
/// @param _user User to verify balance for
/// @param _block_header_rlp The RLP-encoded block header
/// @param _proof_rlp The state proof of the parameters
function verifyBalanceByBlockHash(
address _user,
bytes memory _block_header_rlp,
bytes memory _proof_rlp
) external {
RLPReader.RLPItem[] memory proofs = _proof_rlp.toRlpItem().toList();
require(proofs.length >= 1, "Invalid number of proofs");
(bytes32 storage_root, uint256 block_number) = _extractAccountStorageRoot(_block_header_rlp, proofs[0]);

_updateTotal(storage_root, block_number, proofs[1].toList());
_updateBalance(_user, storage_root, block_number, proofs[2].toList());
}

function _extractAccountStorageRoot(
bytes memory _block_header_rlp,
RLPReader.RLPItem memory account_proof
) internal returns (bytes32, 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"
);
return (_extractAccountStorageRoot(block_header.stateRootHash, account_proof), block_header.number);
}
Example
>>> soon

verifyBalanceByStateRoot

VecrvVerifier.verifyBalanceByStateRoot(address _user, uint256 _block_number, bytes memory _proof_rlp) external

Verifies a user's veCRV balance and updates the total veCRV supply using a state root obtained from the block hash oracle.

InputTypeDescription
_useraddressUser to verify the balance for
_block_numberuint256Block number to use state root
_proof_rlpbytesState proof of the parameters
<>Source code
/// @param _user User to verify balance for
/// @param _block_number Number of the block to use state root hash
/// @param _proof_rlp The state proof of the parameters
function verifyBalanceByStateRoot(
address _user,
uint256 _block_number,
bytes memory _proof_rlp
) external {
RLPReader.RLPItem[] memory proofs = _proof_rlp.toRlpItem().toList();
require(proofs.length >= 1, "Invalid number of proofs");
bytes32 state_root = IBlockHashOracle(BLOCK_HASH_ORACLE).get_state_root(_block_number);
bytes32 storage_root = _extractAccountStorageRoot(state_root, proofs[0]);

_updateTotal(storage_root, _block_number, proofs[1].toList());
_updateBalance(_user, storage_root, _block_number, proofs[2].toList());
}
Example
>>> soon

verifyTotalByBlockHash

VecrvVerifier.verifyTotalByBlockHash(bytes memory _block_header_rlp, bytes memory _proof_rlp) external

Verifies and updates the total veCRV supply using a block hash and state proof. Intended for use with RLP-encoded block headers and state proofs. The proofs must be constructed off-chain and provided as input.

InputTypeDescription
_block_header_rlpbytesRLP-encoded block header from L1
_proof_rlpbytesState proof of the parameters
<>Source code
VecrvVerifier.sol
/// @param _block_header_rlp The RLP-encoded block header
/// @param _proof_rlp The state proof of the parameters
function verifyTotalByBlockHash(
bytes memory _block_header_rlp,
bytes memory _proof_rlp
) external {
RLPReader.RLPItem[] memory proofs = _proof_rlp.toRlpItem().toList();
require(proofs.length >= 1, "Invalid number of proofs");
(bytes32 storage_root, uint256 block_number) = _extractAccountStorageRoot(_block_header_rlp, proofs[0]);

_updateTotal(storage_root, block_number, proofs[1].toList());
}
Example
>>> soon

verifyTotalByStateRoot

VecrvVerifier.verifyTotalByStateRoot(uint256 _block_number, bytes memory _proof_rlp) external

Verifies and updates the total veCRV supply using a state root obtained from the block hash oracle. The proofs must be constructed off-chain and provided as input.

InputTypeDescription
_block_numberuint256Block number to use state root
_proof_rlpbytesState proof of the parameters
<>Source code
VecrvVerifier.sol
/// @param _block_number Number of the block to use state root hash
/// @param _proof_rlp The state proof of the parameters
function verifyTotalByStateRoot(
uint256 _block_number,
bytes memory _proof_rlp
) external {
RLPReader.RLPItem[] memory proofs = _proof_rlp.toRlpItem().toList();
require(proofs.length >= 1, "Invalid number of proofs");
bytes32 state_root = IBlockHashOracle(BLOCK_HASH_ORACLE).get_state_root(_block_number);
bytes32 storage_root = _extractAccountStorageRoot(state_root, proofs[0]);

_updateTotal(storage_root, _block_number, proofs[1].toList());
}
Example
>>> soon

Delegation Verifier

The DelegationVerifier contract is used to verify and update veCRV delegation state on L2s by validating state proofs from L1. It enables trust-minimized synchronization of delegated veCRV balances by accepting Merkle proofs and block data, and updating the canonical veCRV oracle with delegation changes. This contract is typically called by relayers or bridges to reflect L1 delegation state on L2.

DelegationVerifier.sol

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

The DelegationVerifier contract is deployed at the following addresses:

{ }Contract ABI
[{"inputs":[{"internalType":"address","name":"_block_hash_oracle","type":"address"},{"internalType":"address","name":"_vecrv_oracle","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"BLOCK_HASH_ORACLE","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"VE_ORACLE","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_from","type":"address"},{"internalType":"bytes","name":"_block_header_rlp","type":"bytes"},{"internalType":"bytes","name":"_proof_rlp","type":"bytes"}],"name":"verifyDelegationByBlockHash","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_from","type":"address"},{"internalType":"uint256","name":"_block_number","type":"uint256"},{"internalType":"bytes","name":"_proof_rlp","type":"bytes"}],"name":"verifyDelegationByStateRoot","outputs":[],"stateMutability":"nonpayable","type":"function"}]

BLOCK_HASH_ORACLE

DelegationVerifier.BLOCK_HASH_ORACLE() -> address: view

Getter for the block hash oracle contract, which is used to retrieve block hashes and state roots for verification.

Returns: block hash oracle (address).

<>Source code
DelegationVerifier.sol
address public immutable BLOCK_HASH_ORACLE;

constructor(address _block_hash_oracle, address _vecrv_oracle)
{
BLOCK_HASH_ORACLE = _block_hash_oracle;
VE_ORACLE = _vecrv_oracle;
}
Example
>>> DelegationVerifier.BLOCK_HASH_ORACLE()
'0xeB896fB7D1AaE921d586B0E5a037496aFd3E2412'

VE_ORACLE

DelegationVerifier.VE_ORACLE() -> address: view

Getter for the veCRV oracle contract, which is called to update delegation state after verification.

Returns: veCRV oracle (address).

<>Source code
DelegationVerifier.sol
address public immutable VE_ORACLE;

constructor(address _block_hash_oracle, address _vecrv_oracle)
{
BLOCK_HASH_ORACLE = _block_hash_oracle;
VE_ORACLE = _vecrv_oracle;
}
Example
>>> DelegationVerifier.VE_ORACLE()
'0xF1946D4879646e0FCD8F5bb32a5636ED8055176D'

verifyDelegationByBlockHash

DelegationVerifier.verifyDelegationByBlockHash(address _from, bytes memory _block_header_rlp, bytes memory _proof_rlp) external

Verifies and updates the delegation of veCRV balance from _from to the delegated address using a block hash. This function is intended for use with RLP-encoded block headers and state proofs.

InputTypeDescription
_fromaddressAddress from which balance is delegated
_block_header_rlpbytesRLP-encoded block header from L1
_proof_rlpbytesState proof of the parameters
<>Source code
DelegationVerifier.sol
interface IBlockHashOracle {
function get_block_hash(uint256 _number) external view returns (bytes32);
function get_state_root(uint256 _number) external view returns (bytes32);
}

interface IVecrvOracle {
function update_delegation(
address from,
address to,
uint256 block_number
) external;
}

function verifyDelegationByBlockHash(
address _from,
bytes memory _block_header_rlp,
bytes memory _proof_rlp
) external {
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"
);

return _updateDelegation(_from, block_header.number, block_header.stateRootHash, _proof_rlp);
}

/// @dev Update delegation using proof. `blockNumber` is used for updates linearization
function _updateDelegation(
address from,
uint256 blockNumber,
bytes32 stateRoot,
bytes memory proofRlp
) internal {
RLPReader.RLPItem[] memory proofs = proofRlp.toRlpItem().toList();
require(proofs.length == 2, "Invalid number of proofs");

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

// Extract slot values
address to = address(uint160(Verifier.extractSlotValueFromProof(
keccak256(abi.encode(
keccak256(abi.encode(
keccak256(abi.encode(1, block.chainid)), // slot of delegation_from[chain.id][]
from
))
)),
account.storageRoot,
proofs[1].toList()
).value));
require(to != VE_DELEGATE, "Delegate not set");

return IVecrvOracle(VE_ORACLE).update_delegation(from, to, blockNumber);
}
Example
>>> soon

verifyDelegationByStateRoot

DelegationVerifier.verifyDelegationByStateRoot(address _from, uint256 _block_number, bytes memory _proof_rlp) external

Verifies and updates the delegation of veCRV balance from _from to the delegated address using a state root obtained from the block hash oracle.

InputTypeDescription
_fromaddressAddress from which balance is delegated
_block_numberuint256Block number to use state root
_proof_rlpbytesState proof of the parameters
<>Source code
DelegationVerifier.sol
interface IBlockHashOracle {
function get_block_hash(uint256 _number) external view returns (bytes32);
function get_state_root(uint256 _number) external view returns (bytes32);
}

interface IVecrvOracle {
function update_delegation(
address from,
address to,
uint256 block_number
) external;
}

/// @param _from Address from which balance is delegated
/// @param _block_number Number of the block to use state root hash
/// @param _proof_rlp The state proof of the parameters
function verifyDelegationByStateRoot(
address _from,
uint256 _block_number,
bytes memory _proof_rlp
) external {
bytes32 state_root = IBlockHashOracle(BLOCK_HASH_ORACLE).get_state_root(_block_number);

return _updateDelegation(_from, _block_number, state_root, _proof_rlp);
}

/// @dev Update delegation using proof. `blockNumber` is used for updates linearization
function _updateDelegation(
address from,
uint256 blockNumber,
bytes32 stateRoot,
bytes memory proofRlp
) internal {
RLPReader.RLPItem[] memory proofs = proofRlp.toRlpItem().toList();
require(proofs.length == 2, "Invalid number of proofs");

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

// Extract slot values
address to = address(uint160(Verifier.extractSlotValueFromProof(
keccak256(abi.encode(
keccak256(abi.encode(
keccak256(abi.encode(1, block.chainid)), // slot of delegation_from[chain.id][]
from
))
)),
account.storageRoot,
proofs[1].toList()
).value));
require(to != VE_DELEGATE, "Delegate not set");

return IVecrvOracle(VE_ORACLE).update_delegation(from, to, blockNumber);
}
Example
>>> soon