Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(incentives): added fees for adding to gauge and gauge creation #1188

Merged
merged 9 commits into from
Sep 16, 2024
12 changes: 6 additions & 6 deletions app/upgrades/v3/upgrade_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
cometbftproto "github.com/cometbft/cometbft/proto/tendermint/types"
sdk "github.com/cosmos/cosmos-sdk/types"
upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types"
incentivestypes "github.com/dymensionxyz/dymension/v3/x/incentives/types"
"github.com/stretchr/testify/suite"

"github.com/dymensionxyz/dymension/v3/app"
Expand Down Expand Up @@ -39,9 +38,9 @@ var (
DYM = sdk.NewIntFromBigInt(new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil))

// CreateGaugeFee is the fee required to create a new gauge.
expectCreateGaugeFee = DYM.Mul(sdk.NewInt(10))
// expectCreateGaugeFee = DYM.Mul(sdk.NewInt(10))
// AddToGaugeFee is the fee required to add to gauge.
expectAddToGaugeFee = sdk.ZeroInt()
// expectAddToGaugeFee = sdk.ZeroInt()

expectDelayedackEpochIdentifier = "hour"
expectDelayedackBridgingFee = sdk.NewDecWithPrec(1, 3)
Expand Down Expand Up @@ -102,10 +101,11 @@ func (s *UpgradeTestSuite) TestUpgrade() {
return fmt.Errorf("sequencer parameters not set correctly")
}

// These fields are deleted in the next update
// Check Incentives parameters
if !incentivestypes.CreateGaugeFee.Equal(expectCreateGaugeFee) || !incentivestypes.AddToGaugeFee.Equal(expectAddToGaugeFee) {
return fmt.Errorf("incentives parameters not set correctly")
}
//if !incentivestypes.CreateGaugeFee.Equal(expectCreateGaugeFee) || !incentivestypes.AddToGaugeFee.Equal(expectAddToGaugeFee) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not a fan of commented code! :D

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's an old state migration (v3 while current is v4), and i didn't want to modify old migration scripts, so i intentionally left this code commented here to have a historical reference. added an explanation comment for that.

// return fmt.Errorf("incentives parameters not set correctly")
//}

