Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@

An '!' indicates a state machine breaking change.

## Unreleased

### Improvements

- ! [#266](https://github.com/KYVENetwork/chain/pull/266) Improve storage cost payout.

## [v2.1.0](https://github.com/KYVENetwork/chain/releases/tag/v2.1.0) - 2025-04-29

### Improvements
Expand Down
7 changes: 3 additions & 4 deletions proto/kyve/delegation/v1beta1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@ import "cosmos_proto/cosmos.proto";
option go_package = "github.com/KYVENetwork/chain/x/stakers/types_delegation_v1beta1";

/*
NOTICE:
This file needs to be kept for backwards compatibility with the governance module.
Otherwise, it is not possible to decode legacy delegation param update proposals.
NOTICE:
This file needs to be kept for backwards compatibility with the governance module.
Otherwise, it is not possible to decode legacy delegation param update proposals.
*/


// Msg defines the Msg service.
service Msg {
option (cosmos.msg.v1.service) = true;
Expand Down
26 changes: 22 additions & 4 deletions x/bundles/keeper/keeper_suite_inflation_splitting_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1512,12 +1512,21 @@ var _ = Describe("inflation splitting", Ordered, func() {
// for kyve coin (7410 - (7410 * 0.01) - _((100 * 0.5) / (3 * 1))_) * 0.1 + _((100 * 0.5) / (3 * 1))_
// for acoin (10_000 - (10_000 * 0.01) - _((100 * 0.5) / (3 * 1))_) * 0.1 + _((100 * 0.5) / (3 * 1))_
// for bcoin coins (20_000 - (20_000 * 0.01) - _((100 * 0.5) / (3 * 2))_) * 0.1 + _((100 * 0.5) / (3 * 2))_
Expect(s.App().StakersKeeper.GetOutstandingCommissionRewards(s.Ctx(), i.STAKER_0).String()).To(Equal(sdk.NewCoins(i.KYVECoin(748), i.ACoin(1004), i.BCoin(1987)).String()))

// STORAGE COST UPDATE: with $KYVE being used first for storage cost the kyve amount is higher for the uploader
// commission rewards and the amount of the other coins are lower because the uploader reward includes the storage
// cost (if kyve is used first the contribution of the remaining coins will be lower)
// VALUES BEFORE: 1004acoin,1987bcoin,748tkyve
Expect(s.App().StakersKeeper.GetOutstandingCommissionRewards(s.Ctx(), i.STAKER_0).String()).To(Equal(sdk.NewCoins(i.KYVECoin(778), i.ACoin(990), i.BCoin(1980)).String()))
// assert uploader self delegation rewards (here we round up since the result of delegation rewards is the remainder minus the truncated commission rewards)
// for kyve coin (7410 - (7410 * 0.01) - _((100 * 0.5) / (3 * 1))_) * (1 - 0.1)
// for acoin (10_000 - (10_000 * 0.01) - _((100 * 0.5) / (3 * 1))_) * (1 - 0.1)
// for bcoin (20_000 - (20_000 * 0.01) - _((100 * 0.5) / (3 * 2))_) * (1 - 0.1)
Expect(s.App().StakersKeeper.GetOutstandingRewards(s.Ctx(), i.STAKER_0, i.STAKER_0).String()).To(Equal(sdk.NewCoins(i.KYVECoin(6588), i.ACoin(8896), i.BCoin(17813)).String()))

// STORAGE COST UPDATE: with $KYVE being used first for storage cost the delegators receive less kyve and more
// of the remaining coins
// VALUES BEFORE: 8896acoin,17813bcoin,6588tkyve
Expect(s.App().StakersKeeper.GetOutstandingRewards(s.Ctx(), i.STAKER_0, i.STAKER_0).String()).To(Equal(sdk.NewCoins(i.KYVECoin(6558), i.ACoin(8910), i.BCoin(17820)).String()))

fundingState, _ := s.App().FundersKeeper.GetFundingState(s.Ctx(), 0)

Expand Down Expand Up @@ -1622,12 +1631,21 @@ var _ = Describe("inflation splitting", Ordered, func() {
// for kyve coin (24720 - (24720 * 0.01) - _((100 * 0.5) / (3 * 1))_) * 0.1 + _((100 * 0.5) / (3 * 1))_
// for acoin (10_000 - (10_000 * 0.01) - _((100 * 0.5) / (3 * 1))_) * 0.1 + _((100 * 0.5) / (3 * 1))_
// for bcoin coins (20_000 - (20_000 * 0.01) - _((100 * 0.5) / (3 * 2))_) * 0.1 + _((100 * 0.5) / (3 * 2))_
Expect(s.App().StakersKeeper.GetOutstandingCommissionRewards(s.Ctx(), i.STAKER_0).String()).To(Equal(sdk.NewCoins(i.KYVECoin(2461), i.ACoin(1004), i.BCoin(1987)).String()))

// STORAGE COST UPDATE: with $KYVE being used first for storage cost the kyve amount is higher for the uploader
// commission rewards and the amount of the other coins are lower because the uploader reward includes the storage
// cost (if kyve is used first the contribution of the remaining coins will be lower)
// VALUES BEFORE: 1004acoin,1987bcoin,2461tkyve
Expect(s.App().StakersKeeper.GetOutstandingCommissionRewards(s.Ctx(), i.STAKER_0).String()).To(Equal(sdk.NewCoins(i.KYVECoin(2492), i.ACoin(990), i.BCoin(1980)).String()))
// assert uploader self delegation rewards (here we round up since the result of delegation rewards is the remainder minus the truncated commission rewards)
// for kyve coin (24720 - (24720 * 0.01) - _((100 * 0.5) / (3 * 1))_) * (1 - 0.1)
// for acoin (10_000 - (10_000 * 0.01) - _((100 * 0.5) / (3 * 1))_) * (1 - 0.1)
// for bcoin (20_000 - (20_000 * 0.01) - _((100 * 0.5) / (3 * 2))_) * (1 - 0.1)
Expect(s.App().StakersKeeper.GetOutstandingRewards(s.Ctx(), i.STAKER_0, i.STAKER_0).String()).To(Equal(sdk.NewCoins(i.KYVECoin(22012), i.ACoin(8896), i.BCoin(17813)).String()))

// STORAGE COST UPDATE: with $KYVE being used first for storage cost the delegators receive less kyve and more
// of the remaining coins
// VALUES BEFORE: 8896acoin,17813bcoin,22012tkyve
Expect(s.App().StakersKeeper.GetOutstandingRewards(s.Ctx(), i.STAKER_0, i.STAKER_0).String()).To(Equal(sdk.NewCoins(i.KYVECoin(21981), i.ACoin(8910), i.BCoin(17820)).String()))

fundingState, _ := s.App().FundersKeeper.GetFundingState(s.Ctx(), 0)

Expand Down
13 changes: 11 additions & 2 deletions x/bundles/keeper/keeper_suite_valid_bundles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1957,10 +1957,19 @@ var _ = Describe("valid bundles", Ordered, func() {
// (amount_per_bundle - treasury_reward - storage_cost) * uploader_commission + storage_cost
// storage_cost = 1MB * storage_price / coin_length * coin_price
// (amount_per_bundle - (amount_per_bundle * 0.01) - _((1048576 * 0.000000006288 * 10**coin_decimals) / (4 * coin_weight))_) * 0.1 + _((1048576 * 0.000000006288) / (4 * coin_weight))_
Expect(s.App().StakersKeeper.GetOutstandingCommissionRewards(s.Ctx(), poolAccountUploader.Staker).String()).To(Equal(sdk.NewCoins(i.KYVECoin(125_973), i.ACoin(99_143), i.BCoin(116_661_015_771_428_571), i.CCoin(100_765)).String()))

// STORAGE COST UPDATE: with $KYVE being used first for storage cost the kyve amount is higher for the uploader
// commission rewards and the amount of the other coins are lower because the uploader reward includes the storage
// cost (if kyve is used first the contribution of the remaining coins will be lower)
// VALUES BEFORE: 99_143acoin,116_661_015_771_428_571bcoin,100_765ccoin,125_973tkyve
Expect(s.App().StakersKeeper.GetOutstandingCommissionRewards(s.Ctx(), poolAccountUploader.Staker).String()).To(Equal(sdk.NewCoins(i.KYVECoin(206_892), i.ACoin(99_000), i.BCoin(99_000_163_885_714_285), i.CCoin(99_000)).String()))
// assert uploader self delegation rewards (here we round up since the result of delegation rewards is the remainder minus the truncated commission rewards)
// (amount_per_bundle - (amount_per_bundle * 0.01) - _((29970208 * 0.000000006288 * 1**coin_decimals) / (4 * coin_weight))_) * (1 - 0.1)
Expect(s.App().StakersKeeper.GetOutstandingRewards(s.Ctx(), i.STAKER_0, i.STAKER_0).String()).To(Equal(sdk.NewCoins(i.KYVECoin(864_027), i.ACoin(890_857), i.BCoin(873_338_984_228_571_429), i.CCoin(889_235)).String()))

// STORAGE COST UPDATE: with $KYVE being used first for storage cost the delegators receive less kyve and more
// of the remaining coins
// VALUES BEFORE: 890_857acoin,873_338_984_228_571_429bcoin,889_235ccoin,864_027tkyve
Expect(s.App().StakersKeeper.GetOutstandingRewards(s.Ctx(), i.STAKER_0, i.STAKER_0).String()).To(Equal(sdk.NewCoins(i.KYVECoin(783_108), i.ACoin(891_000), i.BCoin(890_999_836_114_285_715), i.CCoin(891_000)).String()))

fundingState, _ := s.App().FundersKeeper.GetFundingState(s.Ctx(), 0)

Expand Down
89 changes: 62 additions & 27 deletions x/bundles/keeper/logic_bundles.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,45 +257,80 @@ func (k Keeper) calculatePayouts(ctx sdk.Context, poolId uint64, totalPayout sdk
return
}

// subtract storage cost from remaining total payout. We split the storage cost between all coins and charge
// the amount per coin, the idea is that every coin should contribute the same USD value to the total storage
// reward. This is done by defining the storage cost as USD / byte and the coin weights as USD / coin denom.
// subtract storage cost from remaining total payout. We first try to cover the storage cost with the
// native $KYVE coin, if that is not enough the remaining USD value is split equally between the remaining
// coins.
//
// If there is not enough of a coin available to cover the storage reward per coin we simply charge what is left,
// so there can be the case that the storageRewards are less than what we actually wanted to pay out. This is
// acceptable because this case is very rare, usually the minFundingAmount ensures that there are always enough
// funds left of each coin, and in the case there are not enough the coins are removed and therefore for the
// next bundle we split between the other remaining coins.
whitelist := k.fundersKeeper.GetCoinWhitelistMap(ctx)
// wantedStorageRewards are the amounts based on the current storage cost we want to pay out, this can be more
// than we have available in totalPayout
wantedStorageRewards := sdk.NewCoins()
// storageCostPerCoin is the storage cost in $USD for each coin. This implies that each coin contributes the same
// amount of value to the storage rewards
storageCostPerCoin := k.GetStorageCost(ctx, bundleProposal.StorageProviderId).MulInt64(int64(bundleProposal.DataSize)).QuoInt64(int64(totalPayout.Len()))
for _, coin := range totalPayout {
weight := whitelist[coin.Denom].CoinWeight
if weight.IsZero() {
continue
storageCost := k.GetStorageCost(ctx, bundleProposal.StorageProviderId).MulInt64(int64(bundleProposal.DataSize))

kyveWeight := whitelist[globalTypes.Denom].CoinWeight
kyveCurrencyUnit := math.LegacyNewDec(10).Power(uint64(whitelist[globalTypes.Denom].CoinDecimals))

if found, _ := totalPayout.Find(globalTypes.Denom); found && !kyveWeight.IsZero() {
kyveAmount := sdk.NewCoins(sdk.NewCoin(globalTypes.Denom, storageCost.Mul(kyveCurrencyUnit).Quo(kyveWeight).TruncateInt()))
bundleReward.UploaderStorageCost = totalPayout.Min(kyveAmount)
totalPayout = totalPayout.Sub(bundleReward.UploaderStorageCost...)
if totalPayout.IsZero() {
return
}

// currencyUnit is the amount of base denoms of the currency
currencyUnit := math.LegacyNewDec(10).Power(uint64(whitelist[coin.Denom].CoinDecimals))
// amount is the value of storageCostPerCoin in the base denomination of the currency. We calculate this
// by multiplying first with the amount of base denoms of the currency and then divide this by the $USD
// value per currency unit which is the weight.
amount := storageCostPerCoin.Mul(currencyUnit).Quo(weight).TruncateInt()
wantedStorageRewards = wantedStorageRewards.Add(sdk.NewCoin(coin.Denom, amount))
// calculate how much of the storage reward was initially paid with native
// kyve and give the remainder to the other coins
storageCostPaid := bundleReward.UploaderStorageCost.AmountOf(globalTypes.Denom)
storageCostPaidUsd := math.LegacyNewDec(storageCostPaid.Int64()).Mul(kyveWeight).Quo(kyveCurrencyUnit)

if storageCost.GTE(storageCostPaidUsd) {
storageCost = storageCost.Sub(storageCostPaidUsd)
} else {
storageCost = math.LegacyZeroDec()
}
Comment thread
troykessler marked this conversation as resolved.
}

// we take the min here since there can be the case where we want to charge more coins for the storage
// reward than we have left in the total payout
bundleReward.UploaderStorageCost = totalPayout.Min(wantedStorageRewards)
kyveCoinFound, kyveCoin := totalPayout.Find(globalTypes.Denom)
remainingCoins := totalPayout
if kyveCoinFound {
remainingCoins = totalPayout.Sub(kyveCoin)
}

if !storageCost.IsZero() && int64(remainingCoins.Len()) > 0 {
// wantedStorageRewards are the amounts based on the current storage cost we want to pay out, this can be more
// than we have available in totalPayout
wantedStorageRewards := sdk.NewCoins()
// storageCostPerCoin is the storage cost in $USD for each coin. This implies that each coin contributes the same
// amount of value to the storage rewards
storageCostPerCoin := storageCost.QuoInt64(int64(remainingCoins.Len()))
for _, coin := range remainingCoins {
weight := whitelist[coin.Denom].CoinWeight
// skip the native kyve denom since we already subtracted it above
if coin.Denom == globalTypes.Denom || weight.IsZero() {
continue
}

// the remaining total payout is split between the uploader and his delegators.
totalPayout = totalPayout.Sub(bundleReward.UploaderStorageCost...)
if totalPayout.IsZero() {
return
// currencyUnit is the amount of base denoms of the currency
currencyUnit := math.LegacyNewDec(10).Power(uint64(whitelist[coin.Denom].CoinDecimals))
// amount is the value of storageCostPerCoin in the base denomination of the currency. We calculate this
// by multiplying first with the amount of base denoms of the currency and then divide this by the $USD
// value per currency unit which is the weight.
amount := storageCostPerCoin.Mul(currencyUnit).Quo(weight).TruncateInt()
wantedStorageRewards = wantedStorageRewards.Add(sdk.NewCoin(coin.Denom, amount))
}

// we take the min here since there can be the case where we want to charge more coins for the storage
// reward than we have left in the total payout
multiCoinStorageCostReward := totalPayout.Min(wantedStorageRewards)
bundleReward.UploaderStorageCost = bundleReward.UploaderStorageCost.Add(multiCoinStorageCostReward...)

// the remaining total payout is split between the uploader and his delegators.
totalPayout = totalPayout.Sub(multiCoinStorageCostReward...)
if totalPayout.IsZero() {
return
}
}

commission := k.stakerKeeper.GetValidatorPoolCommission(ctx, bundleProposal.Uploader, poolId)
Expand Down