Skip to content

Commit

Permalink
external incentives
Browse files Browse the repository at this point in the history
  • Loading branch information
p0mvn committed Jun 8, 2023
1 parent 636b017 commit 4c65a87
Show file tree
Hide file tree
Showing 21 changed files with 547 additions and 435 deletions.
5 changes: 5 additions & 0 deletions proto/osmosis/incentives/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ message MsgCreateGauge {
// num_epochs_paid_over is the number of epochs distribution will be completed
// over
uint64 num_epochs_paid_over = 6;

// pool_id is the ID of the pool that the gauge is meant to be associated
// with. if pool_id is set, then the "QueryCondition.LockQueryType" must be
// "NoLock" with all other fields unset.
uint64 pool_id = 7;
}
message MsgCreateGaugeResponse {}

Expand Down
1 change: 1 addition & 0 deletions proto/osmosis/lockup/lock.proto
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ enum LockQueryType {

ByDuration = 0;
ByTime = 1;
NoLock = 2;
}

// QueryCondition is a struct used for querying locks upon different conditions.
Expand Down
2 changes: 1 addition & 1 deletion x/cosmwasmpool/cosmwasm/msg/transmuter/transmuter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func (s *TransmuterSuite) TestFunctionalTransmuter() {
LockQueryType: lockuptypes.ByDuration,
Denom: expectedShareDenom,
Duration: lockDuration,
}, s.Ctx.BlockTime(), 1)
}, s.Ctx.BlockTime(), 1, 0)
s.Require().NoError(err)
gauge, err := s.App.IncentivesKeeper.GetGaugeByID(s.Ctx, gaugeId)
s.Require().NoError(err)
Expand Down
2 changes: 1 addition & 1 deletion x/incentives/keeper/bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ func benchmarkDistributionLogic(b *testing.B, numAccts, numDenoms, numGauges, nu
numEpochsPaidOver = uint64(r.Int63n(durationMillisecs/millisecsPerEpoch)) + 1
}

