Pre-Proposal: L1 <> L2 Migration Workflow

Motivation

Today, the protocol contracts are deployed on L1 Ethereum and the gas fees for transactions depend on the limited blockspace of L1 which is currently consistently being filled by other popular applications (i.e. NFTs, DeFi, etc.) resulting in high gas fees. A reduction in the gas consumption of contracts can only help so much and in order to reduce the transactions fees by an order of a magnitude the contracts will likely need to be deployed to a L2 with substantially lower gas prices. An important question that needs to be answered in order to move the contracts into a L2 is: how will users move their staked LPT?

Both delegators and orchestrators need to wait through a 7 round (~6-7 day) unbonding period in order to withdraw their staked LPT. As a result, as is, the migration process to another domain would present a lot of friction for users and could take a long time due to the unbonding period requirement.

Ideally, the migration process could have the following properties:

  • Users can submit a single transaction on L1 to migrate staked LPT to L2 contracts
  • The supply of LPT on both L1 and the L2 is controlled by the same monetary policy

This post outlines ideas for a L1 <> L2 migration that would work similarly to how Uniswap LPs migrate liquidity from one version of Uniswap to another and that would support inflationary LPT minting on L2 while still allowing bi-directional transfers of LPT between L1 and L2.

L1 → L2 stake migration workflow

The BondingManager contract on L1 Ethereum can be upgraded with the following function:

// L1
interface IBondingManager {
    function migrateStake(address _delegator) external returns (uint256);
}

The migrateStake() function can only be called by a Migrator contract on L1 to instantly unbond (without an unbonding period) and withdraw the delegator’s staked LPT into the Migrator contract. If the specified delegator is an orchestrator (which is a self-delegated delegator), the orchestrator is deactivated & unregistered in the BondingManager.

// L1
contract BondingManager {
    function migrateStake(address _delegator)
        external
        autoClaimEarnings
        onlyMigrator
        returns (uint256)
    {
        uint256 stake = delegators[_delegator].bondedAmount;

        // Zero out relevant storage values
        // ...

        // Move LPT to Migrator
        minter().trustedWithdrawTokens(address(migrator()), stake);

        return stake;
    }
}

The migrateStake() function will not touch a delegator’s ETH fees. The delegator will still be able to withdraw its ETH fees on L1 using the withdrawFees() function on the BondingManager.

The Migrator contract is responsible for escrowing LPT into a gateway contract that bridges L1 with the L2 and also submitting a message that can be executed on L2 to stake the LPT in the staking contract deployed there.

// L1
contract Migrator {
    IGateway public immutable gateway;
    ILivepeerToken public immutable token;
    IBondingManager public immutable bondingManager;

    function migrate(address _destOrchestrator)
        external
    {
        // Pull staked LPT from BondingManager
        uint256 stakeToMigrate = bondingManager.migrateStake(msg.sender);
        // Approve gateway to pull LPT from this contract
        token.approve(gateway, stakeToMigrate);
    
        // Call gateway to:
        // - Escrow LPT in gateway
        // - Pass message to mint LPT on L2 and stake for
        // the delegator in the staking contract there
    }
}

The _destOrchestrator argument offers the caller flexibility in specifying the orchestrator address that migrated stake should be delegated to on L2. _destOrchestrator may be different from the caller’s current orchestrator on L1. The ability to specify a different orchestrator when migrating stake will also be important if the current orchestrator on L1 has not yet migrated its stake to L2. In this scenario, a delegator that wants to migrate to the L2 likely would want to delegate its stake to a different orchestrator that has already migrated to the L2.

Users will call the migrate() function on the Migrator in order to execute the migration to the destination chain.

The L2 staking contract could support the following function:

// L2
interface IStakingManager {
    function stakeFor(
        address _delegator,
        address _orchestrator,
        uint256 _amount
    )
        external;
}

In contrast with the L1 BondingManager, which only allows LPT to be staked and delegated by the msg.sender in the bond() function, the L2 StakingManager would allow LPT to be staked and delegated on behalf of a specified address that does not have to be msg.sender. This function allows the Migrator to stake and delegate LPT in the StakingManager on behalf of a user that is migrating to the L2.

// L2
contract Migrator {
    IStakingManager public immutable stakingManager;

    function migrate(
        address _delegator,
        address _orchestrator,
        uint256 _amount
    )
        external
        onlyBridge
    {
        stakingManager.stakeFor(_delegator, _orchestrator, _amount);
    }
}

The Migrator on the L2 is just responsible for calling the stakeFor() function on the BondingManager to stake LPT on behalf of the user that triggered a migration on L1.

L2 inflationary minting

A remaining question is how to handle the fact that the LPT escrowed in a L1 gateway will not be able to cover all the LPT that could be burned on a L2 if inflationary minting is supported on the L2. One way to address this by allowing a LPT burn in the L2 gateway to trigger LPT minting on L1.

Note that in practice the L1 and L2 gateways may be composed of multiple contracts. For example, in Arbitrum there could be arbitrary message passing bridge, a ERC-20 token bridge to move liquid LPT between L1 & L2 and a staked LPT bridge to move staked LPT from L1 to L2.

L2 workflow

The minting authority for the L2 LPT contract is set to a Minter contract - the only address that can mint new L2 LPT is the Minter. The Minter can delegating minting authority to one or many other addresses allowing the addresses to trigger minting through the Minter. In this workflow, the Minter delegates minting authority to:

  • The L2 gateway
  • StakingManager

The L2 gateway will trigger minting of L2 LPT through the Minter when an equivalent amount of L1 LPT was escrowed in the L1 gateway.

The StakingManager will trigger minting of L2 LPT through the Minter when LPT inflationary rewards are minted for staked orchestrators.

If a user wants to move back to L1, they can burn L2 LPT in the L2 gateway.

L1 workflow

The minting authority for the L1 LPT contract is also set to a Minter contract similar to on L2. On L1, the Minter only delegates minting authority to the L1 gateway. So, new L1 LPT can only be minted when triggered by an L2 message. Note this means that the L1 LPT minting mechanism completely trusts the L2 minting mechanism - if there are any bugs/issues with the L2 minting mechanism then there is nothing stopping the minting of excess L1 LPT (outside of pause mechanisms built-in to the Minter which are outside of the scope of this post).

After a user burns L2 LPT in the L2 gateway, a message is passed to the L1 gateway.

Let’s assume that X L2 LPT was burned.

If the L1 gateway LPT escrow >= X, then X L1 LPT is transferred to the user on L1.

If the L1 gateway LPT escrow < X, then the L1 gateway will mint X - L1 gateway LPT escrow L1 LPT which is sent into the L1 gateway escrow. Now, with a topped off escrow, the L1 bridge will transfer X L1 LPT to the user on L1.

Next Steps

  • Create a full prototype implementation iterating on these ideas as is needed (already in-progress using Arbitrum as the destination L2)
  • Decide on the destination L2 (Optimism, Arbitrum, etc.)
  • If these ideas are validated in a prototype implementation and there is community support, create a draft LIP
1 Like