Incentivizing Curve stETH liquidity with LDO (tech details)

This proposal details a way of enabling LDO rewards for Curve stETH liquidity providers using the liquidity gauge (still to be deployed, see Curve DAO vote #34) and a custom rewards contract.

(sorry for the lack of links, new users are not allowed to put more than two of them in a post)

About Curve gauges

Curve’s liquidity gauges (source) work in the following way:

  1. A user provides the target token(s) into a liquidity pool, getting the LP (liquidity provider) tokens in return.

  2. If there’s a gauge deployed for that pool, the user can stake their LP tokens into that gauge. As long as LP tokens remain staked in the gauge contract, the user receives their share of the CRV inflation, proportional to the relative weight of the pool among other pools (determined by the Curve DAO on a periodic basis) and to the user’s relative stake in the pool’s total liquidity.

  3. The gauge admin can also assign an external rewards contract that will be called by the gauge each time the total amount of staked LP tokens changes (deposit/withdrawal of LP tokens) and each time someone wants to collect their rewards. This contract may assign reward tokens to the gauge’s address in response. If this happens, the gauge claims the tokens and distributes them between the gauge LP stakers, proportionally to their relative stake.

How, exactly, the gauge distributes the rewards? Each time any user’s LP tokens stake changes, two things get (effectively) calculated: the amount of rewards that all users together should receive since the previous change, and the share of each user’s staked LP tokens within the total pool’s stake. Since the gauge knows that the total stake, as well as each user’s stake, hasn’t changed in between, it can calculate how much rewards should go to each user. The word “effectively” was used above because the gauge doesn’t actually run the unbounded loop over all stakers but instead smartly uses partial integration to do the calculation for each user only when that user performs some action.

The user’s rewards (if any) are transferred to the user’s address each time that specific user deposits or withdraws LP tokens, as well as when the user explicitly requests to collect the rewards. Delayed distribution is not supported.

Adding LDO rewards

The proposal is to deploy the Synthetix StakingRewards contract forked by Ben Hauser and to propose setting it as the external rewards contract to the stETH/ETH Curve pool’s gauge, aiming to fund it with LDO tokens by Lido DAO voting.

The StakingRewards contract is more complex than it’s actually needed for the use with Curve gauges since it supports tracking individual users’ rewards — the gauge already does this, as detailed above, so there will be only one “staker” from the rewards contract’s point of view: the gauge contract. But this rewards contract seems to have already been deployed in production so it makes more practical sense to use it rather than to write another, simpler, version that will still require careful testing.

The StakingRewards contract operates by distributing a set amount of reward tokens (LDO in our case) within the set time period. To start a new period, the rewards distribution operator (Lido DAO voting in our case) calls notifyRewardAmount function on the rewards contract, passing the amount of tokens to be distributed throughout the period being started. The rewards contract then transfers LDO tokens to its balance using ldoToken.transferFrom, meaning that it should be allowed to spend the set amount of LDO tokens. The duration of reward periods is defined during the rewards contract deploy, but can be changed afterwards given that there’s no active rewards period (i.e. no rewards are being issued).

The important detail here is that, ideally, we want the next rewards period to start right after the previous one finished. This can be done by setting the period duration to e.g. a week and managing to finish the DAO voting for the next period before the current period ends. All that’s left to be done then is enacting the passed vote.

There’s one difficulty, though. The new period may be started even when the current one hasn’t finished yet, in which case the undistributed amount of tokens from the current period will be added to the new one, and the new period will start from the present moment. For example, suppose that the contract is configured to use six-day periods, and the current period was started with 100 tokens. If the DAO vote to start the new period was enacted three days within the current one, supplying 200 tokens, then the new period will start immediately, will end in six days, and will distribute 250 tokens: 200 freshly-supplied and 50 leftover tokens from the interrupted period.

Since anyone can enact a passed vote, this can be exploited by liquidity providers who may do that to increase their short-term rewards. There are two obvious ways to fix this:

  1. Modify the StakingRewards fork to disallow starting a new period before the current one finishes.
  2. Deploy an intermediate contract owned by the DAO voting that will do the same instead, and make it the rewards manager (so only that contract may start new reward periods).

I’m leaning toward the first option since it introduces less complexity.

DAO routine operations

Initially, the DAO votes to approve the deployed StakingRewards contract to spend the amount of unassigned LDO tokens that’s enough for, say, several months of rewards.

Each week (or any other period agreed upon) the DAO initiates a vote to start a new reward period and distribute some amount of LDO tokens as LP reward (e.g. to call notifyRewardAmount on StakingRewards or on the intermediate contract).

There is a periodic job that tries to enact the passed votes, so the gap between each two consecutive reward periods is minimized.

When the allowance for the rewards contract to spend LDO becomes insufficient to start a new rewards period, the DAO initiates a vote to increase the allowance.

1 Like
1 Like

The reward contract deployment ETA is 17:00 UTC today, might be earlier. After that it’ll take about a day or so for a vote to transfer LDO to reward contract; after the transfer rewards will start accruing to people staking liquidity in Curve gauge.

For the Curve LPs the timeline work like this:

  • Curve pool is accepted into the gauge (after the vote is conducted and executed)
  • People stake LP tokens of the pool into the gauge and start accruing CRV reward
  • LDO rewards contract is funded
  • People who stake LP tokens in the gauge start automatically accrue LDO rewards

Rewards are accrued per block and can be withdrawn at any time, just like with CRV ones.


Any plan to provide retroactive reward to current LPs?

No proposals of that nature that I’m aware of.

stETH Curve pool LP incentivization

GitHub repo: GitHub - lidofinance/staking-rewards-manager: Staking rewards manager
StakingRewards contract: Contract Address 0x99ac10631F69C753DDb595D074422a0922D9056B | Etherscan
RewardsManager contract: Contract Address 0x753D5167C31fBEB5b49624314d74A957Eb271709 | Etherscan

DAO voting for transferring 0.5% of LDO total supply to RewardsManager contract: Aragon

The setup works in the following way:

  1. DAO transfers LDO tokens to RewardsManager.
  2. Someone calls start_next_rewards_period on RewardsManager.

The latter results in the following:

  1. LDO tokens get transferred from RewardsManager to StakingRewards.
  2. StakingRewards starts distributing the received LDO tokens to Curve stETH LPs. The distribution period is one month.

If DAO performs the next LDO transfer while the current rewards are still being distributed, then the next rewards distribution can be started right after the current one has finished by calling start_next_rewards_period one more time. The RewardsManager contract is needed to prevent anyone from doing this until the current period has ended.

1 Like

Contract can be reused by reseeding it with the token transfer. The amount of LDO being distributed throughout the next rewards period is determined by the LDO balance of RewardsManager at the moment of start_next_rewards_period call. The duration of the reward period can be configured by DAO voting: the DAO Agent app is the owner of both RewardsManager and StakingRewards and is allowed to call StakingRewards.setRewardsDuration.