Skip to content

Commit

Permalink
Merge pull request irisnet#10 from terra-money/feat/rewards
Browse files Browse the repository at this point in the history
feat: support claiming rewards for LSD assets
  • Loading branch information
javiersuweijie authored Oct 18, 2022
2 parents 9e69a84 + 9ad6003 commit a0c4643
Show file tree
Hide file tree
Showing 11 changed files with 341 additions and 37 deletions.
7 changes: 4 additions & 3 deletions config.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
accounts:
- name: alice
coins: ["20000token", "200000000stake"]
coins: ["2000000000token", "200000000stake"]
mnemonic: "wine rich chunk that swim afford found auction travel dentist razor furnace fluid hidden happy enroll pilot dragon market broom bus merry salad assume"
- name: bob
coins: ["10000token", "100000000stake"]
Expand Down Expand Up @@ -32,6 +32,7 @@ genesis:
take_rate: "0.9"
- denom: 'token'
reward_weight: "5"
take_rate: "0"
take_rate: "0.9"
params:
reward_delay_time: "60s"
reward_delay_time: "60s"
reward_claim_interval: "10s"
23 changes: 23 additions & 0 deletions docs/static/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,15 @@ paths:
properties:
reward_delay_time:
type: string
reward_claim_interval:
type: string
title: >-
Time interval between consecutive applications of
`take_rate`
last_reward_claim_time:
type: string
format: date-time
title: Last application of `take_rate` on assets
global_reward_indices:
type: array
items:
Expand Down Expand Up @@ -44993,6 +45002,13 @@ definitions:
properties:
reward_delay_time:
type: string
reward_claim_interval:
type: string
title: Time interval between consecutive applications of `take_rate`
last_reward_claim_time:
type: string
format: date-time
title: Last application of `take_rate` on assets
global_reward_indices:
type: array
items:
Expand Down Expand Up @@ -45240,6 +45256,13 @@ definitions:
properties:
reward_delay_time:
type: string
reward_claim_interval:
type: string
title: Time interval between consecutive applications of `take_rate`
last_reward_claim_time:
type: string
format: date-time
title: Last application of `take_rate` on assets
global_reward_indices:
type: array
items:
Expand Down
13 changes: 12 additions & 1 deletion proto/alliance/params.proto
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package alliance.alliance;
import "gogoproto/gogo.proto";
import "cosmos_proto/cosmos.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/timestamp.proto";

option go_package = "alliance/x/alliance/types";

Expand All @@ -13,7 +14,17 @@ message Params {
(gogoproto.nullable) = false,
(gogoproto.stdduration) = true
];
repeated RewardIndex global_reward_indices = 2 [
// Time interval between consecutive applications of `take_rate`
google.protobuf.Duration reward_claim_interval = 2 [
(gogoproto.nullable) = false,
(gogoproto.stdduration) = true
];
// Last application of `take_rate` on assets
google.protobuf.Timestamp last_reward_claim_time = 3 [
(gogoproto.nullable) = false,
(gogoproto.stdtime) = true
];
repeated RewardIndex global_reward_indices = 4 [
(gogoproto.nullable) = false
];
}
Expand Down
1 change: 1 addition & 0 deletions x/alliance/abci.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ func EndBlocker(ctx sdk.Context, k keeper.Keeper) []abci.ValidatorUpdate {
defer telemetry.ModuleMeasureSince(types.ModuleName, time.Now(), telemetry.MetricKeyEndBlocker)
k.CompleteRedelegations(ctx)
k.CompleteUndelegations(ctx)
k.ClaimAssetsWithTakeRateRateLimited(ctx)
return []abci.ValidatorUpdate{}
}
3 changes: 3 additions & 0 deletions x/alliance/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package alliance

import (
"alliance/x/alliance/types"
"time"
)