return nil
},
Expand Down
11 changes: 11 additions & 0 deletions app/upgrades/v4/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import (
"github.com/dymensionxyz/dymension/v3/app/upgrades"
delayedackkeeper "github.com/dymensionxyz/dymension/v3/x/delayedack/keeper"
delayedacktypes "github.com/dymensionxyz/dymension/v3/x/delayedack/types"
incentiveskeeper "github.com/dymensionxyz/dymension/v3/x/incentives/keeper"
incentivestypes "github.com/dymensionxyz/dymension/v3/x/incentives/types"
lightclientkeeper "github.com/dymensionxyz/dymension/v3/x/lightclient/keeper"
rollappkeeper "github.com/dymensionxyz/dymension/v3/x/rollapp/keeper"
rollapptypes "github.com/dymensionxyz/dymension/v3/x/rollapp/types"
Expand Down Expand Up @@ -56,6 +58,7 @@ func CreateUpgradeHandler(

migrateSequencers(ctx, keepers.SequencerKeeper)
migrateRollappLightClients(ctx, keepers.RollappKeeper, keepers.LightClientKeeper, keepers.IBCKeeper.ChannelKeeper)
migrateIncentivesParams(ctx, keepers.IncentivesKeeper)

// TODO: create rollapp gauges for each existing rollapp (https://github.com/dymensionxyz/dymension/issues/1005)

Expand Down Expand Up @@ -173,6 +176,14 @@ func migrateRollappLightClients(ctx sdk.Context, rollappkeeper *rollappkeeper.Ke
}
}

func migrateIncentivesParams(ctx sdk.Context, ik *incentiveskeeper.Keeper) {
params := ik.GetParams(ctx)
defaultParams := incentivestypes.DefaultParams()
params.CreateGaugeFee = defaultParams.CreateGaugeFee
params.AddToGaugeFee = defaultParams.AddToGaugeFee
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dont we include the defaults for base_gas_fee_for_create_gauge and base_gas_fee_for_add_reward_to_gauge ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed it, thanks for the catch!

ik.SetParams(ctx, params)
}

func ConvertOldRollappToNew(oldRollapp rollapptypes.Rollapp) rollapptypes.Rollapp {
return rollapptypes.Rollapp{
RollappId: oldRollapp.RollappId,
Expand Down
17 changes: 17 additions & 0 deletions proto/dymensionxyz/dymension/incentives/params.proto
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,21 @@ message Params {
// (day, week, etc.)
string distr_epoch_identifier = 1
[ (gogoproto.moretags) = "yaml:\"distr_epoch_identifier\"" ];
// CreateGaugeFee is the fee required to create a new gauge.
string create_gauge_fee = 2 [
(gogoproto.nullable) = false,
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int"
];
// AddToGaugeFee is the fee required to add to gauge.
string add_to_gauge_fee = 3 [
(gogoproto.nullable) = false,
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int"
];
// BaseGasFeeForCreateGauge is the gas fee for creating a gauge.
// This gas is charged for each denom while creating a gauge.
uint64 base_gas_fee_for_create_gauge = 4;
// BaseGasFeeForAddRewardToGauge is the gas fee for adding reward to gauges.
// This gas is charged for each denom while adding to a gauge plus for
// each denom the gauge already holds.
uint64 base_gas_fee_for_add_reward_to_gauge = 5;
}
13 changes: 13 additions & 0 deletions x/incentives/keeper/gauge.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,12 @@ func (k Keeper) CreateGauge(ctx sdk.Context, isPerpetual bool, owner sdk.AccAddr
NumEpochsPaidOver: numEpochsPaidOver,
}

// Fixed gas consumption create gauge based on the number of coins to add
baseGasFee := k.GetParams(ctx).BaseGasFeeForCreateGauge
denoms := uint64(len(gauge.Coins))
// Both baseGasFee and denoms are relatively small, so their multiplication shouldn't lead to overflow in practice
ctx.GasMeter().ConsumeGas(baseGasFee*denoms, "scaling gas cost for creating gauge rewards")

if err := k.bk.SendCoinsFromAccountToModule(ctx, owner, types.ModuleName, gauge.Coins); err != nil {
return 0, err
}
Expand Down Expand Up @@ -149,6 +155,13 @@ func (k Keeper) AddToGaugeRewards(ctx sdk.Context, owner sdk.AccAddress, coins s
if gauge.IsFinishedGauge(ctx.BlockTime()) {
return types.UnexpectedFinishedGaugeError{GaugeId: gaugeID}
}

// Fixed gas consumption adding reward to gauges based on the number of coins to add
baseGasFee := k.GetParams(ctx).BaseGasFeeForAddRewardToGauge
denoms := uint64(len(coins) + len(gauge.Coins))
// Both baseGasFee and denoms are relatively small, so their multiplication shouldn't lead to overflow in practice
ctx.GasMeter().ConsumeGas(baseGasFee*denoms, "scaling gas cost for adding to gauge rewards")

if err := k.bk.SendCoinsFromAccountToModule(ctx, owner, types.ModuleName, coins); err != nil {
return err
}
Expand Down
196 changes: 196 additions & 0 deletions x/incentives/keeper/gauge_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package keeper_test

import (
"fmt"
"time"

"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/suite"
"pgregory.net/rapid"

"github.com/dymensionxyz/dymension/v3/app/apptesting"
"github.com/dymensionxyz/dymension/v3/x/incentives/types"
Expand Down Expand Up @@ -321,3 +324,196 @@ func (suite *KeeperTestSuite) TestChargeFeeIfSufficientFeeDenomBalance() {
})
}
}

func (suite *KeeperTestSuite) TestAddToGaugeRewards() {
params := suite.App.IncentivesKeeper.GetParams(suite.Ctx)
addr := apptesting.CreateRandomAccounts(1)[0]

testCases := []struct {
name string
owner sdk.AccAddress
coinsToAdd sdk.Coins
gaugeCoins sdk.Coins
gaugeId uint64
minimumGasConsumed uint64

expectErr bool
}{
{
name: "valid case: valid gauge",
owner: addr,
coinsToAdd: sdk.NewCoins(
sdk.NewCoin("uosmo", sdk.NewInt(100000)),
sdk.NewCoin("atom", sdk.NewInt(99999)),
),
gaugeCoins: sdk.Coins{
sdk.NewInt64Coin("stake1", 12),
},
gaugeId: 1,
minimumGasConsumed: 3 * params.BaseGasFeeForAddRewardToGauge,
expectErr: false,
},
{
name: "valid case: valid gauge with >4 denoms to add",
owner: addr,
coinsToAdd: sdk.NewCoins(
sdk.NewCoin("uosmo", sdk.NewInt(100000)),
sdk.NewCoin("atom", sdk.NewInt(99999)),
sdk.NewCoin("mars", sdk.NewInt(88888)),
sdk.NewCoin("akash", sdk.NewInt(77777)),
sdk.NewCoin("eth", sdk.NewInt(6666)),
sdk.NewCoin("usdc", sdk.NewInt(555)),
sdk.NewCoin("dai", sdk.NewInt(4444)),
sdk.NewCoin("ust", sdk.NewInt(3333)),
),
gaugeCoins: sdk.Coins{
sdk.NewInt64Coin("stake1", 12),
},
gaugeId: 1,
minimumGasConsumed: 9 * params.BaseGasFeeForAddRewardToGauge,
expectErr: false,
},
{
name: "valid case: valid gauge with >4 initial denoms",
owner: addr,
coinsToAdd: sdk.NewCoins(
sdk.NewCoin("uosmo", sdk.NewInt(100000)),
sdk.NewCoin("atom", sdk.NewInt(99999)),
sdk.NewCoin("mars", sdk.NewInt(88888)),
sdk.NewCoin("akash", sdk.NewInt(77777)),
sdk.NewCoin("eth", sdk.NewInt(6666)),
sdk.NewCoin("usdc", sdk.NewInt(555)),
sdk.NewCoin("dai", sdk.NewInt(4444)),
sdk.NewCoin("ust", sdk.NewInt(3333)),
),
gaugeCoins: sdk.Coins{
sdk.NewCoin("uosmo", sdk.NewInt(100000)),
sdk.NewCoin("atom", sdk.NewInt(99999)),
sdk.NewCoin("mars", sdk.NewInt(88888)),
sdk.NewCoin("akash", sdk.NewInt(77777)),
sdk.NewCoin("eth", sdk.NewInt(6666)),
sdk.NewCoin("usdc", sdk.NewInt(555)),
sdk.NewCoin("dai", sdk.NewInt(4444)),
sdk.NewCoin("ust", sdk.NewInt(3333)),
},
gaugeId: 1,
minimumGasConsumed: 16 * params.BaseGasFeeForAddRewardToGauge,
expectErr: false,
},
{
name: "invalid case: gauge Id is not valid",
owner: addr,
coinsToAdd: sdk.NewCoins(
sdk.NewCoin("uosmo", sdk.NewInt(100000)),
sdk.NewCoin("atom", sdk.NewInt(99999)),
),
gaugeCoins: sdk.Coins{
sdk.NewInt64Coin("stake1", 12),
sdk.NewInt64Coin("stake2", 12),
sdk.NewInt64Coin("stake3", 12),
},
gaugeId: 0,
minimumGasConsumed: uint64(0),
expectErr: true,
},
}

for _, tc := range testCases {
suite.Run(tc.name, func() {
suite.SetupTest()
_, _, existingGaugeCoins, _ := suite.SetupNewGauge(true, sdk.NewCoins(tc.gaugeCoins...))

suite.FundAcc(tc.owner, tc.coinsToAdd)

existingGasConsumed := suite.Ctx.GasMeter().GasConsumed()

err := suite.App.IncentivesKeeper.AddToGaugeRewards(suite.Ctx, tc.owner, tc.coinsToAdd, tc.gaugeId)
if tc.expectErr {
suite.Require().Error(err)

// balance shouldn't change in the module
balance := suite.App.BankKeeper.GetAllBalances(suite.Ctx, suite.App.AccountKeeper.GetModuleAddress(types.ModuleName))
suite.Require().Equal(existingGaugeCoins, balance)

} else {
suite.Require().NoError(err)

// Ensure that at least the minimum amount of gas was charged (based on number of additional gauge coins)
gasConsumed := suite.Ctx.GasMeter().GasConsumed() - existingGasConsumed
fmt.Println(gasConsumed, tc.minimumGasConsumed)
suite.Require().True(gasConsumed >= tc.minimumGasConsumed)

// existing coins gets added to the module when we create gauge and add to gauge
expectedCoins := existingGaugeCoins.Add(tc.coinsToAdd...)

// check module account balance, should go up
balance := suite.App.BankKeeper.GetAllBalances(suite.Ctx, suite.App.AccountKeeper.GetModuleAddress(types.ModuleName))
suite.Require().Equal(expectedCoins, balance)

// check gauge coins should go up
gauge, err := suite.App.IncentivesKeeper.GetGaugeByID(suite.Ctx, tc.gaugeId)
suite.Require().NoError(err)

suite.Require().Equal(expectedCoins, gauge.Coins)
}
})
}
}

func (s *KeeperTestSuite) TestRapidTestAddToGaugeRewards() {
rapid.Check(s.T(), func(t *rapid.T) {
// Generate random data
existingDenoms := make(map[string]struct{})
gcGen := rapid.Custom[sdk.Coin](func(t *rapid.T) sdk.Coin {
return sdk.Coin{
Denom: rapid.StringOfN(rapid.RuneFrom([]rune{'a', 'b', 'c'}), 5, 100, -1).
Filter(func(s string) bool {
_, ok := existingDenoms[s]
existingDenoms[s] = struct{}{}
return !ok
}).
Draw(t, "denom"),
Amount: math.NewInt(rapid.Int64Range(1, 100_000).Draw(t, "coins")),
}
})
gaugeCoins := sdk.NewCoins(rapid.SliceOfN[sdk.Coin](gcGen, 1, 100_000).Draw(t, "gaugeCoins")...)
coinsToAdd := sdk.NewCoins(rapid.SliceOfN[sdk.Coin](gcGen, 1, 100_000).Draw(t, "coinsToAdd")...)

s.SetupTest()

// Create a new gauge
_, _, existingGaugeCoins, _ := s.SetupNewGauge(true, gaugeCoins)
owner := apptesting.CreateRandomAccounts(1)[0]
// Fund the owner account
s.FundAcc(owner, coinsToAdd)

// Save the gas meter before the method call
existingGasConsumed := s.Ctx.GasMeter().GasConsumed()

// AddToGaugeRewards
err := s.App.IncentivesKeeper.AddToGaugeRewards(s.Ctx, owner, coinsToAdd, 1)
s.Require().NoError(err)

// Min expected gas consumed
baseGasFee := s.App.IncentivesKeeper.GetParams(s.Ctx).BaseGasFeeForAddRewardToGauge
minimumGasConsumed := baseGasFee * uint64(len(gaugeCoins)+len(coinsToAdd))

// Ensure that at least the minimum amount of gas was charged (based on number of additional gauge coins)
gasConsumed := s.Ctx.GasMeter().GasConsumed() - existingGasConsumed
fmt.Println(gasConsumed, minimumGasConsumed)
s.Require().True(gasConsumed >= minimumGasConsumed)

// Existing coins gets added to the module when we create gauge and add to gauge
expectedCoins := existingGaugeCoins.Add(coinsToAdd...)

// Check module account balance, should go up
balance := s.App.BankKeeper.GetAllBalances(s.Ctx, s.App.AccountKeeper.GetModuleAddress(types.ModuleName))
s.Require().Equal(expectedCoins, balance)

// Check gauge coins should go up
gauge, err := s.App.IncentivesKeeper.GetGaugeByID(s.Ctx, 1)
s.Require().NoError(err)

s.Require().Equal(expectedCoins, gauge.Coins)
})
}
6 changes: 5 additions & 1 deletion x/incentives/keeper/genesis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,11 @@ func TestIncentivesInitGenesis(t *testing.T) {
// initialize genesis with specified parameter, the gauge created earlier, and lockable durations
app.IncentivesKeeper.InitGenesis(ctx, types.GenesisState{
Params: types.Params{
DistrEpochIdentifier: "week",
DistrEpochIdentifier: "week",
CreateGaugeFee: sdk.ZeroInt(),
AddToGaugeFee: sdk.ZeroInt(),
BaseGasFeeForCreateGauge: 0,
BaseGasFeeForAddRewardToGauge: 0,
},
Gauges: []types.Gauge{gauge},
LockableDurations: []time.Duration{
Expand Down
13 changes: 7 additions & 6 deletions x/incentives/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@ package keeper
import (
"context"

"github.com/dymensionxyz/dymension/v3/x/incentives/types"
"github.com/osmosis-labs/osmosis/v15/osmoutils"

errorsmod "cosmossdk.io/errors"

sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/osmosis-labs/osmosis/v15/osmoutils"

"github.com/dymensionxyz/dymension/v3/x/incentives/types"
)

// msgServer provides a way to reference keeper pointer in the message server interface.
Expand All @@ -35,7 +34,8 @@ func (server msgServer) CreateGauge(goCtx context.Context, msg *types.MsgCreateG
return nil, err
}

if err := server.keeper.chargeFeeIfSufficientFeeDenomBalance(ctx, owner, types.CreateGaugeFee, msg.Coins); err != nil {
createGaugeFee := server.keeper.GetParams(ctx).CreateGaugeFee
if err := server.keeper.chargeFeeIfSufficientFeeDenomBalance(ctx, owner, createGaugeFee, msg.Coins); err != nil {
return nil, err
}

Expand Down Expand Up @@ -63,7 +63,8 @@ func (server msgServer) AddToGauge(goCtx context.Context, msg *types.MsgAddToGau
return nil, err
}

if err := server.keeper.chargeFeeIfSufficientFeeDenomBalance(ctx, owner, types.AddToGaugeFee, msg.Rewards); err != nil {
addToGaugeFee := server.keeper.GetParams(ctx).AddToGaugeFee
if err := server.keeper.chargeFeeIfSufficientFeeDenomBalance(ctx, owner, addToGaugeFee, msg.Rewards); err != nil {
return nil, err
}
err = server.keeper.AddToGaugeRewards(ctx, owner, msg.Rewards, msg.GaugeId)
Expand Down
Loading
Loading