Skip to content

Curve Crosschain Gauges

Due to the x-chain gauge system, Curve allows to deploy liquidity gauges on alternate chains which are eligible for receiving CRV emissions and boosts.

In order for a sidechain gauge to receive CRV emissions, the system uses a two-gauge approach:

  • A Root Gauge, which is deployed on Ethereum and acts as the parent gauge for a child gauge deployed on other chains. This is the gauge that can be added to the GaugeController and is eligible to receive voting weight and therefore CRV emissions. Once a root gauge receives some weight and therefore CRV emissions, it can mint the according CRV emissions and transmit them to the child gauge on the target chain. All this is done in a permissionless way allowing anyone to transmit the CRV emissions to the child gauge.
  • A Child Gauge containing the standard logic of a Curve liquidity gauge on Ethereum, which is deployed on alternate chains and acts as the child gauge for the root gauge on Ethereum.

Smart Contracts

The cross-chain gauge factory requires components to be deployed both on Ethereum and on an alternate EVM compatible network.

  • RootGaugeFactory


    This is the main contract for deploying root gauges on Ethereum. It also serves as a registry for finding deployed gauges and the bridge wrapper contracts used to bridge CRV emissions to alternate chains.

    RootGaugeFactory.vy

  • ChildGaugeFactory


    This is the main contract for deploying child gauges on alternate chains. This contract also serves as a registry for finding deployed gauges and as a psuedo CRV minter where users can claim CRV emissions they are entitled to from LPing.

    ChildGaugeFactory.vy

  • RootGauge


    This is the implementation used for root gauges deployed on Ethereum.

    RootGauge.vy

  • ChildGauge


    This is the implementation used for child gauges deployed on alternate chains.

    ChildGauge.vy

  • Bridgers


    These contracts are used to bridge CRV emissions across chains. Due to the increasing number of networks Curve deploys to, bridge wrappers adhere to a specific interface and allow for a modular bridging system.

    Bridgers.vy

  • Updater


    This contract is used to transmit veCRV information across chains to a L2 VotingEscrow Oracle.

    Updater.vy

  • L2 VotingEscrow Oracle


    This contract is used to store veCRV information on child chains.

    L2 VotingEscrow Oracle.vy


Boosting on Sidechains

Before reading this section, it is recommended to understand how boosting works in general. For a basic understanding of how boosting works, see Boosting.

Crosschain Boosts

This system is farily new and is not rolled out on every chain yet. Crosschain boosts only work if there is a L2 VotingEscrow Oracle set in the ChildGaugeFactory for the child chain.

Because the VotingEscrow, where CRV are locked and user's veCRV informations are stored, is only deployed on Ethereum, a novel system was created to make crosschain boosts possible.

The idea of the system is pretty straight forward: an Updater contract on Ethereum queries the veCRV information of a user from the VotingEscrow contract on Ethereum and transmits the information to a L2 VotingEscrow Oracle on the child chain. This way, boosts on sidechains can be calculated using the veCRV information from Ethereum.

L2 VotingEscrow Oracle Example for Fraxtal

The Updater contract on Ethereum makes use of the update function to query and transmit the veCRV information of a user from the VotingEscrow on Ethereum to the L2 VotingEscrow Oracle on Fraxtal. For messaging, the Fraxtal: L1 Cross Domain Messenger Proxy is used to send the message. To relay the message, the Fraxtal: Cross Domain Messenger is used.

Updater.vy
# @version 0.3.10
"""
@title Updater
"""

interface OVMMessenger:
    def sendMessage(_target: address, _data: Bytes[1024], _gas_limit: uint32): nonpayable

interface OVMChain:
    def enqueueL2GasPrepaid() -> uint32: view

interface VotingEscrow:
    def epoch() -> uint256: view
    def point_history(_idx: uint256) -> Point: view
    def user_point_epoch(_user: address) -> uint256: view
    def user_point_history(_user: address, _idx: uint256) -> Point: view
    def locked(_user: address) -> LockedBalance: view
    def slope_changes(_ts: uint256) -> int128: view


