package keeper_test import ( "testing" "time" sdk "github.com/cosmos/cosmos-sdk/types" proposaltypes "github.com/cosmos/cosmos-sdk/x/params/types/proposal" "github.com/stretchr/testify/suite" tmproto "github.com/tendermint/tendermint/proto/tendermint/types" "github.com/kava-labs/kava/app" cdpkeeper "github.com/kava-labs/kava/x/cdp/keeper" cdptypes "github.com/kava-labs/kava/x/cdp/types" "github.com/kava-labs/kava/x/incentive/keeper" "github.com/kava-labs/kava/x/incentive/testutil" "github.com/kava-labs/kava/x/incentive/types" kavadisttypes "github.com/kava-labs/kava/x/kavadist/types" ) type USDXIntegrationTests struct { testutil.IntegrationTester genesisTime time.Time addrs []sdk.AccAddress } func TestUSDXIntegration(t *testing.T) { suite.Run(t, new(USDXIntegrationTests)) } // SetupTest is run automatically before each suite test func (suite *USDXIntegrationTests) SetupTest() { _, suite.addrs = app.GeneratePrivKeyAddressPairs(5) suite.genesisTime = time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC) } func (suite *USDXIntegrationTests) ProposeAndVoteOnNewRewardPeriods(committeeID uint64, voter sdk.AccAddress, newPeriods types.RewardPeriods) { suite.ProposeAndVoteOnNewParams( voter, committeeID, []proposaltypes.ParamChange{{ Subspace: types.ModuleName, Key: string(types.KeyUSDXMintingRewardPeriods), Value: string(types.ModuleCdc.LegacyAmino.MustMarshalJSON(newPeriods)), }}) } func (suite *USDXIntegrationTests) TestSingleUserAccumulatesRewardsAfterSyncing() { userA := suite.addrs[0] authBulder := app.NewAuthBankGenesisBuilder(). WithSimpleModuleAccount(kavadisttypes.ModuleName, cs(c(types.USDXMintingRewardDenom, 1e18))). // Fill kavadist with enough coins to pay out any reward WithSimpleAccount(userA, cs(c("bnb", 1e12))) // give the user some coins incentBuilder := testutil.NewIncentiveGenesisBuilder(). WithGenesisTime(suite.genesisTime). WithMultipliers(types.MultipliersPerDenoms{{ Denom: types.USDXMintingRewardDenom, Multipliers: types.Multipliers{types.NewMultiplier("large", 12, d("1.0"))}, // keep payout at 1.0 to make maths easier }}). WithSimpleUSDXRewardPeriod("bnb-a", c(types.USDXMintingRewardDenom, 1e6)) suite.SetApp() suite.WithGenesisTime(suite.genesisTime) suite.StartChain( NewPricefeedGenStateMultiFromTime(suite.App.AppCodec(), suite.genesisTime), NewCDPGenStateMulti(suite.App.AppCodec()), authBulder.BuildMarshalled(suite.App.AppCodec()), incentBuilder.BuildMarshalled(suite.App.AppCodec()), ) // User creates a CDP to begin earning rewards. suite.NoError( suite.DeliverMsgCreateCDP(userA, c("bnb", 1e10), c(cdptypes.DefaultStableDenom, 1e9), "bnb-a"), ) // Let time pass to accumulate interest on the deposit // Use one long block instead of many to reduce any rounding errors, and speed up tests. suite.NextBlockAfter(1e6 * time.Second) // about 12 days // User repays and borrows just to sync their CDP suite.NoError( suite.DeliverCDPMsgRepay(userA, "bnb-a", c(cdptypes.DefaultStableDenom, 1)), ) suite.NoError( suite.DeliverCDPMsgBorrow(userA, "bnb-a", c(cdptypes.DefaultStableDenom, 1)), ) // Accumulate more rewards. // The user still has the same percentage of all CDP debt (100%) so their rewards should be the same as in the previous block. suite.NextBlockAfter(1e6 * time.Second) // about 12 days // User claims all their rewards msg := types.NewMsgClaimUSDXMintingReward(userA.String(), "large") suite.NoError(suite.DeliverIncentiveMsg(&msg)) // The users has always had 100% of cdp debt, so they should receive all rewards for the previous two blocks. // Total rewards for each block is block duration * rewards per second accuracy := 1e-18 // using a very high accuracy to flag future small calculation changes suite.BalanceInEpsilon(userA, cs(c("bnb", 1e12-1e10), c(cdptypes.DefaultStableDenom, 1e9), c(types.USDXMintingRewardDenom, 2*1e6*1e6)), accuracy) } func (suite *USDXIntegrationTests) TestSingleUserAccumulatesRewardsWithoutSyncing() { user := suite.addrs[0] initialCollateral := c("bnb", 1e9) authBuilder := app.NewAuthBankGenesisBuilder(). WithSimpleModuleAccount(kavadisttypes.ModuleName, cs(c(types.USDXMintingRewardDenom, 1e18))). // Fill kavadist with enough coins to pay out any reward WithSimpleAccount(user, cs(initialCollateral)) collateralType := "bnb-a" incentBuilder := testutil.NewIncentiveGenesisBuilder(). WithGenesisTime(suite.genesisTime). WithMultipliers(types.MultipliersPerDenoms{{ Denom: types.USDXMintingRewardDenom, Multipliers: types.Multipliers{types.NewMultiplier("large", 12, d("1.0"))}, // keep payout at 1.0 to make maths easier }}). WithSimpleUSDXRewardPeriod(collateralType, c(types.USDXMintingRewardDenom, 1e6)) suite.SetApp() suite.WithGenesisTime(suite.genesisTime) suite.StartChain( authBuilder.BuildMarshalled(suite.App.AppCodec()), NewPricefeedGenStateMultiFromTime(suite.App.AppCodec(), suite.genesisTime), NewCDPGenStateMulti(suite.App.AppCodec()), incentBuilder.BuildMarshalled(suite.App.AppCodec()), ) // Setup cdp state containing one CDP suite.NoError( suite.DeliverMsgCreateCDP(user, initialCollateral, c("usdx", 1e8), collateralType), ) // Skip ahead a few blocks blocks to accumulate both interest and usdx reward for the cdp // Don't sync the CDP between the blocks suite.NextBlockAfter(1e6 * time.Second) // about 12 days suite.NextBlockAfter(1e6 * time.Second) suite.NextBlockAfter(1e6 * time.Second) msg := types.NewMsgClaimUSDXMintingReward(user.String(), "large") suite.NoError(suite.DeliverIncentiveMsg(&msg)) // The users has always had 100% of cdp debt, so they should receive all rewards for the previous two blocks. // Total rewards for each block is block duration * rewards per second accuracy := 1e-18 // using a very high accuracy to flag future small calculation changes suite.BalanceInEpsilon(user, cs(c(cdptypes.DefaultStableDenom, 1e8), c(types.USDXMintingRewardDenom, 3*1e6*1e6)), accuracy) } func (suite *USDXIntegrationTests) TestReinstatingRewardParamsDoesNotTriggerOverPayments() { userA := suite.addrs[0] userB := suite.addrs[1] authBuilder := app.NewAuthBankGenesisBuilder(). WithSimpleModuleAccount(kavadisttypes.ModuleName, cs(c(types.USDXMintingRewardDenom, 1e18))). // Fill kavadist with enough coins to pay out any reward WithSimpleAccount(userA, cs(c("bnb", 1e10))). WithSimpleAccount(userB, cs(c("bnb", 1e10))) incentBuilder := testutil.NewIncentiveGenesisBuilder(). WithGenesisTime(suite.genesisTime). WithMultipliers(types.MultipliersPerDenoms{{ Denom: types.USDXMintingRewardDenom, Multipliers: types.Multipliers{types.NewMultiplier("large", 12, d("1.0"))}, // keep payout at 1.0 to make maths easier }}). WithSimpleUSDXRewardPeriod("bnb-a", c(types.USDXMintingRewardDenom, 1e6)) suite.SetApp() suite.WithGenesisTime(suite.genesisTime) suite.StartChain( authBuilder.BuildMarshalled(suite.App.AppCodec()), NewPricefeedGenStateMultiFromTime(suite.App.AppCodec(), suite.genesisTime), NewCDPGenStateMulti(suite.App.AppCodec()), incentBuilder.BuildMarshalled(suite.App.AppCodec()), NewCommitteeGenesisState(suite.App.AppCodec(), 0, userA), // create a committtee to change params ) // Accumulate some CDP rewards, requires creating a cdp so the total borrowed isn't 0. suite.NoError( suite.DeliverMsgCreateCDP(userA, c("bnb", 1e10), c("usdx", 1e9), "bnb-a"), ) suite.NextBlockAfter(1e6 * time.Second) // Remove the USDX reward period suite.ProposeAndVoteOnNewRewardPeriods(0, userA, types.RewardPeriods{}) // next block so proposal is enacted suite.NextBlockAfter(1 * time.Second) // Create a CDP when there is no reward periods. In a previous version the claim object would not be created, leading to the bug. // Withdraw the same amount of usdx as the first cdp currently has. This make the reward maths easier, as rewards will be split 50:50 between each cdp. firstCDP, f := suite.App.GetCDPKeeper().GetCdpByOwnerAndCollateralType(suite.Ctx, userA, "bnb-a") suite.True(f) firstCDPTotalPrincipal := firstCDP.GetTotalPrincipal() suite.NoError( suite.DeliverMsgCreateCDP(userB, c("bnb", 1e10), firstCDPTotalPrincipal, "bnb-a"), ) // Add back the reward period suite.ProposeAndVoteOnNewRewardPeriods(0, userA, types.RewardPeriods{types.NewRewardPeriod( true, "bnb-a", suite.Ctx.BlockTime(), // start accumulating again from this block suite.genesisTime.Add(365*24*time.Hour), c(types.USDXMintingRewardDenom, 1e6), )}, ) // next block so proposal is enacted suite.NextBlockAfter(1 * time.Second) // Sync the cdp and claim by borrowing a bit // In a previous version this would create the cdp with incorrect indexes, leading to overpayment. suite.NoError( suite.DeliverCDPMsgBorrow(userB, "bnb-a", c(cdptypes.DefaultStableDenom, 1)), ) // Claim rewards msg := types.NewMsgClaimUSDXMintingReward(userB.String(), "large") suite.NoError(suite.DeliverIncentiveMsg(&msg)) // The cdp had half the total borrows for a 1s block. So should earn half the rewards for that block suite.BalanceInEpsilon( userB, cs(firstCDPTotalPrincipal.Add(c(cdptypes.DefaultStableDenom, 1)), c(types.USDXMintingRewardDenom, 0.5*1e6)), 1e-18, // using very high accuracy to catch small changes to the calculations ) } // Test suite used for all keeper tests type USDXRewardsTestSuite struct { suite.Suite keeper keeper.Keeper cdpKeeper cdpkeeper.Keeper app app.TestApp ctx sdk.Context genesisTime time.Time addrs []sdk.AccAddress } // SetupTest is run automatically before each suite test func (suite *USDXRewardsTestSuite) SetupTest() { config := sdk.GetConfig() app.SetBech32AddressPrefixes(config) _, suite.addrs = app.GeneratePrivKeyAddressPairs(5) suite.genesisTime = time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC) } func (suite *USDXRewardsTestSuite) SetupApp() { suite.app = app.NewTestApp() suite.keeper = suite.app.GetIncentiveKeeper() suite.cdpKeeper = suite.app.GetCDPKeeper() suite.ctx = suite.app.NewContext(true, tmproto.Header{Height: 1, Time: suite.genesisTime}) } func (suite *USDXRewardsTestSuite) SetupWithGenState(authBuilder *app.AuthBankGenesisBuilder, incentBuilder testutil.IncentiveGenesisBuilder) { suite.SetupApp() suite.app.InitializeFromGenesisStatesWithTime( suite.genesisTime, authBuilder.BuildMarshalled(suite.app.AppCodec()), NewPricefeedGenStateMultiFromTime(suite.app.AppCodec(), suite.genesisTime), NewCDPGenStateMulti(suite.app.AppCodec()), incentBuilder.BuildMarshalled(suite.app.AppCodec()), ) } func (suite *USDXRewardsTestSuite) TestAccumulateUSDXMintingRewards() { type args struct { ctype string rewardsPerSecond sdk.Coin initialTotalPrincipal sdk.Coin timeElapsed int expectedRewardFactor sdk.Dec } type test struct { name string args args } testCases := []test{ { "7 seconds", args{ ctype: "bnb-a", rewardsPerSecond: c("ukava", 122354), initialTotalPrincipal: c("usdx", 1000000000000), timeElapsed: 7, expectedRewardFactor: d("0.000000856478000000"), }, }, { "1 day", args{ ctype: "bnb-a", rewardsPerSecond: c("ukava", 122354), initialTotalPrincipal: c("usdx", 1000000000000), timeElapsed: 86400, expectedRewardFactor: d("0.0105713856"), }, }, { "0 seconds", args{ ctype: "bnb-a", rewardsPerSecond: c("ukava", 122354), initialTotalPrincipal: c("usdx", 1000000000000), timeElapsed: 0, expectedRewardFactor: d("0.0"), }, }, } for _, tc := range testCases { suite.Run(tc.name, func() { incentBuilder := testutil.NewIncentiveGenesisBuilder().WithGenesisTime(suite.genesisTime).WithSimpleUSDXRewardPeriod(tc.args.ctype, tc.args.rewardsPerSecond) suite.SetupWithGenState(app.NewAuthBankGenesisBuilder(), incentBuilder) // setup cdp state suite.cdpKeeper.SetTotalPrincipal(suite.ctx, tc.args.ctype, cdptypes.DefaultStableDenom, tc.args.initialTotalPrincipal.Amount) updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * tc.args.timeElapsed)) suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime) rewardPeriod, found := suite.keeper.GetUSDXMintingRewardPeriod(suite.ctx, tc.args.ctype) suite.Require().True(found) suite.keeper.AccumulateUSDXMintingRewards(suite.ctx, rewardPeriod) rewardFactor, _ := suite.keeper.GetUSDXMintingRewardFactor(suite.ctx, tc.args.ctype) suite.Require().Equal(tc.args.expectedRewardFactor, rewardFactor) }) } } func (suite *USDXRewardsTestSuite) TestSynchronizeUSDXMintingReward() { type args struct { ctype string rewardsPerSecond sdk.Coin initialCollateral sdk.Coin initialPrincipal sdk.Coin blockTimes []int expectedRewardFactor sdk.Dec expectedRewards sdk.Coin } type test struct { name string args args } testCases := []test{ { "10 blocks", args{ ctype: "bnb-a", rewardsPerSecond: c("ukava", 122354), initialCollateral: c("bnb", 1000000000000), initialPrincipal: c("usdx", 10000000000), blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, expectedRewardFactor: d("0.001223540000000000"), expectedRewards: c("ukava", 12235400), }, }, { "10 blocks - long block time", args{ ctype: "bnb-a", rewardsPerSecond: c("ukava", 122354), initialCollateral: c("bnb", 1000000000000), initialPrincipal: c("usdx", 10000000000), blockTimes: []int{86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400}, expectedRewardFactor: d("10.57138560000000000"), expectedRewards: c("ukava", 105713856000), }, }, } for _, tc := range testCases { suite.Run(tc.name, func() { authBuilder := app.NewAuthBankGenesisBuilder().WithSimpleAccount(suite.addrs[0], cs(tc.args.initialCollateral)) incentBuilder := testutil.NewIncentiveGenesisBuilder().WithGenesisTime(suite.genesisTime).WithSimpleUSDXRewardPeriod(tc.args.ctype, tc.args.rewardsPerSecond) suite.SetupWithGenState(authBuilder, incentBuilder) // setup cdp state err := suite.cdpKeeper.AddCdp(suite.ctx, suite.addrs[0], tc.args.initialCollateral, tc.args.initialPrincipal, tc.args.ctype) suite.Require().NoError(err) claim, found := suite.keeper.GetUSDXMintingClaim(suite.ctx, suite.addrs[0]) suite.Require().True(found) suite.Require().Equal(sdk.ZeroDec(), claim.RewardIndexes[0].RewardFactor) var timeElapsed int previousBlockTime := suite.ctx.BlockTime() for _, t := range tc.args.blockTimes { timeElapsed += t updatedBlockTime := previousBlockTime.Add(time.Duration(int(time.Second) * t)) previousBlockTime = updatedBlockTime blockCtx := suite.ctx.WithBlockTime(updatedBlockTime) rewardPeriod, found := suite.keeper.GetUSDXMintingRewardPeriod(blockCtx, tc.args.ctype) suite.Require().True(found) suite.keeper.AccumulateUSDXMintingRewards(blockCtx, rewardPeriod) } updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * timeElapsed)) suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime) cdp, found := suite.cdpKeeper.GetCdpByOwnerAndCollateralType(suite.ctx, suite.addrs[0], tc.args.ctype) suite.Require().True(found) suite.Require().NotPanics(func() { suite.keeper.SynchronizeUSDXMintingReward(suite.ctx, cdp) }) rewardFactor, _ := suite.keeper.GetUSDXMintingRewardFactor(suite.ctx, tc.args.ctype) suite.Require().Equal(tc.args.expectedRewardFactor, rewardFactor) claim, found = suite.keeper.GetUSDXMintingClaim(suite.ctx, suite.addrs[0]) suite.Require().True(found) suite.Require().Equal(tc.args.expectedRewardFactor, claim.RewardIndexes[0].RewardFactor) suite.Require().Equal(tc.args.expectedRewards, claim.Reward) }) } } func (suite *USDXRewardsTestSuite) TestSimulateUSDXMintingRewardSynchronization() { type args struct { ctype string rewardsPerSecond sdk.Coin initialCollateral sdk.Coin initialPrincipal sdk.Coin blockTimes []int expectedRewardFactor sdk.Dec expectedRewards sdk.Coin } type test struct { name string args args } testCases := []test{ { "10 blocks", args{ ctype: "bnb-a", rewardsPerSecond: c("ukava", 122354), initialCollateral: c("bnb", 1000000000000), initialPrincipal: c("usdx", 10000000000), blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, expectedRewardFactor: d("0.001223540000000000"), expectedRewards: c("ukava", 12235400), }, }, { "10 blocks - long block time", args{ ctype: "bnb-a", rewardsPerSecond: c("ukava", 122354), initialCollateral: c("bnb", 1000000000000), initialPrincipal: c("usdx", 10000000000), blockTimes: []int{86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400}, expectedRewardFactor: d("10.57138560000000000"), expectedRewards: c("ukava", 105713856000), }, }, } for _, tc := range testCases { suite.Run(tc.name, func() { authBuilder := app.NewAuthBankGenesisBuilder().WithSimpleAccount(suite.addrs[0], cs(tc.args.initialCollateral)) incentBuilder := testutil.NewIncentiveGenesisBuilder().WithGenesisTime(suite.genesisTime).WithSimpleUSDXRewardPeriod(tc.args.ctype, tc.args.rewardsPerSecond) suite.SetupWithGenState(authBuilder, incentBuilder) // setup cdp state err := suite.cdpKeeper.AddCdp(suite.ctx, suite.addrs[0], tc.args.initialCollateral, tc.args.initialPrincipal, tc.args.ctype) suite.Require().NoError(err) claim, found := suite.keeper.GetUSDXMintingClaim(suite.ctx, suite.addrs[0]) suite.Require().True(found) suite.Require().Equal(sdk.ZeroDec(), claim.RewardIndexes[0].RewardFactor) var timeElapsed int previousBlockTime := suite.ctx.BlockTime() for _, t := range tc.args.blockTimes { timeElapsed += t updatedBlockTime := previousBlockTime.Add(time.Duration(int(time.Second) * t)) previousBlockTime = updatedBlockTime blockCtx := suite.ctx.WithBlockTime(updatedBlockTime) rewardPeriod, found := suite.keeper.GetUSDXMintingRewardPeriod(blockCtx, tc.args.ctype) suite.Require().True(found) suite.keeper.AccumulateUSDXMintingRewards(blockCtx, rewardPeriod) } updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * timeElapsed)) suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime) claim, found = suite.keeper.GetUSDXMintingClaim(suite.ctx, suite.addrs[0]) suite.Require().True(found) suite.Require().Equal(claim.RewardIndexes[0].RewardFactor, sdk.ZeroDec()) suite.Require().Equal(claim.Reward, sdk.NewCoin("ukava", sdk.ZeroInt())) updatedClaim := suite.keeper.SimulateUSDXMintingSynchronization(suite.ctx, claim) suite.Require().Equal(tc.args.expectedRewardFactor, updatedClaim.RewardIndexes[0].RewardFactor) suite.Require().Equal(tc.args.expectedRewards, updatedClaim.Reward) }) } } func TestUSDXRewardsTestSuite(t *testing.T) { suite.Run(t, new(USDXRewardsTestSuite)) }