diff --git a/x/incentive/keeper/hooks.go b/x/incentive/keeper/hooks.go index dec450b0..3b9425f3 100644 --- a/x/incentive/keeper/hooks.go +++ b/x/incentive/keeper/hooks.go @@ -168,10 +168,10 @@ func (h Hooks) BeforePoolDepositModified(ctx sdk.Context, poolID string, deposit // AfterSavingsDepositCreated function that runs after a deposit is created func (h Hooks) AfterSavingsDepositCreated(ctx sdk.Context, deposit savingstypes.Deposit) { - // TODO: InitializeSavingsReward + h.k.InitializeSavingsReward(ctx, deposit) } // BeforeSavingsDepositModified function that runs before a deposit is modified func (h Hooks) BeforeSavingsDepositModified(ctx sdk.Context, deposit savingstypes.Deposit, incomingDenoms []string) { - // TODO: SynchronizeSavingsReward + h.k.SynchronizeSavingsReward(ctx, deposit, incomingDenoms) } diff --git a/x/incentive/keeper/rewards_savings.go b/x/incentive/keeper/rewards_savings.go index 44e288ff..cbe4fab5 100644 --- a/x/incentive/keeper/rewards_savings.go +++ b/x/incentive/keeper/rewards_savings.go @@ -38,30 +38,54 @@ func (k Keeper) AccumulateSavingsRewards(ctx sdk.Context, rewardPeriod types.Mul } } -func (k Keeper) SynchronizeSavingsClaim(ctx sdk.Context, owner sdk.AccAddress) { - deposit, found := k.savingsKeeper.GetDeposit(ctx, owner) +// InitializeSavingsReward initializes a savings claim by creating the claim and +// setting the reward factor indexes +func (k Keeper) InitializeSavingsReward(ctx sdk.Context, deposit savingstypes.Deposit) { + claim, found := k.GetSavingsClaim(ctx, deposit.Depositor) if !found { - return + claim = types.NewSavingsClaim(deposit.Depositor, sdk.Coins{}, nil) } - k.SynchronizeSavingsReward(ctx, deposit) + + rewardIndexes := claim.RewardIndexes + for _, coin := range deposit.Amount { + globalRewardIndexes, found := k.GetSavingsRewardIndexes(ctx, coin.Denom) + if !found { + globalRewardIndexes = types.RewardIndexes{} + } + rewardIndexes = rewardIndexes.With(coin.Denom, globalRewardIndexes) + } + claim.RewardIndexes = rewardIndexes + + k.SetSavingsClaim(ctx, claim) } // SynchronizeSavingsReward updates the claim object by adding any accumulated rewards // and updating the reward index value -func (k Keeper) SynchronizeSavingsReward(ctx sdk.Context, deposit savingstypes.Deposit) { +func (k Keeper) SynchronizeSavingsReward(ctx sdk.Context, deposit savingstypes.Deposit, incomingDenoms []string) { claim, found := k.GetSavingsClaim(ctx, deposit.Depositor) if !found { return } - // Source shares for savings deposits is the deposit amount - for _, coin := range deposit.Amount { - claim = k.synchronizeSingleSavingsReward(ctx, claim, coin.Denom, coin.Amount.ToDec()) + // Set the reward factor on claim to the global reward factor for each incoming denom + for _, denom := range incomingDenoms { + globalRewardIndexes, found := k.GetSavingsRewardIndexes(ctx, denom) + if !found { + globalRewardIndexes = types.RewardIndexes{} + } + claim.RewardIndexes = claim.RewardIndexes.With(denom, globalRewardIndexes) } + + // Existing denoms have their reward indexes + reward amount synced + existingDenoms := setDifference(getDenoms(deposit.Amount), incomingDenoms) + for _, denom := range existingDenoms { + claim = k.synchronizeSingleSavingsReward(ctx, claim, denom, deposit.Amount.AmountOf(denom).ToDec()) + } + k.SetSavingsClaim(ctx, claim) } -// synchronizeSingleSavingsReward synchronizes a single rewarded savings denom in a s claim. +// synchronizeSingleSavingsReward synchronizes a single rewarded savings denom in a savings claim. // It returns the claim without setting in the store. // The public methods for accessing and modifying claims are preferred over this one. Direct modification of claims is easy to get wrong. func (k Keeper) synchronizeSingleSavingsReward(ctx sdk.Context, claim types.SavingsClaim, denom string, sourceShares sdk.Dec) types.SavingsClaim { @@ -115,3 +139,13 @@ func (k Keeper) GetSynchronizedSavingsClaim(ctx sdk.Context, owner sdk.AccAddres return claim, true } + +// SynchronizeSavingsClaim syncs a savings reward claim from its store +func (k Keeper) SynchronizeSavingsClaim(ctx sdk.Context, owner sdk.AccAddress) { + deposit, found := k.savingsKeeper.GetDeposit(ctx, owner) + if !found { + return + } + + k.SynchronizeSavingsReward(ctx, deposit, []string{}) +} diff --git a/x/incentive/keeper/rewards_savings_init_test.go b/x/incentive/keeper/rewards_savings_init_test.go new file mode 100644 index 00000000..f9b7bac2 --- /dev/null +++ b/x/incentive/keeper/rewards_savings_init_test.go @@ -0,0 +1,194 @@ +package keeper_test + +import ( + "testing" + + "github.com/stretchr/testify/suite" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/kava-labs/kava/x/incentive/types" + savingstypes "github.com/kava-labs/kava/x/savings/types" +) + +// InitializeSavingsRewardTests runs unit tests for the keeper.InitializeSavingsReward method +type InitializeSavingsRewardTests struct { + unitTester +} + +func TestInitializeSavingsRewardTests(t *testing.T) { + suite.Run(t, new(InitializeSavingsRewardTests)) +} + +func (suite *InitializeSavingsRewardTests) TestClaimAddedWhenClaimDoesNotExistAndNoRewards() { + // When a claim doesn't exist, and a user deposits to a non-rewarded pool; + // then a claim is added with no rewards and no indexes + + // no global indexes stored as this pool is not rewarded + + owner := arbitraryAddress() + + amount := sdk.NewCoin("test", sdk.OneInt()) + deposit := savingstypes.NewDeposit(owner, sdk.NewCoins(amount)) + + suite.keeper.InitializeSavingsReward(suite.ctx, deposit) + + syncedClaim, found := suite.keeper.GetSavingsClaim(suite.ctx, owner) + suite.True(found) + // A new claim should have empty indexes. It doesn't strictly need the poolID either. + expectedIndexes := types.MultiRewardIndexes{{ + CollateralType: amount.Denom, + RewardIndexes: nil, + }} + suite.Equal(expectedIndexes, syncedClaim.RewardIndexes) + // a new claim should start with 0 rewards + suite.Equal(sdk.Coins(nil), syncedClaim.Reward) +} + +func (suite *InitializeSavingsRewardTests) TestClaimAddedWhenClaimDoesNotExistAndRewardsExist() { + // When a claim doesn't exist, and a user deposits to a rewarded pool; + // then a claim is added with no rewards and indexes matching the global indexes + + amount := sdk.NewCoin("test", sdk.OneInt()) + + globalIndexes := types.MultiRewardIndexes{ + { + CollateralType: amount.Denom, + RewardIndexes: types.RewardIndexes{ + { + CollateralType: "rewarddenom", + RewardFactor: d("1000.001"), + }, + }, + }, + } + suite.storeGlobalSavingsIndexes(globalIndexes) + + owner := arbitraryAddress() + + deposit := savingstypes.NewDeposit(owner, sdk.NewCoins(amount)) + suite.keeper.InitializeSavingsReward(suite.ctx, deposit) + + syncedClaim, found := suite.keeper.GetSavingsClaim(suite.ctx, owner) + suite.True(found) + // a new claim should start with the current global indexes + suite.Equal(globalIndexes, syncedClaim.RewardIndexes) + // a new claim should start with 0 rewards + suite.Equal(sdk.Coins(nil), syncedClaim.Reward) +} + +func (suite *InitializeSavingsRewardTests) TestClaimUpdatedWhenClaimExistsAndNoRewards() { + // When a claim exists, and a user deposits to a new non-rewarded denom; + // then the claim's rewards don't change + + preexistingDenom := "preexisting" + preexistingIndexes := types.RewardIndexes{ + { + CollateralType: "rewarddenom", + RewardFactor: d("1000.001"), + }, + } + + claim := types.SavingsClaim{ + BaseMultiClaim: types.BaseMultiClaim{ + Owner: arbitraryAddress(), + Reward: arbitraryCoins(), + }, + RewardIndexes: types.MultiRewardIndexes{ + { + CollateralType: preexistingDenom, + RewardIndexes: preexistingIndexes, + }, + }, + } + suite.storeSavingsClaim(claim) + + // no global indexes stored as the new denom is not rewarded + newDenom := "test" + deposit := savingstypes.NewDeposit(claim.Owner, sdk.NewCoins(sdk.NewCoin(newDenom, sdk.OneInt()))) + suite.keeper.InitializeSavingsReward(suite.ctx, deposit) + + syncedClaim, found := suite.keeper.GetSavingsClaim(suite.ctx, claim.Owner) + suite.True(found) + + // The preexisting indexes shouldn't be changed. It doesn't strictly need the new denom either. + expectedIndexes := types.MultiRewardIndexes{ + { + CollateralType: preexistingDenom, + RewardIndexes: preexistingIndexes, + }, + { + CollateralType: newDenom, + RewardIndexes: nil, + }, + } + suite.Equal(expectedIndexes, syncedClaim.RewardIndexes) + // init should never alter the rewards + suite.Equal(claim.Reward, syncedClaim.Reward) +} + +func (suite *InitializeSavingsRewardTests) TestClaimUpdatedWhenClaimExistsAndRewardsExist() { + // When a claim exists, and a user deposits to a new rewarded denom; + // then the claim's rewards don't change and the indexes are updated to match the global indexes + + preexistingDenom := "preexisting" + preexistingIndexes := types.RewardIndexes{ + { + CollateralType: "rewarddenom", + RewardFactor: d("1000.001"), + }, + } + + newDenom := "test" + newIndexes := types.RewardIndexes{ + { + CollateralType: "otherrewarddenom", + RewardFactor: d("1000.001"), + }, + } + + claim := types.SavingsClaim{ + BaseMultiClaim: types.BaseMultiClaim{ + Owner: arbitraryAddress(), + Reward: arbitraryCoins(), + }, + RewardIndexes: types.MultiRewardIndexes{ + { + CollateralType: preexistingDenom, + RewardIndexes: preexistingIndexes, + }, + }, + } + suite.storeSavingsClaim(claim) + + globalIndexes := types.MultiRewardIndexes{ + { + CollateralType: preexistingDenom, + RewardIndexes: increaseRewardFactors(preexistingIndexes), + }, + { + CollateralType: newDenom, + RewardIndexes: newIndexes, + }, + } + suite.storeGlobalSavingsIndexes(globalIndexes) + + deposit := savingstypes.NewDeposit(claim.Owner, sdk.NewCoins(sdk.NewCoin(newDenom, sdk.OneInt()))) + suite.keeper.InitializeSavingsReward(suite.ctx, deposit) + + syncedClaim, _ := suite.keeper.GetSavingsClaim(suite.ctx, claim.Owner) + // only the indexes for the new denom should be updated + expectedIndexes := types.MultiRewardIndexes{ + { + CollateralType: preexistingDenom, + RewardIndexes: preexistingIndexes, + }, + { + CollateralType: newDenom, + RewardIndexes: newIndexes, + }, + } + suite.Equal(expectedIndexes, syncedClaim.RewardIndexes) + // init should never alter the rewards + suite.Equal(claim.Reward, syncedClaim.Reward) +} diff --git a/x/incentive/keeper/rewards_savings_sync_test.go b/x/incentive/keeper/rewards_savings_sync_test.go new file mode 100644 index 00000000..4e7c5439 --- /dev/null +++ b/x/incentive/keeper/rewards_savings_sync_test.go @@ -0,0 +1,245 @@ +package keeper_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/suite" + + "github.com/kava-labs/kava/x/incentive/types" + savingstypes "github.com/kava-labs/kava/x/savings/types" +) + +// SynchronizeSavingsRewardTests runs unit tests for the keeper.SynchronizeSavingsReward method +type SynchronizeSavingsRewardTests struct { + unitTester +} + +func TestSynchronizeSavingsReward(t *testing.T) { + suite.Run(t, new(SynchronizeSavingsRewardTests)) +} + +func (suite *SynchronizeSavingsRewardTests) TestClaimUpdatedWhenGlobalIndexesHaveIncreased() { + // This is the normal case + // Given some time has passed (meaning the global indexes have increased) + // When the claim is synced + // The user earns rewards for the time passed, and the claim indexes are updated + + originalReward := arbitraryCoins() + denom := "test" + + claim := types.SavingsClaim{ + BaseMultiClaim: types.BaseMultiClaim{ + Owner: arbitraryAddress(), + Reward: originalReward, + }, + RewardIndexes: types.MultiRewardIndexes{ + { + CollateralType: denom, + RewardIndexes: types.RewardIndexes{ + { + CollateralType: "rewarddenom", + RewardFactor: d("1000.001"), + }, + }, + }, + }, + } + suite.storeSavingsClaim(claim) + + globalIndexes := types.MultiRewardIndexes{ + { + CollateralType: denom, + RewardIndexes: types.RewardIndexes{ + { + CollateralType: "rewarddenom", + RewardFactor: d("2000.002"), + }, + }, + }, + } + suite.storeGlobalSavingsIndexes(globalIndexes) + + userShares := i(1e9) + deposit := savingstypes.NewDeposit(claim.Owner, sdk.NewCoins(sdk.NewCoin(denom, userShares))) + suite.keeper.SynchronizeSavingsReward(suite.ctx, deposit, []string{}) + + syncedClaim, _ := suite.keeper.GetSavingsClaim(suite.ctx, claim.Owner) + // indexes updated from global + suite.Equal(globalIndexes, syncedClaim.RewardIndexes) + // new reward is (new index - old index) * user shares + suite.Equal( + cs(c("rewarddenom", 1_000_001_000_000)).Add(originalReward...), + syncedClaim.Reward, + ) +} + +func (suite *SynchronizeSavingsRewardTests) TestClaimUnchangedWhenGlobalIndexesUnchanged() { + denom := "test" + unchangingIndexes := types.MultiRewardIndexes{ + { + CollateralType: denom, + RewardIndexes: types.RewardIndexes{ + { + CollateralType: "rewarddenom", + RewardFactor: d("1000.001"), + }, + }, + }, + } + + claim := types.SavingsClaim{ + BaseMultiClaim: types.BaseMultiClaim{ + Owner: arbitraryAddress(), + Reward: arbitraryCoins(), + }, + RewardIndexes: unchangingIndexes, + } + suite.storeSavingsClaim(claim) + + suite.storeGlobalSavingsIndexes(unchangingIndexes) + + userShares := i(1e9) + deposit := savingstypes.NewDeposit(claim.Owner, sdk.NewCoins(sdk.NewCoin(denom, userShares))) + suite.keeper.SynchronizeSavingsReward(suite.ctx, deposit, []string{}) + + syncedClaim, _ := suite.keeper.GetSavingsClaim(suite.ctx, claim.Owner) + // claim should have the same rewards and indexes as before + suite.Equal(claim, syncedClaim) +} + +func (suite *SynchronizeSavingsRewardTests) TestClaimUpdatedWhenNewRewardAdded() { + originalReward := arbitraryCoins() + newlyRewardedDenom := "newlyRewardedDenom" + + claim := types.SavingsClaim{ + BaseMultiClaim: types.BaseMultiClaim{ + Owner: arbitraryAddress(), + Reward: originalReward, + }, + RewardIndexes: types.MultiRewardIndexes{ + { + CollateralType: "currentlyRewardedDenom", + RewardIndexes: types.RewardIndexes{ + { + CollateralType: "reward", + RewardFactor: d("1000.001"), + }, + }, + }, + }, + } + suite.storeSavingsClaim(claim) + + globalIndexes := types.MultiRewardIndexes{ + { + CollateralType: "currentlyRewardedDenom", + RewardIndexes: types.RewardIndexes{ + { + CollateralType: "reward", + RewardFactor: d("2000.002"), + }, + }, + }, + { + CollateralType: newlyRewardedDenom, + RewardIndexes: types.RewardIndexes{ + { + CollateralType: "otherreward", + // Indexes start at 0 when the reward is added by gov, + // so this represents the syncing happening some time later. + RewardFactor: d("1000.001"), + }, + }, + }, + } + suite.storeGlobalSavingsIndexes(globalIndexes) + + userShares := i(1e9) + deposit := savingstypes.NewDeposit(claim.Owner, + sdk.NewCoins( + sdk.NewCoin("currentlyRewardedDenom", userShares), + sdk.NewCoin(newlyRewardedDenom, userShares), + ), + ) + + suite.keeper.SynchronizeSavingsReward(suite.ctx, deposit, []string{newlyRewardedDenom}) + + syncedClaim, _ := suite.keeper.GetSavingsClaim(suite.ctx, claim.Owner) + // the new indexes should be added to the claim and the old ones should be updated + suite.Equal(globalIndexes, syncedClaim.RewardIndexes) + // new reward is (new index - old index) * shares for the synced deposit + // The old index for `newlyrewarded` isn't in the claim, so it's added starting at 0 for calculating the reward. + suite.Equal( + cs(c("reward", 1_000_001_000_000)).Add(originalReward...), + syncedClaim.Reward, + ) +} + +func (suite *SynchronizeSavingsRewardTests) TestClaimUpdatedWhenNewRewardDenomAdded() { + // When a new reward coin is added (via gov) to an already rewarded denom (that the user has already deposited to), and the claim is synced; + // Then the user earns rewards for the time since the reward was added, and the new indexes are added. + + originalReward := arbitraryCoins() + denom := "base" + + claim := types.SavingsClaim{ + BaseMultiClaim: types.BaseMultiClaim{ + Owner: arbitraryAddress(), + Reward: originalReward, + }, + RewardIndexes: types.MultiRewardIndexes{ + { + CollateralType: denom, + RewardIndexes: types.RewardIndexes{ + { + CollateralType: "reward", + RewardFactor: d("1000.001"), + }, + }, + }, + }, + } + suite.storeSavingsClaim(claim) + + globalIndexes := types.MultiRewardIndexes{ + { + CollateralType: denom, + RewardIndexes: types.RewardIndexes{ + { + CollateralType: "reward", + RewardFactor: d("2000.002"), + }, + { + CollateralType: "otherreward", + // Indexes start at 0 when the reward is added by gov, + // so this represents the syncing happening some time later. + RewardFactor: d("1000.001"), + }, + }, + }, + } + suite.storeGlobalSavingsIndexes(globalIndexes) + + userShares := i(1e9) + deposit := savingstypes.NewDeposit(claim.Owner, sdk.NewCoins(sdk.NewCoin(denom, userShares))) + suite.keeper.SynchronizeSavingsReward(suite.ctx, deposit, []string{}) + + syncedClaim, _ := suite.keeper.GetSavingsClaim(suite.ctx, claim.Owner) + // indexes should have the new reward denom added + suite.Equal(globalIndexes, syncedClaim.RewardIndexes) + // new reward is (new index - old index) * shares + // The old index for `otherreward` isn't in the claim, so it's added starting at 0 for calculating the reward. + suite.Equal( + cs(c("reward", 1_000_001_000_000), c("otherreward", 1_000_001_000_000)).Add(originalReward...), + syncedClaim.Reward, + ) +} + +func getDenoms(coins sdk.Coins) []string { + denoms := []string{} + for _, coin := range coins { + denoms = append(denoms, coin.Denom) + } + return denoms +} diff --git a/x/incentive/keeper/rewards_supply_init_test.go b/x/incentive/keeper/rewards_supply_init_test.go index 3e1b218c..e8476819 100644 --- a/x/incentive/keeper/rewards_supply_init_test.go +++ b/x/incentive/keeper/rewards_supply_init_test.go @@ -32,7 +32,7 @@ func (suite *InitializeHardSupplyRewardTests) TestClaimIndexesAreSetWhenClaimExi globalIndexes := nonEmptyMultiRewardIndexes suite.storeGlobalSupplyIndexes(globalIndexes) - deposit := NewDepositBuilder(claim.Owner). + deposit := NewHardDepositBuilder(claim.Owner). WithArbitrarySourceShares(extractCollateralTypes(globalIndexes)...). Build() @@ -46,7 +46,7 @@ func (suite *InitializeHardSupplyRewardTests) TestClaimIndexesAreSetWhenClaimDoe suite.storeGlobalSupplyIndexes(globalIndexes) owner := arbitraryAddress() - deposit := NewDepositBuilder(owner). + deposit := NewHardDepositBuilder(owner). WithArbitrarySourceShares(extractCollateralTypes(globalIndexes)...). Build() @@ -66,7 +66,7 @@ func (suite *InitializeHardSupplyRewardTests) TestClaimIndexesAreSetEmptyForMiss // This happens when a deposit denom has no rewards associated with it. expectedIndexes := appendUniqueEmptyMultiRewardIndex(globalIndexes) depositedDenoms := extractCollateralTypes(expectedIndexes) - deposit := NewDepositBuilder(owner). + deposit := NewHardDepositBuilder(owner). WithArbitrarySourceShares(depositedDenoms...). Build() diff --git a/x/incentive/keeper/rewards_supply_sync_test.go b/x/incentive/keeper/rewards_supply_sync_test.go index c4444665..e443241e 100644 --- a/x/incentive/keeper/rewards_supply_sync_test.go +++ b/x/incentive/keeper/rewards_supply_sync_test.go @@ -32,7 +32,7 @@ func (suite *SynchronizeHardSupplyRewardTests) TestClaimIndexesAreUpdatedWhenGlo globalIndexes := increaseAllRewardFactors(nonEmptyMultiRewardIndexes) suite.storeGlobalSupplyIndexes(globalIndexes) - deposit := NewDepositBuilder(claim.Owner). + deposit := NewHardDepositBuilder(claim.Owner). WithArbitrarySourceShares(extractCollateralTypes(claim.SupplyRewardIndexes)...). Build() @@ -56,7 +56,7 @@ func (suite *SynchronizeHardSupplyRewardTests) TestClaimIndexesAreUnchangedWhenG suite.storeGlobalSupplyIndexes(unchangingIndexes) - deposit := NewDepositBuilder(claim.Owner). + deposit := NewHardDepositBuilder(claim.Owner). WithArbitrarySourceShares(extractCollateralTypes(unchangingIndexes)...). Build() @@ -80,7 +80,7 @@ func (suite *SynchronizeHardSupplyRewardTests) TestClaimIndexesAreUpdatedWhenNew globalIndexes := appendUniqueMultiRewardIndex(nonEmptyMultiRewardIndexes) suite.storeGlobalSupplyIndexes(globalIndexes) - deposit := NewDepositBuilder(claim.Owner). + deposit := NewHardDepositBuilder(claim.Owner). WithArbitrarySourceShares(extractCollateralTypes(globalIndexes)...). Build() @@ -105,7 +105,7 @@ func (suite *SynchronizeHardSupplyRewardTests) TestClaimIndexesAreUpdatedWhenNew globalIndexes := appendUniqueRewardIndexToFirstItem(nonEmptyMultiRewardIndexes) suite.storeGlobalSupplyIndexes(globalIndexes) - deposit := NewDepositBuilder(claim.Owner). + deposit := NewHardDepositBuilder(claim.Owner). WithArbitrarySourceShares(extractCollateralTypes(globalIndexes)...). Build() @@ -154,7 +154,7 @@ func (suite *SynchronizeHardSupplyRewardTests) TestRewardIsIncrementedWhenGlobal }, }) - deposit := NewDepositBuilder(claim.Owner). + deposit := NewHardDepositBuilder(claim.Owner). WithSourceShares("depositdenom", 1e9). Build() @@ -216,7 +216,7 @@ func (suite *SynchronizeHardSupplyRewardTests) TestRewardIsIncrementedWhenNewRew } suite.storeGlobalSupplyIndexes(globalIndexes) - deposit := NewDepositBuilder(claim.Owner). + deposit := NewHardDepositBuilder(claim.Owner). WithSourceShares("rewarded", 1e9). WithSourceShares("newlyrewarded", 1e9). Build() @@ -274,7 +274,7 @@ func (suite *SynchronizeHardSupplyRewardTests) TestRewardIsIncrementedWhenNewRew } suite.storeGlobalSupplyIndexes(globalIndexes) - deposit := NewDepositBuilder(claim.Owner). + deposit := NewHardDepositBuilder(claim.Owner). WithSourceShares("deposited", 1e9). Build() @@ -289,26 +289,26 @@ func (suite *SynchronizeHardSupplyRewardTests) TestRewardIsIncrementedWhenNewRew ) } -// DepositBuilder is a tool for creating a hard deposit in tests. +// HardDepositBuilder is a tool for creating a hard deposit in tests. // The builder inherits from hard.Deposit, so fields can be accessed directly if a helper method doesn't exist. -type DepositBuilder struct { +type HardDepositBuilder struct { hardtypes.Deposit } -// NewDepositBuilder creates a DepositBuilder containing an empty deposit. -func NewDepositBuilder(depositor sdk.AccAddress) DepositBuilder { - return DepositBuilder{ +// NewHardDepositBuilder creates a HardDepositBuilder containing an empty deposit. +func NewHardDepositBuilder(depositor sdk.AccAddress) HardDepositBuilder { + return HardDepositBuilder{ Deposit: hardtypes.Deposit{ Depositor: depositor, }} } // Build assembles and returns the final deposit. -func (builder DepositBuilder) Build() hardtypes.Deposit { return builder.Deposit } +func (builder HardDepositBuilder) Build() hardtypes.Deposit { return builder.Deposit } // WithSourceShares adds a deposit amount and factor such that the source shares for this deposit is equal to specified. // With a factor of 1, the deposit amount is the source shares. This picks an arbitrary factor to ensure factors are accounted for in production code. -func (builder DepositBuilder) WithSourceShares(denom string, shares int64) DepositBuilder { +func (builder HardDepositBuilder) WithSourceShares(denom string, shares int64) HardDepositBuilder { if !builder.Amount.AmountOf(denom).Equal(sdk.ZeroInt()) { panic("adding to amount with existing denom not implemented") } @@ -328,7 +328,7 @@ func (builder DepositBuilder) WithSourceShares(denom string, shares int64) Depos } // WithArbitrarySourceShares adds arbitrary deposit amounts and indexes for each specified denom. -func (builder DepositBuilder) WithArbitrarySourceShares(denoms ...string) DepositBuilder { +func (builder HardDepositBuilder) WithArbitrarySourceShares(denoms ...string) HardDepositBuilder { const arbitraryShares = 1e9 for _, denom := range denoms { builder = builder.WithSourceShares(denom, arbitraryShares) diff --git a/x/incentive/keeper/rewards_supply_update_test.go b/x/incentive/keeper/rewards_supply_update_test.go index 335df05e..fd04f878 100644 --- a/x/incentive/keeper/rewards_supply_update_test.go +++ b/x/incentive/keeper/rewards_supply_update_test.go @@ -29,7 +29,7 @@ func (suite *UpdateHardSupplyIndexDenomsTests) TestClaimIndexesAreRemovedForDeno // remove one denom from the indexes already in the deposit expectedIndexes := claim.SupplyRewardIndexes[1:] - deposit := NewDepositBuilder(claim.Owner). + deposit := NewHardDepositBuilder(claim.Owner). WithArbitrarySourceShares(extractCollateralTypes(expectedIndexes)...). Build() @@ -50,7 +50,7 @@ func (suite *UpdateHardSupplyIndexDenomsTests) TestClaimIndexesAreAddedForNewlyS globalIndexes := appendUniqueMultiRewardIndex(claim.SupplyRewardIndexes) suite.storeGlobalSupplyIndexes(globalIndexes) - deposit := NewDepositBuilder(claim.Owner). + deposit := NewHardDepositBuilder(claim.Owner). WithArbitrarySourceShares(extractCollateralTypes(globalIndexes)...). Build() @@ -72,7 +72,7 @@ func (suite *UpdateHardSupplyIndexDenomsTests) TestClaimIndexesAreUnchangedWhenS // UpdateHardSupplyIndexDenoms should ignore the new values. suite.storeGlobalSupplyIndexes(increaseAllRewardFactors(claim.SupplyRewardIndexes)) - deposit := NewDepositBuilder(claim.Owner). + deposit := NewHardDepositBuilder(claim.Owner). WithArbitrarySourceShares(extractCollateralTypes(claim.SupplyRewardIndexes)...). Build() @@ -95,7 +95,7 @@ func (suite *UpdateHardSupplyIndexDenomsTests) TestEmptyClaimIndexesAreAddedForN // add a denom to the deposited amount that is not in the global or claim's indexes expectedIndexes := appendUniqueEmptyMultiRewardIndex(claim.SupplyRewardIndexes) depositedDenoms := extractCollateralTypes(expectedIndexes) - deposit := NewDepositBuilder(claim.Owner). + deposit := NewHardDepositBuilder(claim.Owner). WithArbitrarySourceShares(depositedDenoms...). Build() diff --git a/x/incentive/keeper/unit_test.go b/x/incentive/keeper/unit_test.go index fd6bc295..f471fd69 100644 --- a/x/incentive/keeper/unit_test.go +++ b/x/incentive/keeper/unit_test.go @@ -92,6 +92,11 @@ func (suite *unitTester) storeGlobalSwapIndexes(indexes types.MultiRewardIndexes suite.keeper.SetSwapRewardIndexes(suite.ctx, i.CollateralType, i.RewardIndexes) } } +func (suite *unitTester) storeGlobalSavingsIndexes(indexes types.MultiRewardIndexes) { + for _, i := range indexes { + suite.keeper.SetSavingsRewardIndexes(suite.ctx, i.CollateralType, i.RewardIndexes) + } +} func (suite *unitTester) storeHardClaim(claim types.HardLiquidityProviderClaim) { suite.keeper.SetHardLiquidityProviderClaim(suite.ctx, claim) @@ -102,6 +107,9 @@ func (suite *unitTester) storeDelegatorClaim(claim types.DelegatorClaim) { func (suite *unitTester) storeSwapClaim(claim types.SwapClaim) { suite.keeper.SetSwapClaim(suite.ctx, claim) } +func (suite *unitTester) storeSavingsClaim(claim types.SavingsClaim) { + suite.keeper.SetSavingsClaim(suite.ctx, claim) +} // fakeParamSubspace is a stub paramSpace to simplify keeper unit test setup. type fakeParamSubspace struct { diff --git a/x/savings/keeper/deposit.go b/x/savings/keeper/deposit.go index 81be0fdf..e57d7d80 100644 --- a/x/savings/keeper/deposit.go +++ b/x/savings/keeper/deposit.go @@ -21,13 +21,20 @@ func (k Keeper) Deposit(ctx sdk.Context, depositor sdk.AccAddress, coins sdk.Coi } currDeposit, foundDeposit := k.GetDeposit(ctx, depositor) - amount := coins + + deposit := types.NewDeposit(depositor, coins) if foundDeposit { - amount = amount.Add(currDeposit.Amount...) + deposit.Amount = deposit.Amount.Add(currDeposit.Amount...) + k.hooks.BeforeSavingsDepositModified(ctx, deposit, setDifference(getDenoms(coins), getDenoms(deposit.Amount))) + } - deposit := types.NewDeposit(depositor, amount) + k.SetDeposit(ctx, deposit) + if !foundDeposit { + k.hooks.AfterSavingsDepositCreated(ctx, deposit) + } + ctx.EventManager().EmitEvent( sdk.NewEvent( types.EventTypeSavingsDeposit, @@ -43,7 +50,7 @@ func (k Keeper) Deposit(ctx sdk.Context, depositor sdk.AccAddress, coins sdk.Coi func (k Keeper) ValidateDeposit(ctx sdk.Context, coins sdk.Coins) error { for _, coin := range coins { supported := k.IsDenomSupported(ctx, coin.Denom) - if supported == false { + if !supported { return sdkerrors.Wrapf(types.ErrInvalidDepositDenom, ": %s", coin.Denom) } } @@ -56,3 +63,27 @@ func (k Keeper) GetTotalDeposited(ctx sdk.Context, depositDenom string) (total s macc := k.accountKeeper.GetModuleAccount(ctx, types.ModuleAccountName) return k.bankKeeper.GetBalance(ctx, macc.GetAddress(), depositDenom).Amount } + +// Set setDifference: A - B +func setDifference(a, b []string) (diff []string) { + m := make(map[string]bool) + + for _, item := range b { + m[item] = true + } + + for _, item := range a { + if _, ok := m[item]; !ok { + diff = append(diff, item) + } + } + return +} + +func getDenoms(coins sdk.Coins) []string { + denoms := []string{} + for _, coin := range coins { + denoms = append(denoms, coin.Denom) + } + return denoms +} diff --git a/x/savings/keeper/diff_test.go b/x/savings/keeper/diff_test.go new file mode 100644 index 00000000..a032a06c --- /dev/null +++ b/x/savings/keeper/diff_test.go @@ -0,0 +1,29 @@ +package keeper + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSetDiff(t *testing.T) { + tests := []struct { + name string + setA []string + setB []string + expected []string + }{ + {"empty", []string{}, []string{}, []string(nil)}, + {"diff equal sets", []string{"busd", "usdx"}, []string{"busd", "usdx"}, []string(nil)}, + {"diff set empty", []string{"bnb", "ukava", "usdx"}, []string{}, []string{"bnb", "ukava", "usdx"}}, + {"input set empty", []string{}, []string{"bnb", "ukava", "usdx"}, []string(nil)}, + {"diff set with common elements", []string{"bnb", "btcb", "usdx", "xrpb"}, []string{"bnb", "usdx"}, []string{"btcb", "xrpb"}}, + {"diff set with all common elements", []string{"bnb", "usdx"}, []string{"bnb", "btcb", "usdx", "xrpb"}, []string(nil)}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.expected, setDifference(tt.setA, tt.setB)) + }) + } +}