From 432208733a44781e85607ef5e3f5bdb7fad61d7f Mon Sep 17 00:00:00 2001 From: Omer <100387053+omerlavanet@users.noreply.github.com> Date: Sun, 15 Dec 2024 14:32:02 +0200 Subject: [PATCH] feat: CNS-daily restaking credit (#1794) * added credit and credit timestamp to delegations * fix nil deref and edge cases where delegation is in the future * added delegation credit to rewards distribution * fix delegate set to account for existing delegation * wip on tests * unitests wip * normalized provider credit too if he wasnt staked for a month, finished unitests for credit * lint * adding unitests * fix ctx * finished delegation set with credit unitests * wip fixing tests * fix a test * fix last test * add unitest - wip * push unitests wip * finished delegation partial test * adapted new sub tests to trigger only on the new flow * fix date add bug using months instead of days * add migrator * PR comments --------- Co-authored-by: Yaroms <103432884+Yaroms@users.noreply.github.com> --- proto/lavanet/lava/dualstaking/delegate.proto | 4 +- testutil/common/tester.go | 8 + testutil/keeper/dualstaking.go | 5 +- testutil/keeper/mock_keepers.go | 56 +++ x/dualstaking/README.md | 53 ++- x/dualstaking/keeper/delegate.go | 17 +- x/dualstaking/keeper/delegate_credit.go | 135 ++++++ x/dualstaking/keeper/delegate_credit_test.go | 386 ++++++++++++++++++ x/dualstaking/keeper/delegator_reward.go | 32 +- x/dualstaking/keeper/keeper.go | 3 +- x/dualstaking/keeper/migrations.go | 13 + x/dualstaking/module.go | 6 +- x/dualstaking/types/delegate.go | 7 +- x/dualstaking/types/delegate.pb.go | 138 +++++-- x/pairing/keeper/cu_tracker_test.go | 4 +- x/pairing/keeper/delegator_rewards_test.go | 122 ++++-- x/pairing/keeper/helpers_test.go | 29 +- 17 files changed, 920 insertions(+), 98 deletions(-) create mode 100644 x/dualstaking/keeper/delegate_credit.go create mode 100644 x/dualstaking/keeper/delegate_credit_test.go diff --git a/proto/lavanet/lava/dualstaking/delegate.proto b/proto/lavanet/lava/dualstaking/delegate.proto index 70761df8ea..28bf91e91a 100644 --- a/proto/lavanet/lava/dualstaking/delegate.proto +++ b/proto/lavanet/lava/dualstaking/delegate.proto @@ -11,7 +11,9 @@ message Delegation { string provider = 1; // provider receives the delegated funds string delegator = 3; // delegator that owns the delegated funds cosmos.base.v1beta1.Coin amount = 4 [(gogoproto.nullable) = false]; - int64 timestamp = 5; // Unix timestamp of the delegation (+ month) + int64 timestamp = 5; // Unix timestamp of the last change + cosmos.base.v1beta1.Coin credit = 6 [(gogoproto.nullable) = false]; // amount of credit earned by the delegation over the period + int64 credit_timestamp = 7; // Unix timestamp of the delegation credit latest calculation capped at 30d } message Delegator { diff --git a/testutil/common/tester.go b/testutil/common/tester.go index 9032bcf281..55bcf924ea 100644 --- a/testutil/common/tester.go +++ b/testutil/common/tester.go @@ -1150,6 +1150,14 @@ func (ts *Tester) AdvanceMonthsFrom(from time.Time, months int) *Tester { return ts } +func (ts *Tester) AdvanceTimeHours(timeDelta time.Duration) *Tester { + endTime := ts.BlockTime().Add(timeDelta) + for ts.BlockTime().Before(endTime) { + ts.AdvanceBlock(time.Hour) + } + return ts +} + func (ts *Tester) BondDenom() string { return ts.Keepers.StakingKeeper.BondDenom(sdk.UnwrapSDKContext(ts.Ctx)) } diff --git a/testutil/keeper/dualstaking.go b/testutil/keeper/dualstaking.go index e6149d6f4b..edb2d5328d 100644 --- a/testutil/keeper/dualstaking.go +++ b/testutil/keeper/dualstaking.go @@ -2,6 +2,7 @@ package keeper import ( "testing" + "time" tmdb "github.com/cometbft/cometbft-db" "github.com/cometbft/cometbft/libs/log" @@ -64,7 +65,7 @@ func DualstakingKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { memStoreKey, paramsSubspace, &mockBankKeeper{}, - nil, + &mockStakingKeeperEmpty{}, &mockAccountKeeper{}, epochstorageKeeper, speckeeper.NewKeeper(cdc, nil, nil, paramsSubspaceSpec, nil), @@ -72,7 +73,7 @@ func DualstakingKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { ) ctx := sdk.NewContext(stateStore, tmproto.Header{}, false, log.NewNopLogger()) - + ctx = ctx.WithBlockTime(time.Now().UTC()) // Initialize params k.SetParams(ctx, types.DefaultParams()) diff --git a/testutil/keeper/mock_keepers.go b/testutil/keeper/mock_keepers.go index 535c25c180..a8e2b7fb6a 100644 --- a/testutil/keeper/mock_keepers.go +++ b/testutil/keeper/mock_keepers.go @@ -4,9 +4,11 @@ import ( "fmt" "time" + "cosmossdk.io/math" tenderminttypes "github.com/cometbft/cometbft/types" sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" ) // account keeper mock @@ -36,6 +38,60 @@ func (k mockAccountKeeper) SetModuleAccount(sdk.Context, authtypes.ModuleAccount // mock bank keeper var balance map[string]sdk.Coins = make(map[string]sdk.Coins) +type mockStakingKeeperEmpty struct{} + +func (k mockStakingKeeperEmpty) ValidatorByConsAddr(sdk.Context, sdk.ConsAddress) stakingtypes.ValidatorI { + return nil +} + +func (k mockStakingKeeperEmpty) UnbondingTime(ctx sdk.Context) time.Duration { + return time.Duration(0) +} + +func (k mockStakingKeeperEmpty) GetAllDelegatorDelegations(ctx sdk.Context, delegator sdk.AccAddress) []stakingtypes.Delegation { + return nil +} + +func (k mockStakingKeeperEmpty) GetDelegatorValidator(ctx sdk.Context, delegatorAddr sdk.AccAddress, validatorAddr sdk.ValAddress) (validator stakingtypes.Validator, err error) { + return stakingtypes.Validator{}, nil +} + +func (k mockStakingKeeperEmpty) GetDelegation(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) (delegation stakingtypes.Delegation, found bool) { + return stakingtypes.Delegation{}, false +} + +func (k mockStakingKeeperEmpty) GetValidator(ctx sdk.Context, addr sdk.ValAddress) (validator stakingtypes.Validator, found bool) { + return stakingtypes.Validator{}, false +} + +func (k mockStakingKeeperEmpty) GetValidatorDelegations(ctx sdk.Context, valAddr sdk.ValAddress) (delegations []stakingtypes.Delegation) { + return []stakingtypes.Delegation{} +} + +func (k mockStakingKeeperEmpty) BondDenom(ctx sdk.Context) string { + return "ulava" +} + +func (k mockStakingKeeperEmpty) ValidateUnbondAmount(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress, amt math.Int) (shares sdk.Dec, err error) { + return sdk.Dec{}, nil +} + +func (k mockStakingKeeperEmpty) Undelegate(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress, sharesAmount sdk.Dec) (time.Time, error) { + return time.Time{}, nil +} + +func (k mockStakingKeeperEmpty) Delegate(ctx sdk.Context, delAddr sdk.AccAddress, bondAmt math.Int, tokenSrc stakingtypes.BondStatus, validator stakingtypes.Validator, subtractAccount bool) (newShares sdk.Dec, err error) { + return sdk.Dec{}, nil +} + +func (k mockStakingKeeperEmpty) GetBondedValidatorsByPower(ctx sdk.Context) []stakingtypes.Validator { + return []stakingtypes.Validator{} +} + +func (k mockStakingKeeperEmpty) GetAllValidators(ctx sdk.Context) (validators []stakingtypes.Validator) { + return []stakingtypes.Validator{} +} + type mockBankKeeper struct{} func init_balance() { diff --git a/x/dualstaking/README.md b/x/dualstaking/README.md index 682b40cc81..3cc782937c 100644 --- a/x/dualstaking/README.md +++ b/x/dualstaking/README.md @@ -6,23 +6,23 @@ This document specifies the dualstaking module of Lava Protocol. In the Lava blockchain there are two kinds of staking users, the first ones are validators, legacy to cosmos, the second ones are providers. Validators play a role in the consensus mechanism, while providers offer services to consumers and compete with other providers by staking tokens. -Since a lot of tokens are expected to be staked by providers, to enhance the security of the chain, Lava lets providers to participate in the consensus via dualstaking. +Since a lot of tokens are expected to be staked by providers, to enhance the security of the chain, Lava lets providers to participate in the consensus via dualstaking. Dualstaking makes this happen by "duplicating" delegations, for each validator delegation a parallel provider delegation will be created for the delegator, As a result, providers gain power in the consensus, influencing governance and block creation. - ## Contents + * [Concepts](#concepts) - * [Delegation](#delegation) - * [Empty Provider](#empty-provider) - * [Dualstaking](#dualstaking) - * [Validator Delegation](#validator-delegation) - * [Validator Unbonding](#validator-unbonding) - * [Validator Slashing](#validator-slashing) - * [Provider Delegation](#provider-delegation) - * [Provider Unbonding](#provider-unbonding) - * [Hooks](#hooks) - * [RedelegateFlag](#redelegateflag) - * [Rewards](#rewards) + * [Delegation](#delegation) + * [Empty Provider](#empty-provider) + * [Dualstaking](#dualstaking) + * [Validator Delegation](#validator-delegation) + * [Validator Unbonding](#validator-unbonding) + * [Validator Slashing](#validator-slashing) + * [Provider Delegation](#provider-delegation) + * [Provider Unbonding](#provider-unbonding) + * [Hooks](#hooks) + * [RedelegateFlag](#redelegateflag) + * [Credit](#credit) * [Parameters](#parameters) * [Queries](#queries) * [Transactions](#transactions) @@ -38,7 +38,7 @@ When a provider stakes tokens, they create a self-delegation entry. Whenever a p ### Empty Provider -The empty provider is a place holder for provider delegations that are issued by the staking module. +The empty provider is a place holder for provider delegations that are issued by the staking module. To support the functionality of the legacy Staking module, when a user delegates to a validator (it can't define the provider to delegate to in the legacy message), the dual staking module will delegate the same ammount to the empty provider. The user can than choose to redelegate from the empty provider to an actual provider. @@ -84,16 +84,35 @@ The following are use cases of the dualstaking module: ### Hooks Dual staking module uses [staking hooks](keeper/hooks.go) to achieve its functionality. + 1. AfterDelegationModified: this hook is called whenever a delegation is changed, whether it is created, or modified (NOT when completly removed). it calculates the difference in providers and validators stake to determine the action of the user (delegation or unbonding) depending on who is higher and than does the same with provider delegation. * If provider delegations > validator delegations: user unbonded, uniform unbond from providers delegations (priority to empty provider). * If provider delegations < validator delegations: user delegation, delegate to the empty provider. 2. BeforeDelegationRemoved: this hook is called when a delegation to a validator is removed (unbonding of all the tokens). uniform unbond from providers delegations -3. BeforeValidatorSlashed: this hook is called when a validator is being slashed. to make sure the balance between validator and provider delegation is kept it uniform unbond from providers delegations the slashed amount. +3. BeforeValidatorSlashed: this hook is called when a validator is being slashed. to make sure the balance between validator and provider delegation is kept it uniform unbond from providers delegations the slashed amount. ### RedelegateFlag To prevent the dual staking module from taking action in the case of validator redelegation, we utilize the [antehandler](ante/ante_handler.go). When a redelegation message is being processed, the RedelegateFlag is set to true, and the hooks will disregard any delegation changes. It is important to note that the RedelegateFlag is stored in memory and not in the chain’s state. +### Credit + +Credit Mechanism Overview +The credit mechanism ensures fair reward distribution to delegators based on both the amount and duration of their delegation. It calculates rewards proportionally to the effective stake over time. + +Key Components + +Credit: Effective delegation for a delegator, adjusted for staking duration. +CreditTimestamp: Last update time for the credit, enabling accurate reward calculations. +How It Works + +Credit is calculated when a delegation is made or modified, based on the current amount and elapsed time. +Rewards are normalized over a 30-day period for consistency. +Example +Alice delegates 100 tokens for a full month, earning a credit of 100 tokens. Bob delegates 200 tokens for half a month, also earning a credit of 100 tokens. With a total reward pool of 500 tokens, both receive 250 tokens, reflecting their credit-adjusted stakes. + +If Alice increases her delegation to 150 tokens mid-month, her credit is updated to reflect rewards earned so far, and future rewards are calculated on the new amount. This ensures fair distribution based on both delegation amount and duration. + ## Parameters The dualstaking parameters: @@ -128,7 +147,6 @@ The Dualstaking module supports the following transactions: | `unbond` | validator-addr (string) provider-addr (string) amount (coin) | undong from validator and provider the given amount | | `claim-rewards` | optional: provider-addr (string)| claim the rewards from a given provider or all rewards | - ## Proposals The Dualstaking module does not have proposals. @@ -136,6 +154,7 @@ The Dualstaking module does not have proposals. ### Events The Dualstaking module has the following events: + | Event | When it happens | | ---------- | --------------- | | `delegate_to_provider` | a successful provider delegation | @@ -143,4 +162,4 @@ The Dualstaking module has the following events: | `redelegate_between_providers` | a successful provider redelegation| | `delegator_claim_rewards` | a successful provider delegator reward claim| | `contributor_rewards` | spec contributor got new rewards| -| `validator_slash` | validator slashed happened, providers slashed accordingly| \ No newline at end of file +| `validator_slash` | validator slashed happened, providers slashed accordingly| diff --git a/x/dualstaking/keeper/delegate.go b/x/dualstaking/keeper/delegate.go index dfa1057876..3bf28f4343 100644 --- a/x/dualstaking/keeper/delegate.go +++ b/x/dualstaking/keeper/delegate.go @@ -34,15 +34,14 @@ import ( // and updates the (epochstorage) stake-entry. func (k Keeper) increaseDelegation(ctx sdk.Context, delegator, provider string, amount sdk.Coin, stake bool) error { // get, update the delegation entry - delegation, err := k.delegations.Get(ctx, types.DelegationKey(provider, delegator)) - if err != nil { + delegation, found := k.GetDelegation(ctx, provider, delegator) + if !found { // new delegation (i.e. not increase of existing one) delegation = types.NewDelegation(delegator, provider, ctx.BlockTime(), k.stakingKeeper.BondDenom(ctx)) } delegation.AddAmount(amount) - - err = k.delegations.Set(ctx, types.DelegationKey(provider, delegator), delegation) + err := k.SetDelegation(ctx, delegation) if err != nil { return err } @@ -364,7 +363,17 @@ func (k Keeper) GetAllDelegations(ctx sdk.Context) ([]types.Delegation, error) { return iter.Values() } +// this function overwrites the time tag with the ctx time upon writing the delegation func (k Keeper) SetDelegation(ctx sdk.Context, delegation types.Delegation) error { + delegation.Timestamp = ctx.BlockTime().UTC().Unix() + existingDelegation, found := k.GetDelegation(ctx, delegation.Provider, delegation.Delegator) + if !found { + return k.delegations.Set(ctx, types.DelegationKey(delegation.Provider, delegation.Delegator), delegation) + } + // calculate credit based on the existing delegation before changes + credit, creditTimestamp := k.CalculateCredit(ctx, existingDelegation) + delegation.Credit = credit + delegation.CreditTimestamp = creditTimestamp return k.delegations.Set(ctx, types.DelegationKey(delegation.Provider, delegation.Delegator), delegation) } diff --git a/x/dualstaking/keeper/delegate_credit.go b/x/dualstaking/keeper/delegate_credit.go new file mode 100644 index 0000000000..d08fdd8164 --- /dev/null +++ b/x/dualstaking/keeper/delegate_credit.go @@ -0,0 +1,135 @@ +package keeper + +// The credit mechanism is designed to fairly distribute rewards to delegators +// based on both the amount of tokens they delegate and the duration of their +// delegation. It ensures that rewards are proportional to the effective stake +// over time, rather than just the nominal amount of tokens delegated. +// +// Key Components: +// - Credit: Represents the effective delegation for a delegator, adjusted for +// the time their tokens have been staked. +// - CreditTimestamp: Records the last time the credit was updated, allowing +// for accurate calculation of rewards over time. +// +// How It Works: +// 1. When a delegation is made or modified, the credit is calculated based on +// the current amount and the time elapsed since the last update. +// 2. The credit is normalized over a 30-day period to ensure consistent reward +// distribution. +// +// Example 1: +// Consider two delegators, Alice and Bob, with a total delegators reward pool of 500 tokens. +// - Alice delegates 100 tokens for the full month, earning a credit of 100 tokens. +// - Bob delegates 200 tokens but only for half the month, earning a credit of 100 tokens. +// +// Total credit-adjusted delegations: Alice (100) + Bob (100) = 200 tokens. +// - Alice's reward: (500 * 100 / 200) = 250 tokens +// - Bob's reward: (500 * 100 / 200) = 250 tokens +// +// Example 2 (Mid-Month Delegation Change): +// Suppose Alice initially delegates 100 tokens, and halfway through the month, +// she increases her delegation to 150 tokens. +// - For the first half of the month, Alice's credit is calculated on 100 tokens. +// - When she increases her delegation, the credit is updated to reflect the rewards +// earned so far (e.g., 50 tokens for 15 days). +// - The CreditTimestamp is updated to the current time. +// - For the remaining half of the month, her credit is calculated on 150 tokens. + +import ( + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/lavanet/lava/v4/x/dualstaking/types" +) + +const ( + monthHours = 720 // 30 days * 24 hours + hourSeconds = 3600 +) + +// CalculateCredit calculates the credit value for a delegation, which represents the +// average stake over time used for reward distribution. +// The credit is normalized according to the difference between credit timestamp and the latest delegation change (in hours) +// The credit is updated only when the delegation amount changes, but is also used to calculate rewards for the current delegation (without updating the entry). +func (k Keeper) CalculateCredit(ctx sdk.Context, delegation types.Delegation) (credit sdk.Coin, creditTimestampRet int64) { + // Calculate the credit for the delegation + currentAmount := delegation.Amount + creditAmount := delegation.Credit + // handle uninitialized amounts + if creditAmount.IsNil() { + creditAmount = sdk.NewCoin(k.stakingKeeper.BondDenom(ctx), sdk.ZeroInt()) + } + if currentAmount.IsNil() { + // this should never happen, but we handle it just in case + currentAmount = sdk.NewCoin(k.stakingKeeper.BondDenom(ctx), sdk.ZeroInt()) + } + currentTimestamp := ctx.BlockTime().UTC() + delegationTimestamp := time.Unix(delegation.Timestamp, 0) + creditTimestamp := time.Unix(delegation.CreditTimestamp, 0) + // we normalize dates before we start the calculation + // maximum scope is 30 days, we start with the delegation truncation then the credit + monthAgo := currentTimestamp.AddDate(0, 0, -30) // we are doing 30 days not a month a month can be a different amount of days + if monthAgo.After(delegationTimestamp) { + // in the case the delegation wasn't changed for 30 days or more we truncate the timestamp to 30 days ago + // and disable the credit for older dates since they are irrelevant + delegationTimestamp = monthAgo + creditTimestamp = delegationTimestamp + creditAmount = sdk.NewCoin(k.stakingKeeper.BondDenom(ctx), sdk.ZeroInt()) + } else if monthAgo.After(creditTimestamp) { + // delegation is less than 30 days, but credit might be older, so truncate it to 30 days + creditTimestamp = monthAgo + } + + creditDelta := int64(0) // hours + if delegation.CreditTimestamp == 0 || creditAmount.IsZero() { + // in case credit was never set, we set it to the delegation timestamp + creditTimestamp = delegationTimestamp + } else if creditTimestamp.Before(delegationTimestamp) { + // calculate the credit delta in hours + creditDelta = (delegationTimestamp.Unix() - creditTimestamp.Unix()) / hourSeconds + } + + amountDelta := int64(0) // hours + if !currentAmount.IsZero() && delegationTimestamp.Before(currentTimestamp) { + amountDelta = (currentTimestamp.Unix() - delegationTimestamp.Unix()) / hourSeconds + } + + // creditDelta is the weight of the history and amountDelta is the weight of the current amount + // we need to average them and store it in the credit + totalDelta := creditDelta + amountDelta + if totalDelta == 0 { + return sdk.NewCoin(k.stakingKeeper.BondDenom(ctx), sdk.ZeroInt()), currentTimestamp.Unix() + } + credit = sdk.NewCoin(k.stakingKeeper.BondDenom(ctx), currentAmount.Amount.MulRaw(amountDelta).Add(creditAmount.Amount.MulRaw(creditDelta)).QuoRaw(totalDelta)) + return credit, creditTimestamp.Unix() +} + +// CalculateMonthlyCredit returns the total credit value for a delegation, normalized over a 30-day period (hours resolution). +// it does so by calculating the historical credit over the difference between the credit timestamp and the delegation timestamp (in hours) and normalizing it to 30 days +// it then adds the current delegation over the difference between the delegation timestamp and now (in hours) and normalizing it to 30 days +// the function does not modify the delegation amounts nor the timestamps, yet calculates the values for distribution with the current time +// +// For example: +// - If a delegator stakes 100 tokens for a full month, their credit will be 100 +// - If they stake 100 tokens for half a month, their credit will be 50 +// - If they stake 100 tokens for 15 days then increase to 200 tokens, their credit +// will be calculated as: (100 * 15 + 200 * 15) / 30 = 150 +func (k Keeper) CalculateMonthlyCredit(ctx sdk.Context, delegation types.Delegation) (credit sdk.Coin) { + credit, creditTimeEpoch := k.CalculateCredit(ctx, delegation) + if credit.IsNil() || credit.IsZero() || creditTimeEpoch <= 0 { + return sdk.NewCoin(k.stakingKeeper.BondDenom(ctx), sdk.ZeroInt()) + } + creditTimestamp := time.Unix(creditTimeEpoch, 0) + timeStampDiff := (ctx.BlockTime().UTC().Unix() - creditTimestamp.Unix()) / hourSeconds + if timeStampDiff <= 0 { + // no positive credit + return sdk.NewCoin(k.stakingKeeper.BondDenom(ctx), sdk.ZeroInt()) + } + // make sure we never increase the credit + if timeStampDiff > monthHours { + timeStampDiff = monthHours + } + // normalize credit to 30 days + credit.Amount = credit.Amount.MulRaw(timeStampDiff).QuoRaw(monthHours) + return credit +} diff --git a/x/dualstaking/keeper/delegate_credit_test.go b/x/dualstaking/keeper/delegate_credit_test.go new file mode 100644 index 0000000000..323f5cb2e9 --- /dev/null +++ b/x/dualstaking/keeper/delegate_credit_test.go @@ -0,0 +1,386 @@ +package keeper_test + +import ( + "strconv" + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/lavanet/lava/v4/testutil/common" + keepertest "github.com/lavanet/lava/v4/testutil/keeper" + "github.com/lavanet/lava/v4/utils" + commontypes "github.com/lavanet/lava/v4/utils/common/types" + "github.com/lavanet/lava/v4/x/dualstaking/keeper" + "github.com/lavanet/lava/v4/x/dualstaking/types" + "github.com/stretchr/testify/require" +) + +func SetDelegationMock(k keeper.Keeper, ctx sdk.Context, delegation types.Delegation) (delegationRet types.Delegation) { + credit, creditTimestamp := k.CalculateCredit(ctx, delegation) + delegation.Credit = credit + delegation.CreditTimestamp = creditTimestamp + return delegation +} + +func TestCalculateCredit(t *testing.T) { + k, ctx := keepertest.DualstakingKeeper(t) + bondDenom := commontypes.TokenDenom + timeNow := ctx.BlockTime() + tests := []struct { + name string + delegation types.Delegation + expectedCredit sdk.Coin + currentTime time.Time + expectedCreditTimestamp int64 + }{ + { + name: "initial delegation 10days ago with no credit", + delegation: types.Delegation{ + Amount: sdk.NewCoin(bondDenom, sdk.NewInt(1000)), + Credit: sdk.NewCoin(bondDenom, sdk.ZeroInt()), + Timestamp: timeNow.Add(-time.Hour * 24 * 10).Unix(), // was done 10 days ago + CreditTimestamp: 0, + }, + expectedCredit: sdk.NewCoin(bondDenom, sdk.NewInt(1000)), + currentTime: timeNow, + expectedCreditTimestamp: timeNow.Add(-time.Hour * 24 * 10).Unix(), + }, + { + name: "delegation with existing credit equal time increase", + delegation: types.Delegation{ + Amount: sdk.NewCoin(bondDenom, sdk.NewInt(2000)), + Credit: sdk.NewCoin(bondDenom, sdk.NewInt(1000)), + Timestamp: timeNow.Add(-time.Hour * 24 * 5).Unix(), + CreditTimestamp: timeNow.Add(-time.Hour * 24 * 10).Unix(), + }, + expectedCredit: sdk.NewCoin(bondDenom, sdk.NewInt(1500)), + currentTime: timeNow, + expectedCreditTimestamp: timeNow.Add(-time.Hour * 24 * 10).Unix(), + }, + { + name: "delegation with existing credit equal time decrease", + delegation: types.Delegation{ + Amount: sdk.NewCoin(bondDenom, sdk.NewInt(1000)), + Credit: sdk.NewCoin(bondDenom, sdk.NewInt(2000)), + Timestamp: timeNow.Add(-time.Hour * 24 * 5).Unix(), + CreditTimestamp: timeNow.Add(-time.Hour * 24 * 10).Unix(), + }, + expectedCredit: sdk.NewCoin(bondDenom, sdk.NewInt(1500)), + currentTime: timeNow, + expectedCreditTimestamp: timeNow.Add(-time.Hour * 24 * 10).Unix(), + }, + { + name: "delegation older than 30 days no credit", + delegation: types.Delegation{ + Amount: sdk.NewCoin(bondDenom, sdk.NewInt(3000)), + Credit: sdk.NewCoin(bondDenom, sdk.ZeroInt()), + Timestamp: timeNow.Add(-time.Hour * 24 * 40).Unix(), + CreditTimestamp: 0, + }, + expectedCredit: sdk.NewCoin(bondDenom, sdk.NewInt(3000)), + currentTime: timeNow, + expectedCreditTimestamp: timeNow.Add(-time.Hour * 24 * 30).Unix(), + }, + { + name: "delegation older than 30 days with credit", + delegation: types.Delegation{ + Amount: sdk.NewCoin(bondDenom, sdk.NewInt(3000)), + Credit: sdk.NewCoin(bondDenom, sdk.NewInt(7000)), + Timestamp: timeNow.Add(-time.Hour * 24 * 40).Unix(), + CreditTimestamp: timeNow.Add(-time.Hour * 24 * 50).Unix(), + }, + expectedCredit: sdk.NewCoin(bondDenom, sdk.NewInt(3000)), + currentTime: timeNow, + expectedCreditTimestamp: timeNow.Add(-time.Hour * 24 * 30).Unix(), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx = ctx.WithBlockTime(tt.currentTime) + credit, creditTimestamp := k.CalculateCredit(ctx, tt.delegation) + require.Equal(t, tt.expectedCredit, credit) + require.Equal(t, tt.expectedCreditTimestamp, creditTimestamp) + }) + } +} + +func TestCalculateMonthlyCredit(t *testing.T) { + k, ctx := keepertest.DualstakingKeeper(t) + bondDenom := commontypes.TokenDenom + timeNow := ctx.BlockTime() + tests := []struct { + name string + delegation types.Delegation + expectedCredit sdk.Coin + }{ + { + name: "monthly delegation no credit", + delegation: types.Delegation{ + Amount: sdk.NewCoin(bondDenom, sdk.NewInt(1000)), + Credit: sdk.NewCoin(bondDenom, sdk.ZeroInt()), + Timestamp: timeNow.Add(-time.Hour * 24 * 30).Unix(), + CreditTimestamp: 0, + }, + expectedCredit: sdk.NewCoin(bondDenom, sdk.NewInt(1000)), + }, + { + name: "old delegation no credit", + delegation: types.Delegation{ + Amount: sdk.NewCoin(bondDenom, sdk.NewInt(1000)), + Credit: sdk.NewCoin(bondDenom, sdk.ZeroInt()), + Timestamp: timeNow.Add(-time.Hour * 24 * 100).Unix(), + CreditTimestamp: 0, + }, + expectedCredit: sdk.NewCoin(bondDenom, sdk.NewInt(1000)), + }, + { + name: "half month delegation no credit", + delegation: types.Delegation{ + Amount: sdk.NewCoin(bondDenom, sdk.NewInt(1000)), + Credit: sdk.NewCoin(bondDenom, sdk.ZeroInt()), + Timestamp: timeNow.Add(-time.Hour * 24 * 15).Unix(), + CreditTimestamp: 0, + }, + expectedCredit: sdk.NewCoin(bondDenom, sdk.NewInt(500)), + }, + { + name: "old delegation with credit", + delegation: types.Delegation{ + Amount: sdk.NewCoin(bondDenom, sdk.NewInt(2000)), + Credit: sdk.NewCoin(bondDenom, sdk.NewInt(1000)), + Timestamp: timeNow.Add(-time.Hour * 24 * 35).Unix(), + CreditTimestamp: timeNow.Add(-time.Hour * 24 * 45).Unix(), + }, + expectedCredit: sdk.NewCoin(bondDenom, sdk.NewInt(2000)), + }, + { + name: "new delegation new credit increased delegation", + delegation: types.Delegation{ + Amount: sdk.NewCoin(bondDenom, sdk.NewInt(6000)), + Credit: sdk.NewCoin(bondDenom, sdk.NewInt(3000)), + Timestamp: timeNow.Add(-time.Hour * 24 * 5).Unix(), + CreditTimestamp: timeNow.Add(-time.Hour * 24 * 10).Unix(), + }, + expectedCredit: sdk.NewCoin(bondDenom, sdk.NewInt(1500)), + }, + { + name: "new delegation new credit decreased delegation", + delegation: types.Delegation{ + Amount: sdk.NewCoin(bondDenom, sdk.NewInt(3000)), + Credit: sdk.NewCoin(bondDenom, sdk.NewInt(6000)), + Timestamp: timeNow.Add(-time.Hour * 24 * 5).Unix(), + CreditTimestamp: timeNow.Add(-time.Hour * 24 * 10).Unix(), + }, + expectedCredit: sdk.NewCoin(bondDenom, sdk.NewInt(1500)), + }, + { + name: "new delegation old credit increased delegation", + delegation: types.Delegation{ + Amount: sdk.NewCoin(bondDenom, sdk.NewInt(6000)), + Credit: sdk.NewCoin(bondDenom, sdk.NewInt(3000)), + Timestamp: timeNow.Add(-time.Hour * 24 * 5).Unix(), + CreditTimestamp: timeNow.Add(-time.Hour * 24 * 40).Unix(), + }, + expectedCredit: sdk.NewCoin(bondDenom, sdk.NewInt(3500)), + }, + { + name: "new delegation old credit decreased delegation", + delegation: types.Delegation{ + Amount: sdk.NewCoin(bondDenom, sdk.NewInt(3000)), + Credit: sdk.NewCoin(bondDenom, sdk.NewInt(6000)), + Timestamp: timeNow.Add(-time.Hour * 24 * 5).Unix(), + CreditTimestamp: timeNow.Add(-time.Hour * 24 * 40).Unix(), + }, + expectedCredit: sdk.NewCoin(bondDenom, sdk.NewInt(5500)), + }, + { + name: "last second change", + delegation: types.Delegation{ + Amount: sdk.NewCoin(bondDenom, sdk.NewInt(10000)), + Credit: sdk.NewCoin(bondDenom, sdk.NewInt(1000)), + Timestamp: timeNow.Add(-time.Minute).Unix(), + CreditTimestamp: timeNow.Add(-time.Hour * 24 * 40).Unix(), + }, + expectedCredit: sdk.NewCoin(bondDenom, sdk.NewInt(1000)), + }, + { + name: "non whole hours old credit", + delegation: types.Delegation{ + Amount: sdk.NewCoin(bondDenom, sdk.NewInt(2*720000)), + Credit: sdk.NewCoin(bondDenom, sdk.NewInt(720000)), + Timestamp: timeNow.Add(-time.Hour - time.Minute).Unix(), // results in 1 hour + CreditTimestamp: timeNow.Add(-time.Hour * 24 * 40).Unix(), // results in 718 hours + }, + expectedCredit: sdk.NewCoin(bondDenom, sdk.NewInt(721001)), // ((718*720 + 2*720*1) / 719) *720/720 = 721001.39 + }, + { + name: "non whole hours new credit", + delegation: types.Delegation{ + Amount: sdk.NewCoin(bondDenom, sdk.NewInt(2*720)), + Credit: sdk.NewCoin(bondDenom, sdk.NewInt(720)), + Timestamp: timeNow.Add(-time.Hour*24*5 - time.Minute).Unix(), // 120 hours + CreditTimestamp: timeNow.Add(-time.Hour*24*15 - time.Minute).Unix(), // 240 hours + }, + expectedCredit: sdk.NewCoin(bondDenom, sdk.NewInt(480)), // (120 * 2 * 720 + 240 * 720) / 360 = 960, and monthly is: 960*360/720 = 480 + }, + { + name: "new delegation new credit last minute", + delegation: types.Delegation{ + Amount: sdk.NewCoin(bondDenom, sdk.NewInt(2*720)), + Credit: sdk.NewCoin(bondDenom, sdk.NewInt(720)), + Timestamp: timeNow.Add(-time.Minute).Unix(), + CreditTimestamp: timeNow.Add(-time.Minute * 2).Unix(), + }, + expectedCredit: sdk.NewCoin(bondDenom, sdk.NewInt(0)), + }, + { + name: "delegation credit are monthly exactly", + delegation: types.Delegation{ + Amount: sdk.NewCoin(bondDenom, sdk.NewInt(2*720)), + Credit: sdk.NewCoin(bondDenom, sdk.NewInt(720)), + Timestamp: timeNow.Add(-time.Hour * 24 * 30).Unix(), + CreditTimestamp: timeNow.Add(-time.Hour * 24 * 30).Unix(), + }, + expectedCredit: sdk.NewCoin(bondDenom, sdk.NewInt(720*2)), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + credit := k.CalculateMonthlyCredit(ctx, tt.delegation) + require.Equal(t, tt.expectedCredit, credit) + }) + } +} + +func TestDelegationSet(t *testing.T) { + ts := newTester(t) + // 1 delegator, 1 provider staked, 0 provider unstaked, 0 provider unstaking + ts.setupForDelegation(1, 1, 0, 0) + _, client1Addr := ts.GetAccount(common.CONSUMER, 0) + _, provider1Addr := ts.GetAccount(common.PROVIDER, 0) + k := ts.Keepers.Dualstaking + bondDenom := commontypes.TokenDenom + tests := []struct { + amount sdk.Coin + expectedMonthlyCredit sdk.Coin + timeWait time.Duration + remove bool + }{ + { // 0 + timeWait: 0, + amount: sdk.NewCoin(bondDenom, sdk.NewInt(1000*720)), + expectedMonthlyCredit: sdk.NewCoin(bondDenom, sdk.NewInt(0)), + }, + { // 1 + timeWait: 15 * time.Hour * 24, + amount: sdk.NewCoin(bondDenom, sdk.NewInt(1000*720)), + expectedMonthlyCredit: sdk.NewCoin(bondDenom, sdk.NewInt(1000*720/2)), + }, + { // 2 + timeWait: 15 * time.Hour * 24, + amount: sdk.NewCoin(bondDenom, sdk.NewInt(1000*720)), + expectedMonthlyCredit: sdk.NewCoin(bondDenom, sdk.NewInt(1000*720)), + }, + { // 3 + timeWait: 15 * time.Hour * 24, + amount: sdk.NewCoin(bondDenom, sdk.NewInt(1000*720)), + expectedMonthlyCredit: sdk.NewCoin(bondDenom, sdk.NewInt(1000*720)), + }, + { // 4 + timeWait: 15 * time.Hour * 24, + amount: sdk.NewCoin(bondDenom, sdk.NewInt(1000*720)), + expectedMonthlyCredit: sdk.NewCoin(bondDenom, sdk.NewInt(1000*720)), + }, + { // 5 + timeWait: 0, + amount: sdk.NewCoin(bondDenom, sdk.NewInt(500*720)), + expectedMonthlyCredit: sdk.NewCoin(bondDenom, sdk.NewInt(1000*720)), + }, + { // 6 + timeWait: 15 * time.Hour * 24, + amount: sdk.NewCoin(bondDenom, sdk.NewInt(500*720)), + expectedMonthlyCredit: sdk.NewCoin(bondDenom, sdk.NewInt(1000*720/2+500*720/2)), // 540000 + }, + { // 7 + timeWait: 15 * time.Hour * 24, + amount: sdk.NewCoin(bondDenom, sdk.NewInt(500*720)), + expectedMonthlyCredit: sdk.NewCoin(bondDenom, sdk.NewInt((1000*720/2+500*720/2)/2+500*720/2)), + }, + { // 8 + timeWait: 30 * time.Hour * 24, + amount: sdk.NewCoin(bondDenom, sdk.NewInt(500*720)), + expectedMonthlyCredit: sdk.NewCoin(bondDenom, sdk.NewInt(500*720)), + }, + { // 9 + timeWait: 1 * time.Hour, + amount: sdk.NewCoin(bondDenom, sdk.NewInt(500*720)), + expectedMonthlyCredit: sdk.NewCoin(bondDenom, sdk.NewInt(0)), + remove: true, // remove existing entry first + }, + { // 10 + timeWait: 1 * time.Hour, + amount: sdk.NewCoin(bondDenom, sdk.NewInt(500*720)), + expectedMonthlyCredit: sdk.NewCoin(bondDenom, sdk.NewInt(500)), + }, + { // 11 + timeWait: 1 * time.Hour, + amount: sdk.NewCoin(bondDenom, sdk.NewInt(500*720)), + expectedMonthlyCredit: sdk.NewCoin(bondDenom, sdk.NewInt(500+500)), + }, + { // 12 + timeWait: 30 * time.Hour * 24, + amount: sdk.NewCoin(bondDenom, sdk.NewInt(1000*720)), + expectedMonthlyCredit: sdk.NewCoin(bondDenom, sdk.NewInt(500*720)), + }, + { // 13 + timeWait: 1 * time.Hour, + amount: sdk.NewCoin(bondDenom, sdk.NewInt(500*720)), + expectedMonthlyCredit: sdk.NewCoin(bondDenom, sdk.NewInt(0)), + remove: true, // remove existing entry first + }, + { // 14 + timeWait: 1 * time.Hour, + amount: sdk.NewCoin(bondDenom, sdk.NewInt(1000*720)), + expectedMonthlyCredit: sdk.NewCoin(bondDenom, sdk.NewInt(500)), + }, + { // 15 + timeWait: 1 * time.Hour, + amount: sdk.NewCoin(bondDenom, sdk.NewInt(2000*720)), + expectedMonthlyCredit: sdk.NewCoin(bondDenom, sdk.NewInt(500+1000)), + }, + { // 16 + timeWait: 1 * time.Hour, + amount: sdk.NewCoin(bondDenom, sdk.NewInt(1000*720)), + expectedMonthlyCredit: sdk.NewCoin(bondDenom, sdk.NewInt(500+1000+2000)), + }, + { // 17 + timeWait: 2 * time.Hour, + amount: sdk.NewCoin(bondDenom, sdk.NewInt(1000*720)), + expectedMonthlyCredit: sdk.NewCoin(bondDenom, sdk.NewInt(500+1000+2000+1000*2)), + }, + } + + for iteration := 0; iteration < len(tests); iteration++ { + t.Run("delegation set tests "+strconv.Itoa(iteration), func(t *testing.T) { + delegation := types.Delegation{ + Delegator: client1Addr, + Provider: provider1Addr, + Amount: tests[iteration].amount, + } + if tests[iteration].remove { + k.RemoveDelegation(ts.Ctx, delegation) + } + utils.LavaFormatDebug("block times for credit", utils.LogAttr("block time", ts.Ctx.BlockTime()), utils.LogAttr("time wait", tests[iteration].timeWait)) + ts.Ctx = ts.Ctx.WithBlockTime(ts.Ctx.BlockTime().Add(tests[iteration].timeWait)) + + err := k.SetDelegation(ts.Ctx, delegation) + require.NoError(t, err) + delegationGot, found := k.GetDelegation(ts.Ctx, delegation.Provider, delegation.Delegator) + require.True(t, found) + monthlyCredit := k.CalculateMonthlyCredit(ts.Ctx, delegationGot) + require.Equal(t, tests[iteration].expectedMonthlyCredit, monthlyCredit) + }) + } +} diff --git a/x/dualstaking/keeper/delegator_reward.go b/x/dualstaking/keeper/delegator_reward.go index ae0259db70..c3d911cc07 100644 --- a/x/dualstaking/keeper/delegator_reward.go +++ b/x/dualstaking/keeper/delegator_reward.go @@ -64,7 +64,7 @@ func (k Keeper) GetAllDelegatorReward(ctx sdk.Context) (list []types.DelegatorRe // CalcRewards calculates the provider reward and the total reward for delegators // providerReward = totalReward * ((effectiveDelegations*commission + providerStake) / effectiveStake) // delegatorsReward = totalReward - providerReward -func (k Keeper) CalcRewards(ctx sdk.Context, totalReward sdk.Coins, totalDelegations math.Int, selfDelegation types.Delegation, delegations []types.Delegation, commission uint64) (providerReward sdk.Coins, delegatorsReward sdk.Coins) { +func (k Keeper) CalcRewards(ctx sdk.Context, totalReward sdk.Coins, totalDelegations math.Int, selfDelegation types.Delegation, commission uint64) (providerReward sdk.Coins, delegatorsReward sdk.Coins) { zeroCoins := sdk.NewCoins() totalDelegationsWithSelf := totalDelegations.Add(selfDelegation.Amount.Amount) @@ -173,18 +173,32 @@ func (k Keeper) RewardProvidersAndDelegators(ctx sdk.Context, provider string, c relevantDelegations := []types.Delegation{} totalDelegations := sdk.ZeroInt() - var selfdelegation types.Delegation + var selfDelegation types.Delegation // fetch relevant delegations (those who are passed the first week of delegation), self delegation and sum the total delegations - for _, d := range delegations { - if d.Delegator == metadata.Vault { - selfdelegation = d - } else if d.IsFirstWeekPassed(ctx.BlockTime().UTC().Unix()) { - relevantDelegations = append(relevantDelegations, d) - totalDelegations = totalDelegations.Add(d.Amount.Amount) + for _, delegation := range delegations { + if delegation.Delegator == metadata.Vault { + selfDelegation = delegation + // we are normalizing all delegations according to the time they were staked, + // if the provider is staked less than a month that would handicap them so we need to adjust the provider stake as well + credit := k.CalculateMonthlyCredit(ctx, selfDelegation) + if credit.IsZero() { + // should never happen + continue + } + selfDelegation.Amount = credit + } else { + credit := k.CalculateMonthlyCredit(ctx, delegation) + if credit.IsZero() { + continue + } + // modify the delegation for reward calculation based on the time it was staked + delegation.Amount = credit + relevantDelegations = append(relevantDelegations, delegation) + totalDelegations = totalDelegations.Add(delegation.Amount.Amount) } } - providerReward, delegatorsReward := k.CalcRewards(ctx, totalReward.Sub(contributorReward...), totalDelegations, selfdelegation, relevantDelegations, metadata.DelegateCommission) + providerReward, delegatorsReward := k.CalcRewards(ctx, totalReward.Sub(contributorReward...), totalDelegations, selfDelegation, metadata.DelegateCommission) leftoverRewards := k.updateDelegatorsReward(ctx, totalDelegations, relevantDelegations, delegatorsReward, senderModule, calcOnlyDelegators) fullProviderReward := providerReward.Add(leftoverRewards...) diff --git a/x/dualstaking/keeper/keeper.go b/x/dualstaking/keeper/keeper.go index 2010449bed..414ded8d8f 100644 --- a/x/dualstaking/keeper/keeper.go +++ b/x/dualstaking/keeper/keeper.go @@ -2,6 +2,7 @@ package keeper import ( "fmt" + "time" "cosmossdk.io/collections" abci "github.com/cometbft/cometbft/abci/types" @@ -92,7 +93,7 @@ func (k Keeper) ChangeDelegationTimestampForTesting(ctx sdk.Context, provider, d if !found { return fmt.Errorf("cannot change delegation timestamp: delegation not found. provider: %s, delegator: %s", provider, delegator) } - d.Timestamp = timestamp + ctx = ctx.WithBlockTime(time.Unix(timestamp, 0)) return k.SetDelegation(ctx, d) } diff --git a/x/dualstaking/keeper/migrations.go b/x/dualstaking/keeper/migrations.go index 8966235507..3d15423764 100644 --- a/x/dualstaking/keeper/migrations.go +++ b/x/dualstaking/keeper/migrations.go @@ -85,3 +85,16 @@ func (m Migrator) MigrateVersion5To6(ctx sdk.Context) error { return nil } + +func (m Migrator) MigrateVersion6To7(ctx sdk.Context) error { + // set all delegations to have a timestamp of 30 days ago + allDelegations, err := m.keeper.GetAllDelegations(ctx) + if err != nil { + for _, delegation := range allDelegations { + delegation.Timestamp = ctx.BlockTime().AddDate(0, 0, -30).UTC().Unix() + m.keeper.SetDelegation(ctx, delegation) + } + } + + return nil +} diff --git a/x/dualstaking/module.go b/x/dualstaking/module.go index aecd7dda6a..cf840df7f3 100644 --- a/x/dualstaking/module.go +++ b/x/dualstaking/module.go @@ -123,6 +123,10 @@ func (am AppModule) RegisterServices(cfg module.Configurator) { // panic:ok: at start up, migration cannot proceed anyhow panic(fmt.Errorf("%s: failed to register migration to v5: %w", types.ModuleName, err)) } + if err := cfg.RegisterMigration(types.ModuleName, 6, migrator.MigrateVersion6To7); err != nil { + // panic:ok: at start up, migration cannot proceed anyhow + panic(fmt.Errorf("%s: failed to register migration to v5: %w", types.ModuleName, err)) + } } // RegisterInvariants registers the invariants of the module. If an invariant deviates from its predicted value, the InvariantRegistry triggers appropriate logic (most often the chain will be halted) @@ -146,7 +150,7 @@ func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.Raw } // ConsensusVersion is a sequence number for state-breaking change of the module. It should be incremented on each consensus-breaking change introduced by the module. To avoid wrong/empty versions, the initial version should be set to 1 -func (AppModule) ConsensusVersion() uint64 { return 6 } +func (AppModule) ConsensusVersion() uint64 { return 7 } // BeginBlock contains the logic that is automatically triggered at the beginning of each block func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) { diff --git a/x/dualstaking/types/delegate.go b/x/dualstaking/types/delegate.go index fe2b82e405..b405117b3f 100644 --- a/x/dualstaking/types/delegate.go +++ b/x/dualstaking/types/delegate.go @@ -33,7 +33,7 @@ func NewDelegation(delegator, provider string, blockTime time.Time, tokenDenom s Delegator: delegator, Provider: provider, Amount: sdk.NewCoin(tokenDenom, sdk.ZeroInt()), - Timestamp: blockTime.AddDate(0, 0, 7).UTC().Unix(), + Timestamp: blockTime.UTC().Unix(), } } @@ -58,11 +58,6 @@ func (delegation *Delegation) Equal(other *Delegation) bool { return true } -func (delegation *Delegation) IsFirstWeekPassed(currentTimestamp int64) bool { - // this is a temporary code to reduce the time to 1 week instead of month, will be changed in the gradual delegation increase feature. - return delegation.Timestamp <= currentTimestamp -} - func NewDelegator(delegator, provider string) Delegator { return Delegator{ Providers: []string{provider}, diff --git a/x/dualstaking/types/delegate.pb.go b/x/dualstaking/types/delegate.pb.go index fbe4c80497..370c0fee9b 100644 --- a/x/dualstaking/types/delegate.pb.go +++ b/x/dualstaking/types/delegate.pb.go @@ -25,10 +25,12 @@ var _ = math.Inf const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package type Delegation struct { - Provider string `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` - Delegator string `protobuf:"bytes,3,opt,name=delegator,proto3" json:"delegator,omitempty"` - Amount types.Coin `protobuf:"bytes,4,opt,name=amount,proto3" json:"amount"` - Timestamp int64 `protobuf:"varint,5,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + Provider string `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` + Delegator string `protobuf:"bytes,3,opt,name=delegator,proto3" json:"delegator,omitempty"` + Amount types.Coin `protobuf:"bytes,4,opt,name=amount,proto3" json:"amount"` + Timestamp int64 `protobuf:"varint,5,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + Credit types.Coin `protobuf:"bytes,6,opt,name=credit,proto3" json:"credit"` + CreditTimestamp int64 `protobuf:"varint,7,opt,name=credit_timestamp,json=creditTimestamp,proto3" json:"credit_timestamp,omitempty"` } func (m *Delegation) Reset() { *m = Delegation{} } @@ -92,6 +94,20 @@ func (m *Delegation) GetTimestamp() int64 { return 0 } +func (m *Delegation) GetCredit() types.Coin { + if m != nil { + return m.Credit + } + return types.Coin{} +} + +func (m *Delegation) GetCreditTimestamp() int64 { + if m != nil { + return m.CreditTimestamp + } + return 0 +} + type Delegator struct { Providers []string `protobuf:"bytes,1,rep,name=providers,proto3" json:"providers,omitempty"` } @@ -146,26 +162,28 @@ func init() { } var fileDescriptor_547eac7f30bf94d4 = []byte{ - // 300 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x54, 0x90, 0xb1, 0x4e, 0xf3, 0x30, - 0x14, 0x85, 0xe3, 0x3f, 0xfd, 0xab, 0xc6, 0x2c, 0x28, 0x62, 0x30, 0x55, 0x65, 0xa2, 0x2e, 0x94, - 0xc5, 0x56, 0x01, 0x89, 0xbd, 0x74, 0x40, 0x8c, 0x1d, 0xd9, 0x9c, 0xd6, 0x0a, 0x16, 0x8d, 0x6f, - 0x14, 0x3b, 0x11, 0xbc, 0x05, 0xef, 0xc0, 0xcb, 0x74, 0xec, 0xc8, 0x84, 0x50, 0xf2, 0x22, 0xc8, - 0x49, 0x48, 0xe9, 0x74, 0xed, 0x73, 0x8f, 0xee, 0x77, 0x74, 0xf0, 0xe5, 0x56, 0x94, 0x42, 0x4b, - 0xcb, 0xdd, 0xe4, 0x9b, 0x42, 0x6c, 0x8d, 0x15, 0x2f, 0x4a, 0x27, 0x7c, 0x23, 0xb7, 0x32, 0x11, - 0x56, 0xb2, 0x2c, 0x07, 0x0b, 0x21, 0xe9, 0x8c, 0xcc, 0x4d, 0xf6, 0xc7, 0x38, 0x3e, 0x4b, 0x20, - 0x81, 0xc6, 0xc4, 0xdd, 0xab, 0xf5, 0x8f, 0xe9, 0x1a, 0x4c, 0x0a, 0x86, 0xc7, 0xc2, 0x48, 0x5e, - 0xce, 0x63, 0x69, 0xc5, 0x9c, 0xaf, 0x41, 0xe9, 0x76, 0x3f, 0xfd, 0x40, 0x18, 0x2f, 0x5b, 0x84, - 0x02, 0x1d, 0x8e, 0xf1, 0x28, 0xcb, 0xa1, 0x54, 0x1b, 0x99, 0x13, 0x14, 0xa1, 0x59, 0xb0, 0xea, - 0xff, 0xe1, 0x04, 0x07, 0x5d, 0x18, 0xc8, 0x89, 0xdf, 0x2c, 0x0f, 0x42, 0x78, 0x87, 0x87, 0x22, - 0x85, 0x42, 0x5b, 0x32, 0x88, 0xd0, 0xec, 0xe4, 0xfa, 0x9c, 0xb5, 0x64, 0xe6, 0xc8, 0xac, 0x23, - 0xb3, 0x7b, 0x50, 0x7a, 0x31, 0xd8, 0x7d, 0x5d, 0x78, 0xab, 0xce, 0xee, 0xce, 0x5a, 0x95, 0x4a, - 0x63, 0x45, 0x9a, 0x91, 0xff, 0x11, 0x9a, 0xf9, 0xab, 0x83, 0xf0, 0x38, 0x18, 0xfd, 0x3b, 0xf5, - 0xa7, 0x57, 0x38, 0x58, 0xf6, 0xa4, 0x09, 0x0e, 0x7e, 0x33, 0x19, 0x82, 0x22, 0xdf, 0xe5, 0xe8, - 0x85, 0xc5, 0xc3, 0xae, 0xa2, 0x68, 0x5f, 0x51, 0xf4, 0x5d, 0x51, 0xf4, 0x5e, 0x53, 0x6f, 0x5f, - 0x53, 0xef, 0xb3, 0xa6, 0xde, 0x13, 0x4b, 0x94, 0x7d, 0x2e, 0x62, 0xb6, 0x86, 0x94, 0x1f, 0xd5, - 0x5d, 0xde, 0xf2, 0xd7, 0xa3, 0xce, 0xed, 0x5b, 0x26, 0x4d, 0x3c, 0x6c, 0x1a, 0xba, 0xf9, 0x09, - 0x00, 0x00, 0xff, 0xff, 0x97, 0xd6, 0x5f, 0xd7, 0x9c, 0x01, 0x00, 0x00, + // 330 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x91, 0xb1, 0x4e, 0xc3, 0x30, + 0x10, 0x86, 0xe3, 0xa6, 0x94, 0xc6, 0x0c, 0x54, 0x11, 0x83, 0xa9, 0x2a, 0x13, 0x75, 0xa1, 0x5d, + 0x6c, 0x15, 0x90, 0xd8, 0x4b, 0x07, 0xc4, 0x58, 0x31, 0xb1, 0x20, 0x27, 0xb1, 0x82, 0x45, 0x13, + 0x47, 0xb1, 0x1b, 0xc1, 0x13, 0xb0, 0xf2, 0x58, 0x1d, 0x3b, 0x32, 0x21, 0xd4, 0xbe, 0x08, 0x72, + 0x9c, 0x36, 0x74, 0x63, 0xba, 0xf3, 0x7f, 0x9f, 0xfe, 0xdf, 0xa7, 0x83, 0x97, 0x0b, 0x56, 0xb2, + 0x8c, 0x6b, 0x6a, 0x2a, 0x8d, 0x97, 0x6c, 0xa1, 0x34, 0x7b, 0x15, 0x59, 0x42, 0x63, 0xbe, 0xe0, + 0x09, 0xd3, 0x9c, 0xe4, 0x85, 0xd4, 0xd2, 0x47, 0x35, 0x48, 0x4c, 0x25, 0x7f, 0xc0, 0xfe, 0x59, + 0x22, 0x13, 0x59, 0x41, 0xd4, 0x74, 0x96, 0xef, 0xe3, 0x48, 0xaa, 0x54, 0x2a, 0x1a, 0x32, 0xc5, + 0x69, 0x39, 0x09, 0xb9, 0x66, 0x13, 0x1a, 0x49, 0x91, 0xd9, 0xf9, 0xf0, 0xa3, 0x05, 0xe1, 0xcc, + 0x46, 0x08, 0x99, 0xf9, 0x7d, 0xd8, 0xcd, 0x0b, 0x59, 0x8a, 0x98, 0x17, 0x08, 0x04, 0x60, 0xe4, + 0xcd, 0xf7, 0x6f, 0x7f, 0x00, 0xbd, 0xfa, 0x33, 0xb2, 0x40, 0x6e, 0x35, 0x6c, 0x04, 0xff, 0x16, + 0x76, 0x58, 0x2a, 0x97, 0x99, 0x46, 0xed, 0x00, 0x8c, 0x4e, 0xae, 0xce, 0x89, 0x4d, 0x26, 0x26, + 0x99, 0xd4, 0xc9, 0xe4, 0x4e, 0x8a, 0x6c, 0xda, 0x5e, 0x7d, 0x5f, 0x38, 0xf3, 0x1a, 0x37, 0xb6, + 0x5a, 0xa4, 0x5c, 0x69, 0x96, 0xe6, 0xe8, 0x28, 0x00, 0x23, 0x77, 0xde, 0x08, 0xc6, 0x36, 0x2a, + 0x78, 0x2c, 0x34, 0xea, 0xfc, 0xd3, 0xd6, 0xe2, 0xfe, 0x18, 0xf6, 0x6c, 0xf7, 0xdc, 0xb8, 0x1f, + 0x57, 0xee, 0xa7, 0x56, 0x7f, 0xdc, 0xc9, 0x0f, 0xed, 0x6e, 0xab, 0xe7, 0x0e, 0xc7, 0xd0, 0x9b, + 0xed, 0xb7, 0x19, 0x40, 0x6f, 0xb7, 0xb7, 0x42, 0x20, 0x70, 0xcd, 0xae, 0x7b, 0x61, 0x7a, 0xbf, + 0xda, 0x60, 0xb0, 0xde, 0x60, 0xf0, 0xb3, 0xc1, 0xe0, 0x73, 0x8b, 0x9d, 0xf5, 0x16, 0x3b, 0x5f, + 0x5b, 0xec, 0x3c, 0x91, 0x44, 0xe8, 0x97, 0x65, 0x48, 0x22, 0x99, 0xd2, 0x83, 0x93, 0x96, 0x37, + 0xf4, 0xed, 0xe0, 0xae, 0xfa, 0x3d, 0xe7, 0x2a, 0xec, 0x54, 0x57, 0xb8, 0xfe, 0x0d, 0x00, 0x00, + 0xff, 0xff, 0x61, 0x94, 0xa1, 0x0b, 0x00, 0x02, 0x00, 0x00, } func (m *Delegation) Marshal() (dAtA []byte, err error) { @@ -188,6 +206,21 @@ func (m *Delegation) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if m.CreditTimestamp != 0 { + i = encodeVarintDelegate(dAtA, i, uint64(m.CreditTimestamp)) + i-- + dAtA[i] = 0x38 + } + { + size, err := m.Credit.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintDelegate(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x32 if m.Timestamp != 0 { i = encodeVarintDelegate(dAtA, i, uint64(m.Timestamp)) i-- @@ -282,6 +315,11 @@ func (m *Delegation) Size() (n int) { if m.Timestamp != 0 { n += 1 + sovDelegate(uint64(m.Timestamp)) } + l = m.Credit.Size() + n += 1 + l + sovDelegate(uint64(l)) + if m.CreditTimestamp != 0 { + n += 1 + sovDelegate(uint64(m.CreditTimestamp)) + } return n } @@ -451,6 +489,58 @@ func (m *Delegation) Unmarshal(dAtA []byte) error { break } } + case 6: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Credit", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowDelegate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthDelegate + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthDelegate + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.Credit.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 7: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field CreditTimestamp", wireType) + } + m.CreditTimestamp = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowDelegate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.CreditTimestamp |= int64(b&0x7F) << shift + if b < 0x80 { + break + } + } default: iNdEx = preIndex skippy, err := skipDelegate(dAtA[iNdEx:]) diff --git a/x/pairing/keeper/cu_tracker_test.go b/x/pairing/keeper/cu_tracker_test.go index 46ba860cb8..002202acb6 100644 --- a/x/pairing/keeper/cu_tracker_test.go +++ b/x/pairing/keeper/cu_tracker_test.go @@ -624,7 +624,9 @@ func TestProviderMonthlyPayoutQueryWithContributor(t *testing.T) { fakeTimestamp := ts.BlockTime().AddDate(0, -2, 0) err = ts.ChangeDelegationTimestamp(provider, delegator, ts.BlockHeight(), ts.GetNextMonth(fakeTimestamp)) require.NoError(t, err) - + // need to do the same for the provider + err = ts.ChangeDelegationTimestamp(provider, providerAcct.GetVaultAddr(), ts.BlockHeight(), ts.GetNextMonth(fakeTimestamp)) + require.NoError(t, err) // send two relay payments in spec and spec1 relaySession := ts.newRelaySession(provider, 0, relayCuSum, ts.BlockHeight(), 0) relaySession2 := ts.newRelaySession(provider, 0, relayCuSum, ts.BlockHeight(), 0) diff --git a/x/pairing/keeper/delegator_rewards_test.go b/x/pairing/keeper/delegator_rewards_test.go index e7038f9285..7d7979c7d8 100644 --- a/x/pairing/keeper/delegator_rewards_test.go +++ b/x/pairing/keeper/delegator_rewards_test.go @@ -2,6 +2,7 @@ package keeper_test import ( "testing" + "time" "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" @@ -13,6 +14,10 @@ import ( "github.com/stretchr/testify/require" ) +const ( + exactConst = "exact" +) + // TestProviderDelegatorsRewards tests that the provider's reward (considering delegations) is as expected // Also, it checks that the delegator reward map is updated as expected func TestProviderDelegatorsRewards(t *testing.T) { @@ -171,6 +176,56 @@ func sendRelay(ts *tester, provider string, clientAcc sigs.Account, chainIDs []s return types.MsgRelayPayment{Creator: provider, Relays: relays} } +func TestPartialMonthDelegation(t *testing.T) { + ts := newTester(t) + ts.setupForPayments(0, 1, 1) // 1 client, 1 providersToPair + ts.AddAccount(common.CONSUMER, 1, testBalance) // add delegator1 + ts.AddAccount(common.CONSUMER, 2, testBalance) // add delegator2 + + clientAcc, client := ts.GetAccount(common.CONSUMER, 0) + _, delegator1 := ts.GetAccount(common.CONSUMER, 1) + + _, err := ts.TxSubscriptionBuy(client, client, "free", 1, false, false) // extend by a month so the sub won't expire + require.NoError(t, err) + + ts.AdvanceTimeHours(time.Hour*24*15 + time.Hour*41) // results in 15 days of the provider being active (41 hours until subscription triggers payout) + + // add another provider after 15 days + err = ts.addProvider(1) + require.Nil(ts.T, err) + providerAcc, provider := ts.GetAccount(common.PROVIDER, 0) + require.NotNil(t, providerAcc) + metadata, err := ts.Keepers.Epochstorage.GetMetadata(ts.Ctx, provider) + require.NoError(t, err) + metadata.DelegateCommission = 50 // 50% commission + ts.Keepers.Epochstorage.SetMetadata(ts.Ctx, metadata) + + ts.AdvanceTimeHours(time.Hour * 24 * 5) // 5 days passed from the provider stake, total 20 days + + stakeEntryResp, err := ts.Keepers.Pairing.Provider(ts.Ctx, &types.QueryProviderRequest{ + Address: provider, + ChainID: ts.spec.Index, + }) + require.Nil(t, err) + stakeEntry := stakeEntryResp.StakeEntries[0] + + delegationAmount1 := sdk.NewCoin(ts.TokenDenom(), sdk.NewInt(testStake*3/2)) // provider did 100000 for 15 days delegator will have 150000 for 10 days so they are equal + _, err = ts.TxDualstakingDelegate(delegator1, provider, delegationAmount1) + require.NoError(t, err) + + res, err := ts.QueryDualstakingProviderDelegators(provider) + require.NoError(t, err) + require.Equal(t, 2, len(res.Delegations)) + require.Equal(t, stakeEntry.DelegateCommission, metadata.DelegateCommission) + + relayPaymentMessage := sendRelay(ts, provider, clientAcc, []string{ts.spec.Index}) + relayPaymentMessage.DescriptionString = exactConst + // we have a provider that staked after 15/30 days, and a delegator that staked after 20/30 days + // provider has 100k stake for 15 days, delegator has 150k stake for 10 days so they should divide half half, and the provider commission is 50% + // 0.5 * reward (due to stake amount) + 0.5 * 0.5 * reward (half the delegator reward due to 50% commission) = 75% + ts.payAndVerifyBalance(relayPaymentMessage, clientAcc.Addr, providerAcc.Vault.Addr, true, true, 75) +} + func TestProviderRewardWithCommission(t *testing.T) { ts := newTester(t) ts.setupForPayments(1, 1, 1) // 1 provider, 1 client, 1 providersToPair @@ -198,32 +253,33 @@ func TestProviderRewardWithCommission(t *testing.T) { ts.Keepers.Epochstorage.SetMetadata(ts.Ctx, metadata) ts.AdvanceEpoch() - stakeEntry, found := ts.Keepers.Epochstorage.GetStakeEntryCurrent(ts.Ctx, ts.spec.Index, provider) - require.True(t, found) - + stakeEntryResp, err := ts.Keepers.Pairing.Provider(ts.Ctx, &types.QueryProviderRequest{ + Address: provider, + ChainID: ts.spec.Index, + }) + require.Nil(t, err) + stakeEntry := stakeEntryResp.StakeEntries[0] res, err := ts.QueryDualstakingProviderDelegators(provider) require.NoError(t, err) require.Equal(t, 2, len(res.Delegations)) // the expected reward for the provider with 100% commission is the total rewards (delegators get nothing) - currentTimestamp := ts.Ctx.BlockTime().UTC().Unix() totalReward := sdk.NewCoins(sdk.NewCoin(ts.TokenDenom(), math.NewInt(int64(relayCuSum)))) - relevantDelegations := []dualstakingtypes.Delegation{} totalDelegations := sdk.ZeroInt() var selfdelegation dualstakingtypes.Delegation + monthTimeCtx := ts.Ctx.WithBlockTime(ts.Ctx.BlockTime().Add(time.Hour * 24 * 30)) // do the calculation for a month so delegation numbers are dividing nicely and don't give a truncation for _, d := range res.Delegations { - if d.Delegator != stakeEntry.Vault { + d.Amount = ts.Keepers.Dualstaking.CalculateMonthlyCredit(monthTimeCtx, d) + if d.Delegator == stakeEntry.Vault { selfdelegation = d - } else if d.IsFirstWeekPassed(currentTimestamp) { - relevantDelegations = append(relevantDelegations, d) + } else { totalDelegations = totalDelegations.Add(d.Amount.Amount) } } - providerReward, _ := ts.Keepers.Dualstaking.CalcRewards(ts.Ctx, totalReward, totalDelegations, selfdelegation, relevantDelegations, stakeEntry.DelegateCommission) - - require.True(t, totalReward.IsEqual(providerReward)) + providerReward, _ := ts.Keepers.Dualstaking.CalcRewards(monthTimeCtx, totalReward, totalDelegations, selfdelegation, stakeEntry.DelegateCommission) + require.True(t, totalReward.IsEqual(providerReward), "total %v vs provider %v", totalReward, providerReward) // check that the expected reward equals to the provider's new balance minus old balance relayPaymentMessage := sendRelay(ts, provider, clientAcc, []string{ts.spec.Index}) @@ -440,10 +496,8 @@ func TestDelegationTimestamp(t *testing.T) { _, provider := ts.GetAccount(common.PROVIDER, 0) _, delegator := ts.GetAccount(common.CONSUMER, 1) - // delegate and check the timestamp is equal to current time + month - currentTimeAfterMonth := ts.BlockTime().AddDate(0, 0, 7).UTC().Unix() _, err := ts.TxDualstakingDelegate(delegator, provider, sdk.NewCoin(ts.TokenDenom(), sdk.NewInt(testStake))) - + delegationTime := ts.BlockTime().UTC().Unix() require.NoError(t, err) ts.AdvanceEpoch() // apply delegations @@ -452,13 +506,24 @@ func TestDelegationTimestamp(t *testing.T) { require.Equal(t, 2, len(res.Delegations)) // expect two because of provider self delegation + delegator for _, d := range res.Delegations { if d.Delegator == delegator { - require.Equal(t, currentTimeAfterMonth, d.Timestamp) + require.Equal(t, delegationTime, d.Timestamp) } } - // advance time and delegate again to verify that the timestamp hasn't changed + // advance time ts.AdvanceMonths(1) + // verify that the timestamp hasn't changed + res, err = ts.QueryDualstakingProviderDelegators(provider) + require.NoError(t, err) + require.Equal(t, 2, len(res.Delegations)) // expect two because of provider self delegation + delegator + for _, d := range res.Delegations { + if d.Delegator == delegator { + require.Equal(t, delegationTime, d.Timestamp) + } + } + // verify that the timestamp changes when delegating more and credit is a month ago expectedDelegation := sdk.NewCoin(ts.TokenDenom(), sdk.NewInt(2*testStake)) + delegationTimeU := ts.BlockTime().UTC() _, err = ts.TxDualstakingDelegate(delegator, provider, sdk.NewCoin(ts.TokenDenom(), sdk.NewInt(testStake))) require.NoError(t, err) ts.AdvanceEpoch() // apply delegations @@ -468,8 +533,10 @@ func TestDelegationTimestamp(t *testing.T) { require.Equal(t, 2, len(res.Delegations)) // expect two because of provider self delegation + delegator for _, d := range res.Delegations { if d.Delegator == delegator { - require.Equal(t, currentTimeAfterMonth, d.Timestamp) + creditStart := delegationTimeU.Add(-30 * time.Hour * 24).UTC().Unix() + require.Equal(t, delegationTimeU.Unix(), d.Timestamp) require.True(t, d.Amount.IsEqual(expectedDelegation)) + require.Equal(t, creditStart, d.CreditTimestamp) } } } @@ -491,7 +558,7 @@ func TestDelegationFirstMonthPairing(t *testing.T) { ts.AdvanceEpoch() // delegate and check the delegation's timestamp is equal than nowPlusWeekTime - nowPlusWeekTime := ts.BlockTime().AddDate(0, 0, 7).UTC().Unix() + delegationTime := ts.BlockTime().UTC().Unix() _, err := ts.TxDualstakingDelegate(delegator, provider, sdk.NewCoin(ts.TokenDenom(), sdk.NewInt(testStake))) require.NoError(t, err) @@ -502,7 +569,7 @@ func TestDelegationFirstMonthPairing(t *testing.T) { require.Equal(t, 2, len(res.Delegations)) // expect two because of provider self delegation + delegator for _, d := range res.Delegations { if d.Delegator == delegator { - require.Equal(t, nowPlusWeekTime, d.Timestamp) + require.Equal(t, delegationTime, d.Timestamp) } } @@ -533,18 +600,17 @@ func TestDelegationFirstMonthReward(t *testing.T) { makeProviderCommissionZero(ts, provider) // delegate and check the delegation's timestamp is equal to nowPlusWeekTime - nowPlusWeekTime := ts.BlockTime().AddDate(0, 0, 7).UTC().Unix() + delegationTime := ts.BlockTime().UTC().Unix() _, err := ts.TxDualstakingDelegate(delegator, provider, sdk.NewCoin(ts.TokenDenom(), sdk.NewInt(testStake))) require.NoError(t, err) - ts.AdvanceEpoch() // apply delegations res, err := ts.QueryDualstakingProviderDelegators(provider) require.NoError(t, err) require.Equal(t, 2, len(res.Delegations)) // expect two because of provider self delegation + delegator for _, d := range res.Delegations { if d.Delegator == delegator { - require.Equal(t, nowPlusWeekTime, d.Timestamp) + require.Equal(t, delegationTime, d.Timestamp) } } @@ -556,12 +622,18 @@ func TestDelegationFirstMonthReward(t *testing.T) { providerReward, err := ts.Keepers.Dualstaking.RewardProvidersAndDelegators(ts.Ctx, provider, ts.spec.Index, fakeReward, subscriptiontypes.ModuleName, true, true, true) require.NoError(t, err) - require.True(t, fakeReward.IsEqual(providerReward)) // if the delegator got anything, this would fail + require.True(t, fakeReward.IsEqual(providerReward), "%v vs %v", providerReward, fakeReward) // if the delegator got something, this would fail // verify again that the delegator has no unclaimed rewards resRewards, err := ts.QueryDualstakingDelegatorRewards(delegator, provider, ts.spec.Index) require.NoError(t, err) require.Equal(t, 0, len(resRewards.Rewards)) + // now we advance some time and check that the delegator gets rewards + ts.AdvanceEpoch() // apply delegations + providerReward, err = ts.Keepers.Dualstaking.RewardProvidersAndDelegators(ts.Ctx, provider, ts.spec.Index, + fakeReward, subscriptiontypes.ModuleName, true, true, true) + require.NoError(t, err) + require.False(t, fakeReward.IsEqual(providerReward), "%v", providerReward) // if the delegator got anything, this would fail } // TestRedelegationFirstMonthReward checks that a delegator that redelegates @@ -588,7 +660,7 @@ func TestRedelegationFirstMonthReward(t *testing.T) { makeProviderCommissionZero(ts, provider) // delegate and check the delegation's timestamp is equal to nowPlusWeekTime - nowPlusWeekTime := ts.BlockTime().AddDate(0, 0, 7).UTC().Unix() + delegationTime := ts.BlockTime().UTC().Unix() _, err := ts.TxDualstakingDelegate(delegator, provider, sdk.NewCoin(ts.TokenDenom(), sdk.NewInt(testStake))) require.NoError(t, err) @@ -599,7 +671,7 @@ func TestRedelegationFirstMonthReward(t *testing.T) { require.Equal(t, 2, len(res.Delegations)) // expect two because of provider self delegation + delegator for _, d := range res.Delegations { if d.Delegator == delegator { - require.Equal(t, nowPlusWeekTime, d.Timestamp) + require.Equal(t, delegationTime, d.Timestamp) } } diff --git a/x/pairing/keeper/helpers_test.go b/x/pairing/keeper/helpers_test.go index ed1d64adcb..eea0eb175b 100644 --- a/x/pairing/keeper/helpers_test.go +++ b/x/pairing/keeper/helpers_test.go @@ -2,6 +2,7 @@ package keeper_test import ( "testing" + "time" "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" @@ -153,7 +154,7 @@ func (ts *tester) setupForPayments(providersCount, clientsCount, providersToPair // payAndVerifyBalance performs payment and then verifies the balances // (provider balance should increase and consumer should decrease) -// The providerRewardPerc arg is the part of the provider reward after dedcuting +// The providerRewardPerc arg is the part of the provider reward after deducting // the delegators portion (in percentage) func (ts *tester) payAndVerifyBalance( relayPayment pairingtypes.MsgRelayPayment, @@ -229,11 +230,25 @@ func (ts *tester) payAndVerifyBalance( require.Nil(ts.T, err) require.NotNil(ts.T, sub.Sub) require.Equal(ts.T, originalSubCuLeft-totalCuUsed, sub.Sub.MonthCuLeft) - - // advance month + blocksToSave + 1 to trigger the provider monthly payment - ts.AdvanceMonths(1) - ts.AdvanceEpoch() - ts.AdvanceBlocks(ts.BlocksToSave() + 1) + timeToExpiry := time.Unix(int64(sub.Sub.MonthExpiryTime), 0) + durLeft := sub.Sub.DurationLeft + if timeToExpiry.After(ts.Ctx.BlockTime()) && relayPayment.DescriptionString == exactConst { + ts.AdvanceTimeHours(timeToExpiry.Sub(ts.Ctx.BlockTime())) + // subs only pays after blocks to save + ts.AdvanceEpoch() + ts.AdvanceBlocks(ts.BlocksToSave() + 1) + if durLeft > 0 { + sub, err = ts.QuerySubscriptionCurrent(proj.Project.Subscription) + require.Nil(ts.T, err) + require.NotNil(ts.T, sub.Sub) + require.Equal(ts.T, durLeft-1, sub.Sub.DurationLeft, "month expiry time: %s current time: %s", time.Unix(int64(sub.Sub.MonthExpiryTime), 0).UTC(), ts.BlockTime().UTC()) + } + } else { + // advance month + blocksToSave + 1 to trigger the provider monthly payment + ts.AdvanceMonths(1) + ts.AdvanceEpoch() + ts.AdvanceBlocks(ts.BlocksToSave() + 1) + } // verify provider's balance credit := sub.Sub.Credit.Amount.QuoRaw(int64(sub.Sub.DurationLeft)) @@ -248,7 +263,7 @@ func (ts *tester) payAndVerifyBalance( for _, reward := range reward.Rewards { want = want.Sub(reward.Amount.AmountOf(ts.BondDenom())) } - require.True(ts.T, want.IsZero()) + require.True(ts.T, want.IsZero(), want) _, err = ts.TxDualstakingClaimRewards(providerVault.String(), relayPayment.Creator) require.Nil(ts.T, err)