gaugeId, err := app.IncentivesKeeper.CreateGauge(ctx, isPerpetual, addr, rewards, distributeTo, startTime, numEpochsPaidOver)
gaugeId, err := app.IncentivesKeeper.CreateGauge(ctx, isPerpetual, addr, rewards, distributeTo, startTime, numEpochsPaidOver, 0)
if err != nil {
fmt.Printf("Create Gauge, %v\n", err)
b.FailNow()
Expand Down
190 changes: 88 additions & 102 deletions x/incentives/keeper/distribute.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package keeper

import (
"errors"
"fmt"
"time"

Expand All @@ -11,7 +10,6 @@ import (

"github.com/osmosis-labs/osmosis/v16/x/incentives/types"
lockuptypes "github.com/osmosis-labs/osmosis/v16/x/lockup/types"
poolincentivestypes "github.com/osmosis-labs/osmosis/v16/x/pool-incentives/types"
poolmanagertypes "github.com/osmosis-labs/osmosis/v16/x/poolmanager/types"
)

Expand Down Expand Up @@ -268,42 +266,13 @@ func (k Keeper) distributeSyntheticInternal(
return k.distributeInternal(ctx, gauge, sortedAndTrimmedQualifiedLocks, distrInfo)
}

// distributeConcentratedLiquidity runs the distribution logic for CL pools only. It creates new incentive record with osmo incentives
// and distributes all the tokens to the dedicated pool
func (k Keeper) distributeConcentratedLiquidity(ctx sdk.Context, poolId uint64, sender sdk.AccAddress, incentiveCoin sdk.Coin, emissionRate sdk.Dec, startTime time.Time, minUptime time.Duration, gauge types.Gauge) error {
_, err := k.clk.CreateIncentive(ctx,
poolId,
sender,
incentiveCoin,
emissionRate,
startTime,
minUptime,
)
if err != nil {
return err
}

// updateGaugePostDistribute adds the coins that were just distributed to the gauge's distributed coins field.
err = k.updateGaugePostDistribute(ctx, gauge, sdk.NewCoins(incentiveCoin))
if err != nil {
return err
}
return nil
}

// distributeInternal runs the distribution logic for a gauge, and adds the sends to
// the distrInfo struct. It also updates the gauge for the distribution.
// Locks is expected to be the correct set of lock recipients for this gauge.
func (k Keeper) distributeInternal(
ctx sdk.Context, gauge types.Gauge, locks []lockuptypes.PeriodLock, distrInfo *distributionInfo,
) (sdk.Coins, error) {
totalDistrCoins := sdk.NewCoins()
denom := lockuptypes.NativeDenom(gauge.DistributeTo.Denom)
lockSum := lockuptypes.SumLocksByDenom(locks, denom)

if lockSum.IsZero() {
return nil, nil
}

remainCoins := gauge.Coins.Sub(gauge.DistributedCoins)
// if its a perpetual gauge, we set remaining epochs to 1.
Expand All @@ -313,34 +282,90 @@ func (k Keeper) distributeInternal(
remainEpochs = gauge.NumEpochsPaidOver - gauge.FilledEpochs
}

for _, lock := range locks {
distrCoins := sdk.Coins{}
for _, coin := range remainCoins {
// distribution amount = gauge_size * denom_lock_amount / (total_denom_lock_amount * remain_epochs)
denomLockAmt := lock.Coins.AmountOfNoDenomValidation(denom)
amt := coin.Amount.Mul(denomLockAmt).Quo(lockSum.Mul(sdk.NewInt(int64(remainEpochs))))
if amt.IsPositive() {
newlyDistributedCoin := sdk.Coin{Denom: coin.Denom, Amount: amt}
distrCoins = distrCoins.Add(newlyDistributedCoin)
}
// This is a no lock distribution flow that assumes that we have a pool associated with the gauge.
// Currently, this flow is only used for CL pools. Fails if the pool is not found.
// Fails if the pool found is not a CL pool.
if gauge.DistributeTo.LockQueryType == lockuptypes.NoLock {
pool, err := k.GetPoolFromGaugeId(ctx, gauge.Id, gauge.DistributeTo.Duration)

if err != nil {
return nil, err
}
distrCoins = distrCoins.Sort()
if distrCoins.Empty() {
continue

poolType := pool.GetType()
if poolType != poolmanagertypes.Concentrated {
return nil, fmt.Errorf("pool type %s is not supported for no lock distribution", poolType)
}
// update the amount for that address
rewardReceiver := lock.RewardReceiverAddress

// if the reward receiver stored in state is an empty string, it indicates that the owner is the reward receiver.
if rewardReceiver == "" {
rewardReceiver = lock.Owner
// Get distribution epoch duration. This is used to calculate the emission rate.
currentEpoch := k.GetEpochInfo(ctx)

// only want to run this logic if the gaugeId is associated with CL PoolId
for _, remainCoin := range remainCoins {
remainAmountPerEpoch := remainCoin.Amount.Quo(sdk.NewIntFromUint64(remainEpochs))
remainCoinPerEpoch := sdk.NewCoin(remainCoin.Denom, remainAmountPerEpoch)

// emissionRate calculates amount of tokens to emit per second
// for ex: 10000tokens to be distributed over 1day epoch will be 1000 tokens ÷ 86,400 seconds ≈ 0.01157 tokens per second (truncated)
// Note: reason why we do millisecond conversion is because floats are non-deterministic so if someone refactors this and accidentally
// uses the return of currEpoch.Duration.Seconds() in math operations, this will lead to an app hash.
emissionRate := sdk.NewDecFromInt(remainAmountPerEpoch).QuoTruncate(sdk.NewDec(currentEpoch.Duration.Milliseconds()).QuoInt(sdk.NewInt(1000)))

_, err := k.clk.CreateIncentive(ctx,
pool.GetId(),
k.ak.GetModuleAddress(types.ModuleName),
remainCoinPerEpoch,
emissionRate,
gauge.GetStartTime(),
// Note that the minimum uptime does not affect the distribution of incentives from the gauge and
// thus can be any value authorized by the CL module.
types.DefaultConcentratedUptime,
)
if err != nil {
return nil, err
}

totalDistrCoins = totalDistrCoins.Add(remainCoinPerEpoch)
}
err := distrInfo.addLockRewards(lock.Owner, rewardReceiver, distrCoins)
if err != nil {
return nil, err

} else {
// This is a standard lock distribution flow that assumes that we have locks associated with the gauge.
denom := lockuptypes.NativeDenom(gauge.DistributeTo.Denom)
lockSum := lockuptypes.SumLocksByDenom(locks, denom)

if lockSum.IsZero() {
return nil, nil
}

totalDistrCoins = totalDistrCoins.Add(distrCoins...)
for _, lock := range locks {
distrCoins := sdk.Coins{}
for _, coin := range remainCoins {
// distribution amount = gauge_size * denom_lock_amount / (total_denom_lock_amount * remain_epochs)
denomLockAmt := lock.Coins.AmountOfNoDenomValidation(denom)
amt := coin.Amount.Mul(denomLockAmt).Quo(lockSum.Mul(sdk.NewInt(int64(remainEpochs))))
if amt.IsPositive() {
newlyDistributedCoin := sdk.Coin{Denom: coin.Denom, Amount: amt}
distrCoins = distrCoins.Add(newlyDistributedCoin)
}
}
distrCoins = distrCoins.Sort()
if distrCoins.Empty() {
continue
}
// update the amount for that address
rewardReceiver := lock.RewardReceiverAddress

// if the reward receiver stored in state is an empty string, it indicates that the owner is the reward receiver.
if rewardReceiver == "" {
rewardReceiver = lock.Owner
}
err := distrInfo.addLockRewards(lock.Owner, rewardReceiver, distrCoins)
if err != nil {
return nil, err
}

totalDistrCoins = totalDistrCoins.Add(distrCoins...)
}
}

err := k.updateGaugePostDistribute(ctx, gauge, totalDistrCoins)
Expand Down Expand Up @@ -384,59 +409,20 @@ func (k Keeper) Distribute(ctx sdk.Context, gauges []types.Gauge) (sdk.Coins, er
locksByDenomCache := make(map[string][]lockuptypes.PeriodLock)
totalDistributedCoins := sdk.NewCoins()

// get pool Id corresponding to the gaugeId
incentiveParams := k.GetParams(ctx).DistrEpochIdentifier
currentEpoch := k.ek.GetEpochInfo(ctx, incentiveParams)

for _, gauge := range gauges {
var gaugeDistributedCoins sdk.Coins
pool, err := k.GetPoolFromGaugeId(ctx, gauge.Id, currentEpoch.Duration)
// Note: getting NoPoolAssociatedWithGaugeError implies that there is no pool associated with the gauge but we still want to distribute.
// This happens with superfluid gauges which are not connected to any specific pool directly but, instead,
// via an intermediary account.
if err != nil && !errors.Is(err, poolincentivestypes.NoPoolAssociatedWithGaugeError{GaugeId: gauge.Id, Duration: currentEpoch.Duration}) {
// TODO: add test case to cover this
return nil, err
} else if pool != nil && pool.GetType() == poolmanagertypes.Concentrated {
// only want to run this logic if the gaugeId is associated with CL PoolId
for _, coin := range gauge.Coins {
// emissionRate calculates amount of tokens to emit per second
// for ex: 10000tokens to be distributed over 1day epoch will be 1000 tokens ÷ 86,400 seconds ≈ 0.01157 tokens per second (truncated)
// Note: reason why we do millisecond conversion is because floats are non-deterministic so if someone refactors this and accidentally
// uses the return of currEpoch.Duration.Seconds() in math operations, this will lead to an app hash.
emissionRate := sdk.NewDecFromInt(coin.Amount).QuoTruncate(sdk.NewDec(currentEpoch.Duration.Milliseconds()).QuoInt(sdk.NewInt(1000)))
err := k.distributeConcentratedLiquidity(ctx,
pool.GetId(),
k.ak.GetModuleAddress(types.ModuleName),
coin,
emissionRate,
gauge.GetStartTime(),
// Note that the minimum uptime does not affect the distribution of incentives from the gauge and
// thus can be any value authorized by the CL module.
types.DefaultConcentratedUptime,
gauge,
)
if err != nil {
return nil, err
}
gaugeDistributedCoins = gaugeDistributedCoins.Add(coin)
}
filteredLocks := k.getDistributeToBaseLocks(ctx, gauge, locksByDenomCache)
// send based on synthetic lockup coins if it's distributing to synthetic lockups
var err error
if lockuptypes.IsSyntheticDenom(gauge.DistributeTo.Denom) {
gaugeDistributedCoins, err = k.distributeSyntheticInternal(ctx, gauge, filteredLocks, &distrInfo)
} else {
// Assume that there is no pool associated with the gauge and attempt to distribute to base locks
filteredLocks := k.getDistributeToBaseLocks(ctx, gauge, locksByDenomCache)
// send based on synthetic lockup coins if it's distributing to synthetic lockups
var err error
if lockuptypes.IsSyntheticDenom(gauge.DistributeTo.Denom) {
// TODO: add test case to cover this
gaugeDistributedCoins, err = k.distributeSyntheticInternal(ctx, gauge, filteredLocks, &distrInfo)
} else {
gaugeDistributedCoins, err = k.distributeInternal(ctx, gauge, filteredLocks, &distrInfo)
}
if err != nil {
// TODO: add test case to cover this
return nil, err
}
gaugeDistributedCoins, err = k.distributeInternal(ctx, gauge, filteredLocks, &distrInfo)
}
if err != nil {
return nil, err
}

totalDistributedCoins = totalDistributedCoins.Add(gaugeDistributedCoins...)
}

Expand Down
Loading

0 comments on commit 4c65a87

Please sign in to comment.