// ValidateGenesis
Expand All @@ -13,6 +14,8 @@ func DefaultGenesisState() *types.GenesisState {
return &types.GenesisState{
Params: types.Params{
RewardDelayTime: 24 * 60 * 60 * 1000_000_000,
RewardClaimInterval: 5 * 60 * 1000_000_000,
LastRewardClaimTime: time.Now(),
GlobalRewardIndices: []types.RewardIndex{},
},
Assets: []types.AllianceAsset{},
Expand Down
8 changes: 8 additions & 0 deletions x/alliance/keeper/genesis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ func TestGenesis(t *testing.T) {
app.AllianceKeeper.InitGenesis(ctx, &types.GenesisState{
Params: types.Params{
RewardDelayTime: time.Duration(1000000),
RewardClaimInterval: time.Duration(1000000),
LastRewardClaimTime: time.Unix(0, 0).UTC(),
GlobalRewardIndices: types.RewardIndices{},
},
Assets: []types.AllianceAsset{
Expand All @@ -30,6 +32,12 @@ func TestGenesis(t *testing.T) {
delay := app.AllianceKeeper.RewardDelayTime(ctx)
require.Equal(t, time.Duration(1000000), delay)

interval := app.AllianceKeeper.RewardClaimInterval(ctx)
require.Equal(t, time.Duration(1000000), interval)

lastClaimTime := app.AllianceKeeper.LastRewardClaimTime(ctx)
require.Equal(t, time.Unix(0, 0).UTC(), lastClaimTime)

index := app.AllianceKeeper.GlobalRewardIndices(ctx)
require.Equal(t, types.RewardIndices(nil), index)

Expand Down
15 changes: 15 additions & 0 deletions x/alliance/keeper/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,18 @@ func (k Keeper) SetGlobalRewardIndex(ctx sdk.Context, index types.RewardIndices)
k.paramstore.Set(ctx, types.GlobalRewardIndices, &index)
return
}

func (k Keeper) RewardClaimInterval(ctx sdk.Context) (res time.Duration) {
k.paramstore.Get(ctx, types.RewardClaimInterval, &res)
return
}

func (k Keeper) LastRewardClaimTime(ctx sdk.Context) (res time.Time) {
k.paramstore.Get(ctx, types.LastRewardClaimTime, &res)
return
}

func (k Keeper) SetLastRewardClaimTime(ctx sdk.Context, lastTime time.Time) {
k.paramstore.Set(ctx, types.LastRewardClaimTime, &lastTime)
return
}
45 changes: 45 additions & 0 deletions x/alliance/keeper/reward.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
"golang.org/x/exp/slices"
"time"
)

type RewardsKeeper interface {
ClaimDistributionRewards(ctx sdk.Context, val stakingtypes.Validator) (sdk.Coins, error)
ClaimDelegationRewards(ctx sdk.Context, delAddr sdk.AccAddress, val stakingtypes.Validator, denom string) (sdk.Coins, error)
CalculateDelegationRewards(ctx sdk.Context, delegation types.Delegation, asset types.AllianceAsset) (sdk.Coins, types.RewardIndices, error)
AddAssetsToRewardPool(ctx sdk.Context, from sdk.AccAddress, coins sdk.Coins) error
Expand All @@ -17,6 +19,10 @@ var (
_ RewardsKeeper = Keeper{}
)

const (
YEAR_IN_NANOS int64 = 31_557_000_000_000_000
)

// ClaimDistributionRewards to be called right before any reward claims so that we get
// the latest rewards
func (k Keeper) ClaimDistributionRewards(ctx sdk.Context, val stakingtypes.Validator) (sdk.Coins, error) {
Expand Down Expand Up @@ -120,6 +126,45 @@ func (k Keeper) AddAssetsToRewardPool(ctx sdk.Context, from sdk.AccAddress, coin
return nil
}

func (k Keeper) ClaimAssetsWithTakeRateRateLimited(ctx sdk.Context) (sdk.Coins, error) {
last := k.LastRewardClaimTime(ctx)
interval := k.RewardClaimInterval(ctx)
next := last.Add(interval)
if ctx.BlockTime().After(next) {
return k.ClaimAssetsWithTakeRate(ctx, last)
}
return nil, nil
}

func (k Keeper) ClaimAssetsWithTakeRate(ctx sdk.Context, lastClaim time.Time) (sdk.Coins, error) {
assets := k.GetAllAssets(ctx)
durationSinceLastClaim := ctx.BlockTime().Sub(lastClaim)
prorate := sdk.NewDec(durationSinceLastClaim.Nanoseconds()).Quo(sdk.NewDec(YEAR_IN_NANOS))
var coins sdk.Coins
for _, asset := range assets {
if asset.TotalTokens.IsPositive() && asset.TakeRate.IsPositive() {
reward := asset.TakeRate.Mul(prorate).MulInt(asset.TotalTokens).TruncateInt()
asset.TotalTokens = asset.TotalTokens.Sub(reward)

// We also scale reward rate for newly staked tokens but not voting weight,
// since we assume the take rate is the rate at which the assets appreciates.
// More value = more voting rights
asset.RewardWeight = asset.RewardWeight.Mul(sdk.OneDec().Add(asset.TakeRate.Mul(prorate)))
coins = append(coins, sdk.NewCoin(asset.Denom, reward))
k.SetAsset(ctx, asset)
}
}
moduleAddr := k.accountKeeper.GetModuleAddress(types.ModuleName)
if !coins.Empty() && !coins.IsZero() {
err := k.AddAssetsToRewardPool(ctx, moduleAddr, coins)
if err != nil {
return nil, err
}
}
k.SetLastRewardClaimTime(ctx, ctx.BlockTime())
return coins, nil
}

func (k Keeper) totalAssetWeight(ctx sdk.Context) sdk.Dec {
assets := k.GetAllAssets(ctx)
total := sdk.ZeroDec()
Expand Down
76 changes: 72 additions & 4 deletions x/alliance/keeper/reward_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,7 @@ func TestRewardPoolAndGlobalIndex(t *testing.T) {
func TestClaimRewards(t *testing.T) {
app, ctx := createTestContext(t)
app.AllianceKeeper.InitGenesis(ctx, &types.GenesisState{
Params: types.Params{
RewardDelayTime: time.Duration(1000000),
GlobalRewardIndices: types.RewardIndices{},
},
Params: types.DefaultParams(),
Assets: []types.AllianceAsset{
{
Denom: ALLIANCE_TOKEN_DENOM,
Expand Down Expand Up @@ -222,3 +219,74 @@ func TestClaimRewards(t *testing.T) {
require.True(t, found)
require.Equal(t, indices, types.NewRewardIndices(delegation.RewardIndices))
}

func TestClaimTakeRate(t *testing.T) {
app, ctx := createTestContext(t)
startTime := time.Now()
ctx = ctx.WithBlockTime(startTime)
app.AllianceKeeper.InitGenesis(ctx, &types.GenesisState{
Params: types.Params{
RewardDelayTime: time.Minute * 60,
RewardClaimInterval: time.Minute * 5,
LastRewardClaimTime: startTime,
GlobalRewardIndices: types.NewRewardIndices(nil),
},
Assets: []types.AllianceAsset{
{
Denom: ALLIANCE_TOKEN_DENOM,
RewardWeight: sdk.NewDec(2),
TakeRate: sdk.MustNewDecFromStr("0.5"),
TotalShares: sdk.NewDec(0),
TotalTokens: sdk.ZeroInt(),
},
{
Denom: ALLIANCE_2_TOKEN_DENOM,
RewardWeight: sdk.NewDec(10),
TakeRate: sdk.NewDec(0),
TotalShares: sdk.NewDec(0),
TotalTokens: sdk.ZeroInt(),
},
},
})

// Accounts
//mintPoolAddr := app.AccountKeeper.GetModuleAddress(minttypes.ModuleName)
rewardsPoolAddr := app.AccountKeeper.GetModuleAddress(types.RewardsPoolName)
delegations := app.StakingKeeper.GetAllDelegations(ctx)
valAddr1, err := sdk.ValAddressFromBech32(delegations[0].ValidatorAddress)
require.NoError(t, err)
val1, found := app.StakingKeeper.GetValidator(ctx, valAddr1)
require.True(t, found)
addrs := test_helpers.AddTestAddrsIncremental(app, ctx, 1, sdk.NewCoins(
sdk.NewCoin(ALLIANCE_TOKEN_DENOM, sdk.NewInt(1000_000_000)),
sdk.NewCoin(ALLIANCE_2_TOKEN_DENOM, sdk.NewInt(1000_000_000)),
))
user1 := addrs[0]

app.AllianceKeeper.Delegate(ctx, user1, val1, sdk.NewCoin(ALLIANCE_TOKEN_DENOM, sdk.NewInt(1000_000_000)))
app.AllianceKeeper.Delegate(ctx, user1, val1, sdk.NewCoin(ALLIANCE_2_TOKEN_DENOM, sdk.NewInt(1000_000_000)))

// Calling it immediately will not update anything
coins, err := app.AllianceKeeper.ClaimAssetsWithTakeRateRateLimited(ctx)
require.Nil(t, coins)
require.Nil(t, err)

// Advance block time
timePassed := time.Minute*5 + time.Second
ctx = ctx.WithBlockTime(ctx.BlockTime().Add(timePassed))
coinsClaimed, err := app.AllianceKeeper.ClaimAssetsWithTakeRateRateLimited(ctx)
coins = app.BankKeeper.GetAllBalances(ctx, rewardsPoolAddr)
require.Equal(t, coinsClaimed, coins)

expectedAmount := sdk.MustNewDecFromStr("0.5").Mul(sdk.NewDec(timePassed.Nanoseconds()).Quo(sdk.NewDec(31_557_000_000_000_000))).MulInt(sdk.NewInt(1000_000_000))
require.Equal(t, expectedAmount.TruncateInt(), coins.AmountOf(ALLIANCE_TOKEN_DENOM))

lastUpdate := app.AllianceKeeper.LastRewardClaimTime(ctx)
require.Equal(t, ctx.BlockTime(), lastUpdate)

asset, found := app.AllianceKeeper.GetAssetByDenom(ctx, ALLIANCE_TOKEN_DENOM)
require.True(t, found)
expectedRewardRate := sdk.MustNewDecFromStr("2").Mul(sdk.OneDec().Add(sdk.MustNewDecFromStr("0.5").Mul(sdk.NewDec(timePassed.Nanoseconds()).Quo(sdk.NewDec(31_557_000_000_000_000)))))
require.Equal(t, expectedRewardRate, asset.RewardWeight)

}
16 changes: 15 additions & 1 deletion x/alliance/types/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (

var (
RewardDelayTime = []byte("RewardDelayTime")
RewardClaimInterval = []byte("RewardClaimInterval")
LastRewardClaimTime = []byte("LastRewardClaimTime")
GlobalRewardIndices = []byte("GlobalRewardIndices")
)

Expand All @@ -20,6 +22,8 @@ func (p *Params) ParamSetPairs() paramtypes.ParamSetPairs {
idxs := NewRewardIndices(p.GlobalRewardIndices)
return paramtypes.ParamSetPairs{
paramtypes.NewParamSetPair(RewardDelayTime, &p.RewardDelayTime, validatePositiveDuration),
paramtypes.NewParamSetPair(RewardClaimInterval, &p.RewardClaimInterval, validatePositiveDuration),
paramtypes.NewParamSetPair(LastRewardClaimTime, &p.LastRewardClaimTime, validateTime),
paramtypes.NewParamSetPair(GlobalRewardIndices, &idxs, validatePositiveRewardIndices),
}
}
Expand All @@ -30,7 +34,15 @@ func validatePositiveDuration(i interface{}) error {
return fmt.Errorf("invalid parameter type: %T", i)
}
if v <= 0 {
return fmt.Errorf("unbonding time must be positive: %d", v)
return fmt.Errorf("duration must be positive: %d", v)
}
return nil
}

func validateTime(i interface{}) error {
_, ok := i.(time.Time)
if !ok {
return fmt.Errorf("invalid parameter type: %T", i)
}
return nil
}
Expand All @@ -53,6 +65,8 @@ func NewParams() Params {
return Params{
RewardDelayTime: time.Hour,
GlobalRewardIndices: make([]RewardIndex, 0),
RewardClaimInterval: time.Minute * 5,
LastRewardClaimTime: time.Now(),
}
}

Expand Down
Loading

0 comments on commit a0c4643

Please sign in to comment.