0g-chain/x/incentive/keeper/rewards_borrow_sync_test.go
Draco 614d4e40fe
Update cosmos-sdk to v0.47.7 (#1811)
* Update cometbft, cosmos, ethermint, and ibc-go

* Replace github.com/tendermint/tendermint by github.com/cometbft/cometbft

* Replace github.com/tendermint/tm-db by github.com/cometbft/cometbft-db

* Replace gogo/protobuf with cosmos/gogoproto & simapp replacement

* Replace cosmos-sdk/simapp/helpers with cosmos-sdk/testutil/sims

* Remove no longer used simulations

* Replace ibchost with ibcexported
See https://github.com/cosmos/ibc-go/blob/v7.2.2/docs/migrations/v6-to-v7.md#ibc-module-constants

* Add new consensus params keeper

* Add consensus keeper to blockers

* Fix keeper and module issues in app.go

* Add IsSendEnabledCoins and update SetParams interface changes

* Fix protobuf build for cosmos 47 (#1800)

* fix cp errors by using -f; fix lint by only linting our proto dir;
and use proofs.proto directly from ics23 for ibc-go v7

* run proto-all; commit updated third party deps and swagger changes

* regenerate proto files

* use correct gocosmos build plugin for buf

* re-gen all protobuf files to update paths for new gocosmos plugin

* update protoc and buf to latest versions

* fix staking keeper issues in app.go

* update tally handler for gov changes

* chain id fix and flag fixes

* update deps for cometbft 47.7 upgrade

* remove all module legacy queriers

* update stakingKeeper to pointer

* Replace ModuleCdc from govv1beta1 to govcodec

* remove simulations

* abci.LastCommitInfo → abci.CommitInfo

* Remove unused code in keys.go

* simapp.MakeTestEncodingConfig -> moduletestutil.MakeTestEncodingConfi

* Fix chain id issues in tests

* Fix remaining unit test issues

* Update changelog for upgrade

* Fix e2e tests using updated kvtool

* Update protonet to v47 compatible genesis

* Bump cometbft-db to v0.9.1-kava.1

* Update kvtool

* Remove extra changelog

* Fix merged rocksdb issues

* go mod cleanup

* Bump cometbft-db to v9 and go to 1.21

* Bump rocksdb version to v8.10.0

* Update kvtool to latest version

* Update gin to v1.9.0

* Use ibctm.ModuleName in app_test

* Fallback to genesis chain id instead of client toml

* Remove all simulations

* Fix cdp migrations issue with v47

* Update dependencies to correct tags

---------

Co-authored-by: Nick DeLuca <nickdeluca08@gmail.com>
2024-02-06 17:54:10 -05:00

569 lines
16 KiB
Go

package keeper_test
import (
"errors"
"testing"
sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
hardtypes "github.com/kava-labs/kava/x/hard/types"
"github.com/kava-labs/kava/x/incentive/keeper"
"github.com/kava-labs/kava/x/incentive/types"
)
// SynchronizeHardBorrowRewardTests runs unit tests for the keeper.SynchronizeHardBorrowReward method
type SynchronizeHardBorrowRewardTests struct {
unitTester
}
func TestSynchronizeHardBorrowReward(t *testing.T) {
suite.Run(t, new(SynchronizeHardBorrowRewardTests))
}
func (suite *SynchronizeHardBorrowRewardTests) TestClaimIndexesAreUpdatedWhenGlobalIndexesHaveIncreased() {
// This is the normal case
claim := types.HardLiquidityProviderClaim{
BaseMultiClaim: types.BaseMultiClaim{
Owner: arbitraryAddress(),
},
BorrowRewardIndexes: nonEmptyMultiRewardIndexes,
}
suite.storeHardClaim(claim)
globalIndexes := increaseAllRewardFactors(nonEmptyMultiRewardIndexes)
suite.storeGlobalBorrowIndexes(globalIndexes)
borrow := NewBorrowBuilder(claim.Owner).
WithArbitrarySourceShares(extractCollateralTypes(claim.BorrowRewardIndexes)...).
Build()
suite.keeper.SynchronizeHardBorrowReward(suite.ctx, borrow)
syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
suite.Equal(globalIndexes, syncedClaim.BorrowRewardIndexes)
}
func (suite *SynchronizeHardBorrowRewardTests) TestClaimIndexesAreUnchangedWhenGlobalIndexesUnchanged() {
// It should be safe to call SynchronizeHardBorrowReward multiple times
unchangingIndexes := nonEmptyMultiRewardIndexes
claim := types.HardLiquidityProviderClaim{
BaseMultiClaim: types.BaseMultiClaim{
Owner: arbitraryAddress(),
},
BorrowRewardIndexes: unchangingIndexes,
}
suite.storeHardClaim(claim)
suite.storeGlobalBorrowIndexes(unchangingIndexes)
borrow := NewBorrowBuilder(claim.Owner).
WithArbitrarySourceShares(extractCollateralTypes(unchangingIndexes)...).
Build()
suite.keeper.SynchronizeHardBorrowReward(suite.ctx, borrow)
syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
suite.Equal(unchangingIndexes, syncedClaim.BorrowRewardIndexes)
}
func (suite *SynchronizeHardBorrowRewardTests) TestClaimIndexesAreUpdatedWhenNewRewardAdded() {
// When a new reward is added (via gov) for a hard borrow denom the user has already borrowed, and the claim is synced;
// Then the new reward's index should be added to the claim.
claim := types.HardLiquidityProviderClaim{
BaseMultiClaim: types.BaseMultiClaim{
Owner: arbitraryAddress(),
},
BorrowRewardIndexes: nonEmptyMultiRewardIndexes,
}
suite.storeHardClaim(claim)
globalIndexes := appendUniqueMultiRewardIndex(nonEmptyMultiRewardIndexes)
suite.storeGlobalBorrowIndexes(globalIndexes)
borrow := NewBorrowBuilder(claim.Owner).
WithArbitrarySourceShares(extractCollateralTypes(globalIndexes)...).
Build()
suite.keeper.SynchronizeHardBorrowReward(suite.ctx, borrow)
syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
suite.Equal(globalIndexes, syncedClaim.BorrowRewardIndexes)
}
func (suite *SynchronizeHardBorrowRewardTests) TestClaimIndexesAreUpdatedWhenNewRewardDenomAdded() {
// When a new reward coin is added (via gov) to an already rewarded borrow denom (that the user has already borrowed), and the claim is synced;
// Then the new reward coin's index should be added to the claim.
claim := types.HardLiquidityProviderClaim{
BaseMultiClaim: types.BaseMultiClaim{
Owner: arbitraryAddress(),
},
BorrowRewardIndexes: nonEmptyMultiRewardIndexes,
}
suite.storeHardClaim(claim)
globalIndexes := appendUniqueRewardIndexToFirstItem(nonEmptyMultiRewardIndexes)
suite.storeGlobalBorrowIndexes(globalIndexes)
borrow := NewBorrowBuilder(claim.Owner).
WithArbitrarySourceShares(extractCollateralTypes(globalIndexes)...).
Build()
suite.keeper.SynchronizeHardBorrowReward(suite.ctx, borrow)
syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
suite.Equal(globalIndexes, syncedClaim.BorrowRewardIndexes)
}
func (suite *SynchronizeHardBorrowRewardTests) TestRewardIsIncrementedWhenGlobalIndexesHaveIncreased() {
// 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
originalReward := arbitraryCoins()
claim := types.HardLiquidityProviderClaim{
BaseMultiClaim: types.BaseMultiClaim{
Owner: arbitraryAddress(),
Reward: originalReward,
},
BorrowRewardIndexes: types.MultiRewardIndexes{
{
CollateralType: "borrowdenom",
RewardIndexes: types.RewardIndexes{
{
CollateralType: "rewarddenom",
RewardFactor: d("1000.001"),
},
},
},
},
}
suite.storeHardClaim(claim)
suite.storeGlobalBorrowIndexes(types.MultiRewardIndexes{
{
CollateralType: "borrowdenom",
RewardIndexes: types.RewardIndexes{
{
CollateralType: "rewarddenom",
RewardFactor: d("2000.002"),
},
},
},
})
borrow := NewBorrowBuilder(claim.Owner).
WithSourceShares("borrowdenom", 1e9).
Build()
suite.keeper.SynchronizeHardBorrowReward(suite.ctx, borrow)
// new reward is (new index - old index) * borrow amount
syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
suite.Equal(
cs(c("rewarddenom", 1_000_001_000_000)).Add(originalReward...),
syncedClaim.Reward,
)
}
func (suite *SynchronizeHardBorrowRewardTests) TestRewardIsIncrementedWhenNewRewardAdded() {
// When a new reward is added (via gov) for a hard borrow denom the user has already borrowed, and the claim is synced
// Then the user earns rewards for the time since the reward was added
originalReward := arbitraryCoins()
claim := types.HardLiquidityProviderClaim{
BaseMultiClaim: types.BaseMultiClaim{
Owner: arbitraryAddress(),
Reward: originalReward,
},
BorrowRewardIndexes: types.MultiRewardIndexes{
{
CollateralType: "rewarded",
RewardIndexes: types.RewardIndexes{
{
CollateralType: "reward",
RewardFactor: d("1000.001"),
},
},
},
},
}
suite.storeHardClaim(claim)
globalIndexes := types.MultiRewardIndexes{
{
CollateralType: "rewarded",
RewardIndexes: types.RewardIndexes{
{
CollateralType: "reward",
RewardFactor: d("2000.002"),
},
},
},
{
CollateralType: "newlyrewarded",
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.storeGlobalBorrowIndexes(globalIndexes)
borrow := NewBorrowBuilder(claim.Owner).
WithSourceShares("rewarded", 1e9).
WithSourceShares("newlyrewarded", 1e9).
Build()
suite.keeper.SynchronizeHardBorrowReward(suite.ctx, borrow)
// new reward is (new index - old index) * borrow amount for each borrowed denom
// The old index for `newlyrewarded` isn't in the claim, so it's added starting at 0 for calculating the reward.
syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
suite.Equal(
cs(c("otherreward", 1_000_001_000_000), c("reward", 1_000_001_000_000)).Add(originalReward...),
syncedClaim.Reward,
)
}
func (suite *SynchronizeHardBorrowRewardTests) TestRewardIsIncrementedWhenNewRewardDenomAdded() {
// When a new reward coin is added (via gov) to an already rewarded borrow denom (that the user has already borrowed), and the claim is synced;
// Then the user earns rewards for the time since the reward was added
originalReward := arbitraryCoins()
claim := types.HardLiquidityProviderClaim{
BaseMultiClaim: types.BaseMultiClaim{
Owner: arbitraryAddress(),
Reward: originalReward,
},
BorrowRewardIndexes: types.MultiRewardIndexes{
{
CollateralType: "borrowed",
RewardIndexes: types.RewardIndexes{
{
CollateralType: "reward",
RewardFactor: d("1000.001"),
},
},
},
},
}
suite.storeHardClaim(claim)
globalIndexes := types.MultiRewardIndexes{
{
CollateralType: "borrowed",
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.storeGlobalBorrowIndexes(globalIndexes)
borrow := NewBorrowBuilder(claim.Owner).
WithSourceShares("borrowed", 1e9).
Build()
suite.keeper.SynchronizeHardBorrowReward(suite.ctx, borrow)
// new reward is (new index - old index) * borrow amount for each borrowed denom
// The old index for `otherreward` isn't in the claim, so it's added starting at 0 for calculating the reward.
syncedClaim, _ := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, claim.Owner)
suite.Equal(
cs(c("reward", 1_000_001_000_000), c("otherreward", 1_000_001_000_000)).Add(originalReward...),
syncedClaim.Reward,
)
}
// BorrowBuilder is a tool for creating a hard borrows.
// The builder inherits from hard.Borrow, so fields can be accessed directly if a helper method doesn't exist.
type BorrowBuilder struct {
hardtypes.Borrow
}
// NewBorrowBuilder creates a BorrowBuilder containing an empty borrow.
func NewBorrowBuilder(borrower sdk.AccAddress) BorrowBuilder {
return BorrowBuilder{
Borrow: hardtypes.Borrow{
Borrower: borrower,
},
}
}
// Build assembles and returns the final borrow.
func (builder BorrowBuilder) Build() hardtypes.Borrow { return builder.Borrow }
// WithSourceShares adds a borrow amount and factor such that the source shares for this borrow is equal to specified.
// With a factor of 1, the borrow amount is the source shares. This picks an arbitrary factor to ensure factors are accounted for in production code.
func (builder BorrowBuilder) WithSourceShares(denom string, shares int64) BorrowBuilder {
if !builder.Amount.AmountOf(denom).Equal(sdk.ZeroInt()) {
panic("adding to amount with existing denom not implemented")
}
if _, f := builder.Index.GetInterestFactor(denom); f {
panic("adding to indexes with existing denom not implemented")
}
// pick arbitrary factor
factor := sdk.MustNewDecFromStr("2")
// Calculate borrow amount that would equal the requested source shares given the above factor.
amt := sdkmath.NewInt(shares).Mul(factor.RoundInt())
builder.Amount = builder.Amount.Add(sdk.NewCoin(denom, amt))
builder.Index = builder.Index.SetInterestFactor(denom, factor)
return builder
}
// WithArbitrarySourceShares adds arbitrary borrow amounts and indexes for each specified denom.
func (builder BorrowBuilder) WithArbitrarySourceShares(denoms ...string) BorrowBuilder {
const arbitraryShares = 1e9
for _, denom := range denoms {
builder = builder.WithSourceShares(denom, arbitraryShares)
}
return builder
}
func TestCalculateRewards(t *testing.T) {
type expected struct {
err error
coins sdk.Coins
}
type args struct {
oldIndexes, newIndexes types.RewardIndexes
sourceAmount sdk.Dec
}
testcases := []struct {
name string
args args
expected expected
}{
{
name: "when old and new indexes have same denoms, rewards are calculated correctly",
args: args{
oldIndexes: types.RewardIndexes{
{
CollateralType: "hard",
RewardFactor: d("0.000000001"),
},
{
CollateralType: "ukava",
RewardFactor: d("0.1"),
},
},
newIndexes: types.RewardIndexes{
{
CollateralType: "hard",
RewardFactor: d("1000.0"),
},
{
CollateralType: "ukava",
RewardFactor: d("0.100000001"),
},
},
sourceAmount: d("1000000000"),
},
expected: expected{
// for each denom: (new - old) * sourceAmount
coins: cs(c("hard", 999999999999), c("ukava", 1)),
},
},
{
name: "when new indexes have an extra denom, rewards are calculated as if it was 0 in old indexes",
args: args{
oldIndexes: types.RewardIndexes{
{
CollateralType: "hard",
RewardFactor: d("0.000000001"),
},
},
newIndexes: types.RewardIndexes{
{
CollateralType: "hard",
RewardFactor: d("1000.0"),
},
{
CollateralType: "ukava",
RewardFactor: d("0.100000001"),
},
},
sourceAmount: d("1000000000"),
},
expected: expected{
// for each denom: (new - old) * sourceAmount
coins: cs(c("hard", 999999999999), c("ukava", 100000001)),
},
},
{
name: "when new indexes are smaller than old, an error is returned",
args: args{
oldIndexes: types.RewardIndexes{
{
CollateralType: "hard",
RewardFactor: d("0.2"),
},
},
newIndexes: types.RewardIndexes{
{
CollateralType: "hard",
RewardFactor: d("0.1"),
},
},
sourceAmount: d("1000000000"),
},
expected: expected{
err: types.ErrDecreasingRewardFactor,
},
},
{
name: "when old indexes have an extra denom, an error is returned",
args: args{
oldIndexes: types.RewardIndexes{
{
CollateralType: "hard",
RewardFactor: d("0.1"),
},
{
CollateralType: "ukava",
RewardFactor: d("0.1"),
},
},
newIndexes: types.RewardIndexes{
{
CollateralType: "hard",
RewardFactor: d("0.2"),
},
},
sourceAmount: d("1000000000"),
},
expected: expected{
err: types.ErrDecreasingRewardFactor,
},
},
{
name: "when old and new indexes are 0, rewards are 0",
args: args{
oldIndexes: types.RewardIndexes{
{
CollateralType: "hard",
RewardFactor: d("0.0"),
},
},
newIndexes: types.RewardIndexes{
{
CollateralType: "hard",
RewardFactor: d("0.0"),
},
},
sourceAmount: d("1000000000"),
},
expected: expected{
coins: sdk.Coins{},
},
},
{
name: "when old and new indexes are empty, rewards are 0",
args: args{
oldIndexes: types.RewardIndexes{},
newIndexes: nil,
sourceAmount: d("1000000000"),
},
expected: expected{
coins: nil,
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
coins, err := keeper.Keeper{}.CalculateRewards(tc.args.oldIndexes, tc.args.newIndexes, tc.args.sourceAmount)
if tc.expected.err != nil {
require.True(t, errors.Is(err, tc.expected.err))
} else {
require.Equal(t, tc.expected.coins, coins)
}
})
}
}
func TestCalculateSingleReward(t *testing.T) {
type expected struct {
err error
reward sdkmath.Int
}
type args struct {
oldIndex, newIndex sdk.Dec
sourceAmount sdk.Dec
}
testcases := []struct {
name string
args args
expected expected
}{
{
name: "when new index is > old, rewards are calculated correctly",
args: args{
oldIndex: d("0.000000001"),
newIndex: d("1000.0"),
sourceAmount: d("1000000000"),
},
expected: expected{
// (new - old) * sourceAmount
reward: i(999999999999),
},
},
{
name: "when new index is < old, an error is returned",
args: args{
oldIndex: d("0.000000001"),
newIndex: d("0.0"),
sourceAmount: d("1000000000"),
},
expected: expected{
err: types.ErrDecreasingRewardFactor,
},
},
{
name: "when old and new indexes are 0, rewards are 0",
args: args{
oldIndex: d("0.0"),
newIndex: d("0.0"),
sourceAmount: d("1000000000"),
},
expected: expected{
reward: sdk.ZeroInt(),
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
reward, err := keeper.Keeper{}.CalculateSingleReward(tc.args.oldIndex, tc.args.newIndex, tc.args.sourceAmount)
if tc.expected.err != nil {
require.True(t, errors.Is(err, tc.expected.err))
} else {
require.Equal(t, tc.expected.reward, reward)
}
})
}
}