struct LockedBalance:
    amount: int128
    end: uint256

struct Point:
    bias: int128
    slope: int128
    ts: uint256
    blk: uint256


WEEK: constant(uint256) = 86400 * 7
VOTING_ESCROW: public(constant(address)) = 0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2


ovm_chain: public(address)  # CanonicalTransactionChain
ovm_messenger: public(address)  # CrossDomainMessenger


@external
def __init__(_ovm_chain: address, _ovm_messenger: address):
    self.ovm_chain = _ovm_chain
    self.ovm_messenger = _ovm_messenger


@external
def update(_user: address = msg.sender, _gas_limit: uint32 = 0):
    # https://community.optimism.io/docs/developers/bridge/messaging/#for-l1-%E2%87%92-l2-transactions
    gas_limit: uint32 = _gas_limit
    if gas_limit == 0:
        gas_limit = OVMChain(self.ovm_chain).enqueueL2GasPrepaid()

    epoch: uint256 = VotingEscrow(VOTING_ESCROW).epoch()
    point_history: Point = VotingEscrow(VOTING_ESCROW).point_history(epoch)

    user_point_epoch: uint256 = VotingEscrow(VOTING_ESCROW).user_point_epoch(_user)
    user_point_history: Point = VotingEscrow(VOTING_ESCROW).user_point_history(_user, user_point_epoch)
    locked: LockedBalance = VotingEscrow(VOTING_ESCROW).locked(_user)

    start_time: uint256 = WEEK + (point_history.ts / WEEK) * WEEK
    slope_changes: int128[12] = empty(int128[12])

    for i in range(12):
        slope_changes[i] = VotingEscrow(VOTING_ESCROW).slope_changes(start_time + WEEK * i)

    OVMMessenger(self.ovm_messenger).sendMessage(
        self,
        _abi_encode(
            _user,
            epoch,
            point_history,
            user_point_epoch,
            user_point_history,
            locked,
            slope_changes,
            method_id=method_id("update(address,uint256,(int128,int128,uint256,uint256),uint256,(int128,int128,uint256,uint256),(int128,uint256),int128[12])")
        ),
        gas_limit
    )

Deploying a Sidechain Gauge

A sidechain gauge can be deployed by calling the deploy_gauge function of the ChildGaugeFactory on the respective chain. This creates a minimal proxy using Vyper’s built-in create_from_minimal_proxy function, which points to the ChildGauge implementation and initializes the ChildGauge with the provided parameters, such as LP token, salt, and manager.

Deploying a RootGauge AFTER deploying a ChildGauge

There is no specific order in which root and child gauges must be deployed, and deploying a root gauge is optional. It is perfectly fine to deploy only child gauges. In this case, the child gauge will not be linked to any root gauge and therefore will not be eligible to receive any CRV emissions (if the root gauge is added to the GaugeController).

It does not matter if a root gauge is deployed before or after the child gauge. However, to link the root gauge to the child gauge, the root gauge must be deployed using the same salt as the child gauge.

Additionally, a sidechain gauge can also be deployed directly from the RootGaugeFactory on Ethereum. This is achieved using a call_proxy, which acts as an intermediary contract to facilitate cross-chain calls. Currently, this feature is not enabled, and the call_proxy contract has not been set.


Killing Sidechain Gauges

Killing a gauge essentially means cutting off all CRV emissions to the gauge by setting the inflation rate to 0.

Although each sidechain gauge has an is_killed variable and a set_killed function to modify its killed status, these do not affect the gauge directly. To kill sidechain gauges, the root gauge must be killed. This is done by setting the is_killed variable to True by calling the set_killed function. Only the owner, which is controlled by the CurveDAO and the EmergencyDAO of the RootGaugeFactory, can call this function.1


  1. The owner of the RootGaugeFactory is set to a proxy contract controlled by the CurveDAO and EmergencyDAO.