Add GetSynchronizedClaim and swap adapter (#1386)

* Add source adapters to keeper, implement GetSynchronizedClaim

* Iterate acc shares sorted

* Add swap adapter, update tests to use swap claimtype

* Add swap adapter test

* Add tests for non-empty pools

* Iterate over source ids instead of sorted keys
This commit is contained in:
Derrick Lee 2022-11-14 17:55:10 -08:00 committed by GitHub
parent c2061f626e
commit f52a581ea9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 550 additions and 21 deletions

View File

@ -0,0 +1,46 @@
package swap
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/incentive/types"
)
var _ types.SourceAdapter = SourceAdapter{}
type SourceAdapter struct {
keeper types.SwapKeeper
}
func NewSourceAdapter(keeper types.SwapKeeper) SourceAdapter {
return SourceAdapter{
keeper: keeper,
}
}
func (f SourceAdapter) TotalSharesBySource(ctx sdk.Context, sourceID string) sdk.Dec {
shares, found := f.keeper.GetPoolShares(ctx, sourceID)
if !found {
shares = sdk.ZeroInt()
}
return shares.ToDec()
}
func (f SourceAdapter) OwnerSharesBySource(
ctx sdk.Context,
owner sdk.AccAddress,
sourceIDs []string,
) map[string]sdk.Dec {
shares := make(map[string]sdk.Dec)
for _, id := range sourceIDs {
s, found := f.keeper.GetDepositorSharesAmount(ctx, owner, id)
if !found {
s = sdk.ZeroInt()
}
shares[id] = s.ToDec()
}
return shares
}

View File

