diff --git a/proto/lavanet/lava/dualstaking/delegate.proto b/proto/lavanet/lava/dualstaking/delegate.proto index 70761df8ea..b980f614d5 100644 --- a/proto/lavanet/lava/dualstaking/delegate.proto +++ b/proto/lavanet/lava/dualstaking/delegate.proto @@ -12,6 +12,8 @@ message Delegation { 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) + 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 start } 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/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..c1dd1ce953 --- /dev/null +++ b/x/dualstaking/keeper/delegate_credit.go @@ -0,0 +1,107 @@ +package keeper + +// Delegation allows securing funds for a specific provider to effectively increase +// its stake so it will be paired with consumers more often. The delegators do not +// transfer the funds to the provider but only bestow the funds with it. In return +// to locking the funds there, delegators get some of the provider’s profit (after +// commission deduction). +// +// The delegated funds are stored in the module's BondedPoolName account. On request +// to terminate the delegation, they are then moved to the modules NotBondedPoolName +// account, and remain locked there for staking.UnbondingTime() witholding period +// before finally released back to the delegator. The timers for bonded funds are +// tracked are indexed by the delegator, provider, and chainID. +// +// The delegation state is stores with fixation using two maps: one for delegations +// indexed by the combination , used to track delegations +// and find/access delegations by provider (and chainID); and another for delegators +// tracking the list of providers for a delegator, indexed by the delegator. + +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 +) + +// calculate the delegation credit based on the timestamps, and the amounts of delegations +// amounts and credits represent daily value, rounded down +// can be used to calculate the credit for distribution or update the credit fields in the delegation +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() +} + +// this function takes the delegation and returns it's credit within the last 30 days +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/types/delegate.go b/x/dualstaking/types/delegate.go index fe2b82e405..016e772040 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(), } } 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..34bdfb3efb 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,55 @@ 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% + 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 +252,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 +495,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 +505,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 +532,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 +557,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 +568,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 +599,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 +621,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 +659,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 +670,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)