@ -0,0 +1,283 @@
package swap_test
import (
"testing"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
tmprototypes "github.com/tendermint/tendermint/proto/tendermint/types"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/incentive/keeper/adapters/swap"
swaptypes "github.com/kava-labs/kava/x/swap/types"
"github.com/stretchr/testify/suite"
)
type SwapAdapterTestSuite struct {
suite.Suite
app app.TestApp
ctx sdk.Context
genesisTime time.Time
addrs []sdk.AccAddress
}
func TestSwapAdapterTestSuite(t *testing.T) {
suite.Run(t, new(SwapAdapterTestSuite))
}
func (suite *SwapAdapterTestSuite) 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)
suite.app = app.NewTestApp()
suite.ctx = suite.app.NewContext(true, tmprototypes.Header{Time: suite.genesisTime})
}
func (suite *SwapAdapterTestSuite) TestSwapAdapter_OwnerSharesBySource_Empty() {
adapter := swap.NewSourceAdapter(suite.app.GetSwapKeeper())
tests := []struct {
name string
giveOwner sdk.AccAddress
giveSourceIDs []string
wantShares map[string]sdk.Dec
}{
{
"empty requests",
suite.addrs[0],
[]string{},
map[string]sdk.Dec{},
},
{
"empty pools are zero",
suite.addrs[0],
[]string{
"pool1",
"pool2",
"pool3",
},
map[string]sdk.Dec{
"pool1": sdk.ZeroDec(),
"pool2": sdk.ZeroDec(),
"pool3": sdk.ZeroDec(),
},
},
}
for _, tt := range tests {
suite.Run(tt.name, func() {
shares := adapter.OwnerSharesBySource(suite.ctx, tt.giveOwner, tt.giveSourceIDs)
suite.Equal(tt.wantShares, shares)
})
}
}
func (suite *SwapAdapterTestSuite) TestSwapAdapter_OwnerSharesBySource() {
poolDenomA := "ukava"
poolDenomB := "usdx"
swapKeeper := suite.app.GetSwapKeeper()
swapKeeper.SetParams(suite.ctx, swaptypes.NewParams(
swaptypes.NewAllowedPools(
swaptypes.NewAllowedPool(poolDenomA, poolDenomB),
),
sdk.ZeroDec(),
))
suite.app.FundAccount(
suite.ctx,
suite.addrs[0],
sdk.NewCoins(
sdk.NewCoin(poolDenomA, sdk.NewInt(1000000000000)),
sdk.NewCoin(poolDenomB, sdk.NewInt(1000000000000)),
),
)
suite.app.FundAccount(
suite.ctx,
suite.addrs[1],
sdk.NewCoins(
sdk.NewCoin(poolDenomA, sdk.NewInt(1000000000000)),
sdk.NewCoin(poolDenomB, sdk.NewInt(1000000000000)),
),
)
err := swapKeeper.Deposit(
suite.ctx,
suite.addrs[0],
sdk.NewCoin(poolDenomA, sdk.NewInt(100)),
sdk.NewCoin(poolDenomB, sdk.NewInt(100)),
sdk.NewDecWithPrec(1, 1),
)
suite.NoError(err)
err = swapKeeper.Deposit(
suite.ctx,
suite.addrs[1],
sdk.NewCoin(poolDenomA, sdk.NewInt(250)),
sdk.NewCoin(poolDenomB, sdk.NewInt(250)),
sdk.NewDecWithPrec(1, 0),
)
suite.NoError(err)
adapter := swap.NewSourceAdapter(suite.app.GetSwapKeeper())
tests := []struct {
name string
giveOwner sdk.AccAddress
giveSourceIDs []string
wantShares map[string]sdk.Dec
}{
{
"depositor has shares",
suite.addrs[0],
[]string{
swaptypes.PoolID(poolDenomA, poolDenomB),
},
map[string]sdk.Dec{
swaptypes.PoolID(poolDenomA, poolDenomB): sdk.NewDecWithPrec(100, 0),
},
},
{
"depositor has shares - including empty deposits",
suite.addrs[1],
[]string{
swaptypes.PoolID(poolDenomA, poolDenomB),
"pool2",
},
map[string]sdk.Dec{
swaptypes.PoolID(poolDenomA, poolDenomB): sdk.NewDecWithPrec(250, 0),
"pool2": sdk.ZeroDec(),
},
},
{
"non-depositor has zero shares",
suite.addrs[2],
[]string{
swaptypes.PoolID(poolDenomA, poolDenomB),
},
map[string]sdk.Dec{
swaptypes.PoolID(poolDenomA, poolDenomB): sdk.ZeroDec(),
},
},
}
for _, tt := range tests {
suite.Run(tt.name, func() {
shares := adapter.OwnerSharesBySource(suite.ctx, tt.giveOwner, tt.giveSourceIDs)
suite.Equal(tt.wantShares, shares)
})
}
}
func (suite *SwapAdapterTestSuite) TestSwapAdapter_TotalSharesBySource_Empty() {
adapter := swap.NewSourceAdapter(suite.app.GetSwapKeeper())
tests := []struct {
name string
giveSourceID string
wantShares sdk.Dec
}{
{
"empty/invalid pools are zero",
"pool1",
sdk.ZeroDec(),
},
{
"invalid request returns zero",
"",
sdk.ZeroDec(),
},
}
for _, tt := range tests {
suite.Run(tt.name, func() {
shares := adapter.TotalSharesBySource(suite.ctx, tt.giveSourceID)
suite.Equal(tt.wantShares, shares)
})
}
}
func (suite *SwapAdapterTestSuite) TestSwapAdapter_TotalSharesBySource() {
poolDenomA := "ukava"
poolDenomB := "usdx"
swapKeeper := suite.app.GetSwapKeeper()
swapKeeper.SetParams(suite.ctx, swaptypes.NewParams(
swaptypes.NewAllowedPools(
swaptypes.NewAllowedPool(poolDenomA, poolDenomB),
),
sdk.ZeroDec(),
))
suite.app.FundAccount(
suite.ctx,
suite.addrs[0],
sdk.NewCoins(
sdk.NewCoin(poolDenomA, sdk.NewInt(1000000000000)),
sdk.NewCoin(poolDenomB, sdk.NewInt(1000000000000)),
),
)
suite.app.FundAccount(
suite.ctx,
suite.addrs[1],
sdk.NewCoins(
sdk.NewCoin(poolDenomA, sdk.NewInt(1000000000000)),
sdk.NewCoin(poolDenomB, sdk.NewInt(1000000000000)),
),
)
err := swapKeeper.Deposit(
suite.ctx,
suite.addrs[0],
sdk.NewCoin(poolDenomA, sdk.NewInt(100)),
sdk.NewCoin(poolDenomB, sdk.NewInt(100)),
sdk.NewDecWithPrec(1, 1),
)
suite.NoError(err)
err = swapKeeper.Deposit(
suite.ctx,
suite.addrs[1],
sdk.NewCoin(poolDenomA, sdk.NewInt(250)),
sdk.NewCoin(poolDenomB, sdk.NewInt(250)),
sdk.NewDecWithPrec(1, 0),
)
suite.NoError(err)
adapter := swap.NewSourceAdapter(suite.app.GetSwapKeeper())
tests := []struct {
name string
giveSourceID string
wantShares sdk.Dec
}{
{
"total shares",
swaptypes.PoolID(poolDenomA, poolDenomB),
sdk.NewDecWithPrec(350, 0),
},
{
"empty or invalid pool empty",
"pool2",
sdk.ZeroDec(),
},
}
for _, tt := range tests {
suite.Run(tt.name, func() {
shares := adapter.TotalSharesBySource(suite.ctx, tt.giveSourceID)
suite.Equal(tt.wantShares, shares)
})
}
}

View File

@ -1,12 +1,14 @@
package keeper package keeper
import ( import (
"fmt"
"time" "time"
"github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/store/prefix" "github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/incentive/keeper/adapters/swap"
"github.com/kava-labs/kava/x/incentive/types" "github.com/kava-labs/kava/x/incentive/types"
) )
@ -25,6 +27,8 @@ type Keeper struct {
liquidKeeper types.LiquidKeeper liquidKeeper types.LiquidKeeper
earnKeeper types.EarnKeeper earnKeeper types.EarnKeeper
adapters map[types.ClaimType]types.SourceAdapter
// Keepers used for APY queries // Keepers used for APY queries
mintKeeper types.MintKeeper mintKeeper types.MintKeeper
distrKeeper types.DistrKeeper distrKeeper types.DistrKeeper
@ -43,18 +47,23 @@ func NewKeeper(
} }
return Keeper{ return Keeper{
accountKeeper: ak, accountKeeper: ak,
cdc: cdc, cdc: cdc,
key: key, key: key,
paramSubspace: paramstore, paramSubspace: paramstore,
bankKeeper: bk, bankKeeper: bk,
cdpKeeper: cdpk, cdpKeeper: cdpk,
hardKeeper: hk, hardKeeper: hk,
stakingKeeper: stk, stakingKeeper: stk,
swapKeeper: swpk, swapKeeper: swpk,
savingsKeeper: svk, savingsKeeper: svk,
liquidKeeper: lqk, liquidKeeper: lqk,
earnKeeper: ek, earnKeeper: ek,
adapters: map[types.ClaimType]types.SourceAdapter{
types.CLAIM_TYPE_SWAP: swap.NewSourceAdapter(swpk),
},
mintKeeper: mk, mintKeeper: mk,
distrKeeper: dk, distrKeeper: dk,
pricefeedKeeper: pfk, pricefeedKeeper: pfk,
@ -885,6 +894,15 @@ func (k Keeper) IterateEarnRewardAccrualTimes(ctx sdk.Context, cb func(string, t
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// New deduplicated methods // New deduplicated methods
func (k Keeper) GetSourceAdapter(claimType types.ClaimType) types.SourceAdapter {
fetcher, found := k.adapters[claimType]
if !found {
panic(fmt.Sprintf("no source share fetcher for claim type %s", claimType))
}
return fetcher
}
// GetClaim returns the claim in the store corresponding the the owner and // GetClaim returns the claim in the store corresponding the the owner and
// claimType, and a boolean for if the claim was found // claimType, and a boolean for if the claim was found
func (k Keeper) GetClaim( func (k Keeper) GetClaim(

View File

@ -36,7 +36,7 @@ func (k Keeper) SynchronizeClaim(
claimType types.ClaimType, claimType types.ClaimType,
sourceID string, sourceID string,
owner sdk.AccAddress, owner sdk.AccAddress,
shares sdk.Int, shares sdk.Dec,
) { ) {
claim, found := k.GetClaim(ctx, claimType, owner) claim, found := k.GetClaim(ctx, claimType, owner)
if !found { if !found {
@ -53,7 +53,7 @@ func (k *Keeper) synchronizeClaim(
claim types.Claim, claim types.Claim,
sourceID string, sourceID string,
owner sdk.AccAddress, owner sdk.AccAddress,
shares sdk.Int, shares sdk.Dec,
) types.Claim { ) types.Claim {
globalRewardIndexes, found := k.GetRewardIndexesOfClaimType(ctx, claim.Type, sourceID) globalRewardIndexes, found := k.GetRewardIndexesOfClaimType(ctx, claim.Type, sourceID)
if !found { if !found {
@ -74,7 +74,7 @@ func (k *Keeper) synchronizeClaim(
userRewardIndexes = types.RewardIndexes{} userRewardIndexes = types.RewardIndexes{}
} }
newRewards, err := k.CalculateRewards(userRewardIndexes, globalRewardIndexes, shares.ToDec()) newRewards, err := k.CalculateRewards(userRewardIndexes, globalRewardIndexes, shares)
if err != nil { if err != nil {
// Global reward factors should never decrease, as it would lead to a negative update to claim.Rewards. // Global reward factors should never decrease, as it would lead to a negative update to claim.Rewards.
// This panics if a global reward factor decreases or disappears between the old and new indexes. // This panics if a global reward factor decreases or disappears between the old and new indexes.
@ -86,3 +86,33 @@ func (k *Keeper) synchronizeClaim(
return claim return claim
} }
// GetSynchronizedClaim fetches a claim from the store and syncs rewards for all
// rewarded sourceIDs.
func (k Keeper) GetSynchronizedClaim(
ctx sdk.Context,
claimType types.ClaimType,
owner sdk.AccAddress,
) (types.Claim, bool) {
claim, found := k.GetClaim(ctx, claimType, owner)
if !found {
return types.Claim{}, false
}
// Fetch all source IDs from indexes
var sourceIDs []string
k.IterateRewardIndexesByClaimType(ctx, claimType, func(rewardIndexes types.TypedRewardIndexes) bool {
sourceIDs = append(sourceIDs, rewardIndexes.CollateralType)
return false
})
adapter := k.GetSourceAdapter(claimType)
accShares := adapter.OwnerSharesBySource(ctx, owner, sourceIDs)
// Synchronize claim for each source ID
for _, sourceID := range sourceIDs {
claim = k.synchronizeClaim(ctx, claim, sourceID, owner, accShares[sourceID])
}
return claim, true
}

View File

@ -68,7 +68,7 @@ func (suite *SynchronizeClaimTests) TestClaimUpdatedWhenGlobalIndexesHaveIncreas
userShares := i(1e9) userShares := i(1e9)
suite.keeper.SynchronizeClaim(suite.ctx, claimType, collateralType, claim.Owner, userShares) suite.keeper.SynchronizeClaim(suite.ctx, claimType, collateralType, claim.Owner, userShares.ToDec())
syncedClaim, _ := suite.keeper.GetClaim(suite.ctx, claimType, claim.Owner) syncedClaim, _ := suite.keeper.GetClaim(suite.ctx, claimType, claim.Owner)
// indexes updated from global // indexes updated from global
@ -110,7 +110,7 @@ func (suite *SynchronizeClaimTests) TestClaimUnchangedWhenGlobalIndexesUnchanged
userShares := i(1e9) userShares := i(1e9)
suite.keeper.SynchronizeClaim(suite.ctx, claimType, collateralType, claim.Owner, userShares) suite.keeper.SynchronizeClaim(suite.ctx, claimType, collateralType, claim.Owner, userShares.ToDec())
syncedClaim, _ := suite.keeper.GetClaim(suite.ctx, claimType, claim.Owner) syncedClaim, _ := suite.keeper.GetClaim(suite.ctx, claimType, claim.Owner)
// claim should have the same rewards and indexes as before // claim should have the same rewards and indexes as before
@ -169,7 +169,7 @@ func (suite *SynchronizeClaimTests) TestClaimUpdatedWhenNewRewardAdded() {
userShares := i(1e9) userShares := i(1e9)
suite.keeper.SynchronizeClaim(suite.ctx, claimType, newlyRewardcollateralType, claim.Owner, userShares) suite.keeper.SynchronizeClaim(suite.ctx, claimType, newlyRewardcollateralType, claim.Owner, userShares.ToDec())
syncedClaim, _ := suite.keeper.GetClaim(suite.ctx, claimType, claim.Owner) syncedClaim, _ := suite.keeper.GetClaim(suite.ctx, claimType, claim.Owner)
// the new indexes should be added to the claim, but the old ones should be unchanged // the new indexes should be added to the claim, but the old ones should be unchanged
@ -203,7 +203,7 @@ func (suite *SynchronizeClaimTests) TestClaimUnchangedWhenNoReward() {
userShares := i(1e9) userShares := i(1e9)
suite.keeper.SynchronizeClaim(suite.ctx, claimType, collateralType, claim.Owner, userShares) suite.keeper.SynchronizeClaim(suite.ctx, claimType, collateralType, claim.Owner, userShares.ToDec())
syncedClaim, _ := suite.keeper.GetClaim(suite.ctx, claimType, claim.Owner) syncedClaim, _ := suite.keeper.GetClaim(suite.ctx, claimType, claim.Owner)
suite.Equal(claim, syncedClaim) suite.Equal(claim, syncedClaim)
@ -256,7 +256,7 @@ func (suite *SynchronizeClaimTests) TestClaimUpdatedWhenNewRewardDenomAdded() {
userShares := i(1e9) userShares := i(1e9)
suite.keeper.SynchronizeClaim(suite.ctx, claimType, collateralType, claim.Owner, userShares) suite.keeper.SynchronizeClaim(suite.ctx, claimType, collateralType, claim.Owner, userShares.ToDec())
syncedClaim, _ := suite.keeper.GetClaim(suite.ctx, claimType, claim.Owner) syncedClaim, _ := suite.keeper.GetClaim(suite.ctx, claimType, claim.Owner)
// indexes should have the new reward denom added // indexes should have the new reward denom added
@ -310,7 +310,7 @@ func (suite *SynchronizeClaimTests) TestClaimUpdatedWhenGlobalIndexesIncreasedAn
userShares := i(0) userShares := i(0)
suite.keeper.SynchronizeClaim(suite.ctx, claimType, collateralType, claim.Owner, userShares) suite.keeper.SynchronizeClaim(suite.ctx, claimType, collateralType, claim.Owner, userShares.ToDec())
syncedClaim, _ := suite.keeper.GetClaim(suite.ctx, claimType, claim.Owner) syncedClaim, _ := suite.keeper.GetClaim(suite.ctx, claimType, claim.Owner)
// indexes updated from global // indexes updated from global
@ -318,3 +318,155 @@ func (suite *SynchronizeClaimTests) TestClaimUpdatedWhenGlobalIndexesIncreasedAn
// reward is unchanged // reward is unchanged
suite.Equal(claim.Reward, syncedClaim.Reward) suite.Equal(claim.Reward, syncedClaim.Reward)
} }
func (suite *SynchronizeClaimTests) TestGetSyncedClaim_ClaimUnchangedWhenNoGlobalIndexes() {
collateralType_1 := "btcb:usdx"
owner := arbitraryAddress()
claimType := types.CLAIM_TYPE_SWAP
swapKeeper := newFakeSwapKeeper().
addDeposit(collateralType_1, owner, i(1e9))
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, nil, swapKeeper, nil, nil, nil)
claim := types.Claim{
Type: claimType,
Owner: owner,
Reward: nil,
RewardIndexes: types.MultiRewardIndexes{
{
CollateralType: collateralType_1,
RewardIndexes: nil, // this state only happens because Init stores empty indexes
},
},
}
suite.keeper.SetClaim(suite.ctx, claim)
// no global indexes for any pool
syncedClaim, f := suite.keeper.GetSynchronizedClaim(suite.ctx, claimType, claim.Owner)
suite.True(f)
// indexes are unchanged
suite.Equal(claim.RewardIndexes, syncedClaim.RewardIndexes)
// reward is unchanged
suite.Equal(claim.Reward, syncedClaim.Reward)
}
func (suite *SynchronizeClaimTests) TestGetSyncedClaim_ClaimUpdatedWhenMissingIndexAndHasNoSourceShares() {
collateralType_1 := "btcb:usdx"
collateralType_2 := "ukava:usdx"
owner := arbitraryAddress()
claimType := types.CLAIM_TYPE_SWAP
// owner has no shares in any pool
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, nil, newFakeSwapKeeper(), nil, nil, nil)
claim := types.Claim{
Type: claimType,
Owner: owner,
Reward: arbitraryCoins(),
RewardIndexes: types.MultiRewardIndexes{
{
CollateralType: collateralType_1,
RewardIndexes: types.RewardIndexes{
{
CollateralType: "rewarddenom1",
RewardFactor: d("1000.001"),
},
},
},
},
}
suite.keeper.SetClaim(suite.ctx, claim)
globalIndexes := types.MultiRewardIndexes{
{
CollateralType: collateralType_1,
RewardIndexes: types.RewardIndexes{
{
CollateralType: "rewarddenom1",
RewardFactor: d("2000.002"),
},
},
},
{
CollateralType: collateralType_2,
RewardIndexes: types.RewardIndexes{
{
CollateralType: "rewarddenom2",
RewardFactor: d("2000.002"),
},
},
},
}
suite.storeGlobalIndexes(claimType, globalIndexes)
syncedClaim, f := suite.keeper.GetSynchronizedClaim(suite.ctx, claimType, claim.Owner)
suite.True(f)
// indexes updated from global
suite.Equal(globalIndexes, syncedClaim.RewardIndexes)
// reward is unchanged
suite.Equal(claim.Reward, syncedClaim.Reward)
}
func (suite *SynchronizeClaimTests) TestGetSyncedClaim_ClaimUpdatedWhenMissingIndexButHasSourceShares() {
collateralType_1 := "btcb:usdx"
collateralType_2 := "ukava:usdx"
owner := arbitraryAddress()
claimType := types.CLAIM_TYPE_SWAP
swapKeeper := newFakeSwapKeeper().
addDeposit(collateralType_1, owner, i(1e9)).
addDeposit(collateralType_2, owner, i(1e9))
suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, nil, swapKeeper, nil, nil, nil)
claim := types.Claim{
Type: claimType,
Owner: owner,
Reward: arbitraryCoins(),
RewardIndexes: types.MultiRewardIndexes{
{
CollateralType: collateralType_1,
RewardIndexes: types.RewardIndexes{
{
CollateralType: "rewarddenom1",
RewardFactor: d("1000.001"),
},
},
},
},
}
suite.keeper.SetClaim(suite.ctx, claim)
globalIndexes := types.MultiRewardIndexes{
{
CollateralType: collateralType_1,
RewardIndexes: types.RewardIndexes{
{
CollateralType: "rewarddenom1",
RewardFactor: d("2000.002"),
},
},
},
{
CollateralType: collateralType_2,
RewardIndexes: types.RewardIndexes{
{
CollateralType: "rewarddenom2",
RewardFactor: d("2000.002"),
},
},
},
}
suite.storeGlobalIndexes(claimType, globalIndexes)
syncedClaim, f := suite.keeper.GetSynchronizedClaim(suite.ctx, claimType, claim.Owner)
suite.True(f)
// indexes updated from global
suite.Equal(globalIndexes, syncedClaim.RewardIndexes)
// reward is incremented
expectedReward := cs(c("rewarddenom1", 1_000_001_000_000), c("rewarddenom2", 2_000_002_000_000))
suite.Equal(claim.Reward.Add(expectedReward...), syncedClaim.Reward)
}