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: IPRPC over IBC: Part 5 - CNS-967: Cover pending IBC IPRPC fund costs #1470

Merged
Merged
9 changes: 9 additions & 0 deletions proto/lavanet/lava/rewards/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ option go_package = "github.com/lavanet/lava/x/rewards/types";
service Msg {
rpc SetIprpcData(MsgSetIprpcData) returns (MsgSetIprpcDataResponse);
rpc FundIprpc(MsgFundIprpc) returns (MsgFundIprpcResponse);
rpc CoverIbcIprpcFundCost(MsgCoverIbcIprpcFundCost) returns (MsgCoverIbcIprpcFundCostResponse);
// this line is used by starport scaffolding # proto/tx/rpc
}

Expand All @@ -37,4 +38,12 @@ message MsgFundIprpc {
message MsgFundIprpcResponse {
}

message MsgCoverIbcIprpcFundCost {
string creator = 1;
uint64 index = 2; // PendingIbcIprpcFund index
oren-lava marked this conversation as resolved.
Show resolved Hide resolved
}

message MsgCoverIbcIprpcFundCostResponse {
}

// this line is used by starport scaffolding # proto/tx/message
5 changes: 5 additions & 0 deletions testutil/common/tester.go
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,11 @@ func (ts *Tester) TxRewardsFundIprpc(creator string, spec string, duration uint6
return ts.Servers.RewardsServer.FundIprpc(ts.GoCtx, msg)
}

func (ts *Tester) TxRewardsCoverIbcIprpcFundCost(creator string, index uint64) (*rewardstypes.MsgCoverIbcIprpcFundCostResponse, error) {
msg := rewardstypes.NewMsgCoverIbcIprpcFundCost(creator, index)
return ts.Servers.RewardsServer.CoverIbcIprpcFundCost(ts.GoCtx, msg)
}

// TxCreateValidator: implement 'tx staking createvalidator' and bond its tokens
func (ts *Tester) TxCreateValidator(validator sigs.Account, amount math.Int) {
consensusPowerTokens := ts.Keepers.StakingKeeper.TokensFromConsensusPower(ts.Ctx, 1)
Expand Down
1 change: 1 addition & 0 deletions x/rewards/client/cli/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ func GetTxCmd() *cobra.Command {
}

cmd.AddCommand(CmdFundIprpc())
cmd.AddCommand(CmdCoverIbcIprpcFundCost())
// this line is used by starport scaffolding # 1

return cmd
Expand Down
48 changes: 48 additions & 0 deletions x/rewards/client/cli/tx_cover_ibc_iprpc_fund_cost.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package cli

import (
"strconv"

"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/client/tx"
"github.com/lavanet/lava/x/rewards/types"
"github.com/spf13/cobra"
)

var _ = strconv.Itoa(0)

func CmdCoverIbcIprpcFundCost() *cobra.Command {
cmd := &cobra.Command{
Use: "cover-ibc-iprpc-fund-cost [index] --from <creator>",
Short: `Apply a pending IBC IPRPC fund by paying its mandatory minimum cost of funding the IPRPC pool. Find your desired
fund's index and cost by using the query pending-ibc-iprpc-funds. By sending this message, the full cost of the fund will be
paid automatically. Then, the pending fund's coins will be sent to the IPRPC pool.`,
Example: `lavad tx rewards cover-ibc-iprpc-fund-cost 4 --from alice`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) (err error) {
clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}

index, err := strconv.ParseUint(args[0], 10, 64)
if err != nil {
return err
}

msg := types.NewMsgCoverIbcIprpcFundCost(
clientCtx.GetFromAddress().String(),
index,
)
if err := msg.ValidateBasic(); err != nil {
return err
}
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
},
}

flags.AddTxFlagsToCmd(cmd)
cmd.MarkFlagRequired(flags.FlagFrom)
return cmd
}
59 changes: 59 additions & 0 deletions x/rewards/keeper/ibc_iprpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"strconv"

"cosmossdk.io/math"
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
transfertypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types"
Expand Down Expand Up @@ -289,3 +290,61 @@ func (k Keeper) CalcPendingIbcIprpcFundMinCost(ctx sdk.Context, pendingIbcIprpcF
func (k Keeper) CalcPendingIbcIprpcFundExpiration(ctx sdk.Context) uint64 {
return uint64(ctx.BlockTime().Add(k.IbcIprpcExpiration(ctx)).UTC().Unix())
}

// CoverIbcIprpcFundCost covers the cost of a PendingIbcIprpcFund by sending the min cost funds to the IBC IPRPC receiver
// address and call FundIprpc(). Finally, it removes the PendingIbcIprpcFund object from the store
func (k Keeper) CoverIbcIprpcFundCost(ctx sdk.Context, creator string, index uint64) (costCovered sdk.Coin, err error) {
zeroCoin := sdk.NewCoin(k.stakingKeeper.BondDenom(ctx), math.ZeroInt())
creatorAddr, err := sdk.AccAddressFromBech32(creator)
if err != nil {
return zeroCoin, utils.LavaFormatWarning("invalid creator address. not Bech32", types.ErrCoverIbcIprpcFundCostFailed,
utils.LogAttr("creator", creator),
utils.LogAttr("index", index),
)
}

// get the PendingIbcIprpcFund with index
piif, found := k.GetPendingIbcIprpcFund(ctx, index)
if !found {
return zeroCoin, utils.LavaFormatWarning("PendingIbcIprpcFund not found", types.ErrCoverIbcIprpcFundCostFailed,
utils.LogAttr("creator", creator),
utils.LogAttr("index", index),
)
}

// sanity check: PendingIbcIprpcFund is not expired
if piif.IsExpired(ctx) {
oren-lava marked this conversation as resolved.
Show resolved Hide resolved
k.RemovePendingIbcIprpcFund(ctx, index)
return zeroCoin, utils.LavaFormatWarning("PendingIbcIprpcFund with index is expired (deleted fund)", types.ErrCoverIbcIprpcFundCostFailed,
utils.LogAttr("creator", creator),
utils.LogAttr("index", index),
)
}

// send the min cost to the ValidatorsRewardsAllocationPoolName (gov module doesn't pay min cost)
cost := zeroCoin
if creator != k.authority {
cost = k.CalcPendingIbcIprpcFundMinCost(ctx, piif)
err = k.bankKeeper.SendCoinsFromAccountToModule(ctx, creatorAddr, string(types.ValidatorsRewardsAllocationPoolName), sdk.NewCoins(cost))
if err != nil {
return zeroCoin, utils.LavaFormatWarning(types.ErrCoverIbcIprpcFundCostFailed.Error(), err,
utils.LogAttr("creator", creator),
utils.LogAttr("index", index),
)
}
}

// fund the iprpc pool with funds of IbcIprpcReceiver (inside, the IbcIprpcReceiver and the gov module are not paying the min cost)
err = k.FundIprpc(ctx, types.IbcIprpcReceiver, piif.Duration, sdk.NewCoins(piif.Fund), piif.Spec)
if err != nil {
return zeroCoin, utils.LavaFormatWarning(types.ErrCoverIbcIprpcFundCostFailed.Error(), err,
utils.LogAttr("creator", creator),
utils.LogAttr("index", index),
)
}

// remove the PendingIbcIprpcFund
k.RemovePendingIbcIprpcFund(ctx, index)

return cost, nil
}
98 changes: 98 additions & 0 deletions x/rewards/keeper/ibc_iprpc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
commontypes "github.com/lavanet/lava/common/types"
keepertest "github.com/lavanet/lava/testutil/keeper"
"github.com/lavanet/lava/testutil/nullify"
"github.com/lavanet/lava/testutil/sample"
"github.com/lavanet/lava/x/rewards/keeper"
"github.com/lavanet/lava/x/rewards/types"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -449,3 +450,100 @@ func TestPendingIbcIprpcFundNewFunds(t *testing.T) {
})
}
}

// TestCoverIbcIprpcFundCost tests that the cover-ibc-iprpc-fund-cost transaction
// Scenarios:
// 0. Create 2 PendingIbcIprpcFund objects and fund IbcIprpcReceiver and gov. First with 101ulava for 2 months, second with
// 99ulava for 2 months. Expected: PendingIbcIprpcFund with 50ulava, PendingIbcIprpcFund with 49ulava, community pool 2ulava
// 1. Cover costs with alice for first PendingIbcIprpcFund. Expect two iprpc rewards from next month of 50ulava, PendingIbcIprpcFund
// removed, IPRPC pool with 100ulava, second PendingIbcIprpcFund remains (49ulava), alice balance reduced by MinIprpcCost
// 2. Cover costs with gov module for second PendingIbcIprpcFund. Expect two iprpc rewards from next month of 99ulava, PendingIbcIprpcFund
// removed, IPRPC pool with 198ulava, gov module balance not reduced by MinIprpcCost
func TestCoverIbcIprpcFundCost(t *testing.T) {
ts := newTester(t, true)
ts.setupForIprpcTests(false)
keeper, ctx := ts.Keepers.Rewards, ts.Ctx
spec := ts.Spec(mockSpec2)
alice := sample.AccAddressObject()
aliceBalance := sdk.NewCoins(sdk.NewCoin(ts.TokenDenom(), math.NewInt(10000)))
err := ts.Keepers.BankKeeper.SetBalance(ctx, alice, aliceBalance)
require.NoError(t, err)

// fund IbcIprpcReceiver and gov module
// IbcIprpcReceiver gets the total coins that were sent
// gov module get a dummy balance, just to see it's not changing
ibcIprpcReceiverBalance := sdk.NewCoins(sdk.NewCoin(ts.TokenDenom(), math.NewInt(200)))
err = ts.Keepers.BankKeeper.SetBalance(ctx, types.IbcIprpcReceiverAddress(), ibcIprpcReceiverBalance)
require.NoError(t, err)
govModuleBalance := sdk.NewCoins(sdk.NewCoin(ts.TokenDenom(), math.OneInt()))
govModule := ts.Keepers.AccountKeeper.GetModuleAddress("gov")
err = ts.Keepers.BankKeeper.SetBalance(ctx, govModule, govModuleBalance)
require.NoError(t, err)

// create PendingIbcIprpcFund objects
piif1, err := keeper.NewPendingIbcIprpcFund(ctx, alice.String(), spec.Index, 2, sdk.NewCoin(ts.TokenDenom(), math.NewInt(101)))
require.NoError(t, err)
require.True(t, piif1.Fund.Amount.Equal(math.NewInt(50)))
piif2, err := keeper.NewPendingIbcIprpcFund(ctx, alice.String(), spec.Index, 2, sdk.NewCoin(ts.TokenDenom(), math.NewInt(99)))
require.NoError(t, err)
require.True(t, piif2.Fund.Amount.Equal(math.NewInt(49)))

// check community pool balance
communityCoins := ts.Keepers.Distribution.GetFeePoolCommunityCoins(ts.Ctx)
communityBalance := communityCoins.AmountOf(ts.TokenDenom()).TruncateInt()
require.True(t, communityBalance.Equal(math.NewInt(2)))

// cover costs of first PendingIbcIprpcFund with alice
expectedMinCost := keeper.CalcPendingIbcIprpcFundMinCost(ctx, piif1).Amount.Int64()
_, err = ts.TxRewardsCoverIbcIprpcFundCost(alice.String(), piif1.Index)
require.NoError(t, err)
_, found := keeper.GetPendingIbcIprpcFund(ctx, piif1.Index)
require.False(t, found)
require.Equal(t, expectedMinCost, aliceBalance.AmountOf(ts.TokenDenom()).Int64()-ts.GetBalance(alice))

res, err := ts.QueryRewardsIprpcSpecReward(mockSpec2)
require.NoError(t, err)
expectedIprpcRewards := []types.IprpcReward{
{Id: 1, SpecFunds: []types.Specfund{{Spec: mockSpec2, Fund: sdk.NewCoins(sdk.NewCoin(ts.TokenDenom(), math.NewInt(50)))}}},
{Id: 2, SpecFunds: []types.Specfund{{Spec: mockSpec2, Fund: sdk.NewCoins(sdk.NewCoin(ts.TokenDenom(), math.NewInt(50)))}}},
}
require.Len(t, res.IprpcRewards, len(expectedIprpcRewards))
for i := range res.IprpcRewards {
require.Equal(t, expectedIprpcRewards[i].Id, res.IprpcRewards[i].Id)
require.ElementsMatch(t, expectedIprpcRewards[i].SpecFunds, res.IprpcRewards[i].SpecFunds)
}

val, found := keeper.GetPendingIbcIprpcFund(ctx, piif2.Index)
require.True(t, found)
require.True(t, val.IsEqual(piif2))

expectedIprpcPoolBalance := sdk.NewCoins(sdk.NewCoin(ts.TokenDenom(), math.NewInt(100)))
iprpcPoolBalance := ts.Keepers.Rewards.TotalPoolTokens(ts.Ctx, types.IprpcPoolName)
require.True(t, expectedIprpcPoolBalance.IsEqual(iprpcPoolBalance))

// cover costs of first PendingIbcIprpcFund with gov
_, err = ts.TxRewardsCoverIbcIprpcFundCost(govModule.String(), piif2.Index)
require.NoError(t, err)
_, found = keeper.GetPendingIbcIprpcFund(ctx, piif2.Index)
require.False(t, found)
require.Equal(t, int64(0), govModuleBalance.AmountOf(ts.TokenDenom()).Int64()-ts.GetBalance(govModule))

res, err = ts.QueryRewardsIprpcSpecReward(mockSpec2)
require.NoError(t, err)
expectedIprpcRewards = []types.IprpcReward{
{Id: 1, SpecFunds: []types.Specfund{{Spec: mockSpec2, Fund: sdk.NewCoins(sdk.NewCoin(ts.TokenDenom(), math.NewInt(99)))}}},
{Id: 2, SpecFunds: []types.Specfund{{Spec: mockSpec2, Fund: sdk.NewCoins(sdk.NewCoin(ts.TokenDenom(), math.NewInt(99)))}}},
}
require.Len(t, res.IprpcRewards, len(expectedIprpcRewards))
for i := range res.IprpcRewards {
require.Equal(t, expectedIprpcRewards[i].Id, res.IprpcRewards[i].Id)
require.ElementsMatch(t, expectedIprpcRewards[i].SpecFunds, res.IprpcRewards[i].SpecFunds)
}

expectedIprpcPoolBalance = sdk.NewCoins(sdk.NewCoin(ts.TokenDenom(), math.NewInt(198)))
iprpcPoolBalance = ts.Keepers.Rewards.TotalPoolTokens(ts.Ctx, types.IprpcPoolName)
require.True(t, expectedIprpcPoolBalance.IsEqual(iprpcPoolBalance))

// verify that IbcIprpcReceiver has zero balance (to test that the SetBalance() is correct)
require.Equal(t, int64(0), ts.GetBalance(types.IbcIprpcReceiverAddress()))
}
93 changes: 59 additions & 34 deletions x/rewards/keeper/iprpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,49 +17,56 @@ func (k Keeper) FundIprpc(ctx sdk.Context, creator string, duration uint64, fund
return utils.LavaFormatWarning("spec not found or disabled", types.ErrFundIprpc)
}

// check fund consists of minimum amount of ulava (min_iprpc_cost)
minIprpcFundCost := k.GetMinIprpcCost(ctx)
if fund.AmountOf(k.stakingKeeper.BondDenom(ctx)).LT(minIprpcFundCost.Amount) {
return utils.LavaFormatWarning("insufficient ulava tokens in fund. should be at least min iprpc cost * duration", types.ErrFundIprpc,
utils.LogAttr("min_iprpc_cost", k.GetMinIprpcCost(ctx).String()),
utils.LogAttr("duration", strconv.FormatUint(duration, 10)),
utils.LogAttr("fund_ulava_amount", fund.AmountOf(k.stakingKeeper.BondDenom(ctx))),
)
} else if fund.IsEqual(sdk.NewCoins(minIprpcFundCost)) {
return utils.LavaFormatWarning("funds are equal to min iprpc cost, no funds left to send to iprpc pool", types.ErrFundIprpc,
utils.LogAttr("creator", creator),
utils.LogAttr("spec", spec),
utils.LogAttr("funds", fund.String()),
utils.LogAttr("min_iprpc_cost", minIprpcFundCost.String()),
)
}

// check creator has enough balance
addr, err := sdk.AccAddressFromBech32(creator)
if err != nil {
return utils.LavaFormatWarning("invalid creator address", types.ErrFundIprpc)
}
// calculate total funds to transfer to the IPRPC pool (input fund is monthly fund)
totalFunds := fund.MulInt(math.NewIntFromUint64(duration))

// if the fund TX originates from the gov module (keeper's authority field) it's not paying the minimum IPRPC cost
oren-lava marked this conversation as resolved.
Show resolved Hide resolved
if creator != k.authority && creator != types.IbcIprpcReceiver {
// check fund consists of minimum amount of ulava (min_iprpc_cost)
minIprpcFundCost := k.GetMinIprpcCost(ctx)
if fund.AmountOf(k.stakingKeeper.BondDenom(ctx)).LT(minIprpcFundCost.Amount) {
return utils.LavaFormatWarning("insufficient ulava tokens in fund. should be at least min iprpc cost * duration", types.ErrFundIprpc,
utils.LogAttr("min_iprpc_cost", k.GetMinIprpcCost(ctx).String()),
utils.LogAttr("duration", strconv.FormatUint(duration, 10)),
utils.LogAttr("fund_ulava_amount", fund.AmountOf(k.stakingKeeper.BondDenom(ctx))),
)
} else if fund.IsEqual(sdk.NewCoins(minIprpcFundCost)) {
return utils.LavaFormatWarning("funds are equal to min iprpc cost, no funds left to send to iprpc pool", types.ErrFundIprpc,
utils.LogAttr("creator", creator),
utils.LogAttr("spec", spec),
utils.LogAttr("funds", fund.String()),
utils.LogAttr("min_iprpc_cost", minIprpcFundCost.String()),
)
}

// send the minimum cost to the validators allocation pool (and subtract them from the fund)
minIprpcFundCostCoins := sdk.NewCoins(minIprpcFundCost).MulInt(sdk.NewIntFromUint64(duration))
err = k.bankKeeper.SendCoinsFromAccountToModule(ctx, addr, string(types.ValidatorsRewardsAllocationPoolName), minIprpcFundCostCoins)
if err != nil {
return utils.LavaFormatError(types.ErrFundIprpc.Error()+"for funding validator allocation pool", err,
utils.LogAttr("creator", creator),
utils.LogAttr("min_iprpc_fund_cost", minIprpcFundCost.String()),
)
// send the minimum cost to the validators allocation pool (and subtract them from the fund)
minIprpcFundCostCoins := sdk.NewCoins(minIprpcFundCost).MulInt(sdk.NewIntFromUint64(duration))
addr, err := sdk.AccAddressFromBech32(creator)
if err != nil {
return utils.LavaFormatError(types.ErrFundIprpc.Error()+"for funding validator allocation pool with min iprpc cost", err,
utils.LogAttr("creator", creator),
utils.LogAttr("min_iprpc_fund_cost", minIprpcFundCost.String()),
)
}
err = k.bankKeeper.SendCoinsFromAccountToModule(ctx, addr, string(types.ValidatorsRewardsAllocationPoolName), minIprpcFundCostCoins)
if err != nil {
return utils.LavaFormatError(types.ErrFundIprpc.Error()+"for funding validator allocation pool with min iprpc cost", err,
utils.LogAttr("creator", creator),
utils.LogAttr("min_iprpc_fund_cost", minIprpcFundCost.String()),
)
}
fund = fund.Sub(minIprpcFundCost)
totalFunds = fund.MulInt(math.NewIntFromUint64(duration))
}
fund = fund.Sub(minIprpcFundCost)
allFunds := fund.MulInt(math.NewIntFromUint64(duration))

// send the funds to the iprpc pool
err = k.bankKeeper.SendCoinsFromAccountToModule(ctx, addr, string(types.IprpcPoolName), allFunds)
err := k.sendCoinsToIprpcPool(ctx, creator, totalFunds)
if err != nil {
return utils.LavaFormatError(types.ErrFundIprpc.Error()+"for funding iprpc pool", err,
utils.LogAttr("creator", creator),
utils.LogAttr("monthly_fund", fund.String()),
utils.LogAttr("duration", duration),
utils.LogAttr("total_fund", allFunds.String()),
utils.LogAttr("total_fund", totalFunds.String()),
)
}

Expand All @@ -69,6 +76,24 @@ func (k Keeper) FundIprpc(ctx sdk.Context, creator string, duration uint64, fund
return nil
}

func (k Keeper) sendCoinsToIprpcPool(ctx sdk.Context, sender string, amount sdk.Coins) (err error) {
// sender is gov module - use SendCoinsFromModuleToModule
if sender == k.authority {
return k.bankKeeper.SendCoinsFromModuleToModule(ctx, sender, string(types.IprpcPoolName), amount)
}

// sender is account. check if IbcIprpcReceiver or not
addr := types.IbcIprpcReceiverAddress()
if sender != types.IbcIprpcReceiver {
addr, err = sdk.AccAddressFromBech32(sender)
if err != nil {
return err
}
}

return k.bankKeeper.SendCoinsFromAccountToModule(ctx, addr, string(types.IprpcPoolName), amount)
}

// handleNoIprpcRewardToProviders handles the situation in which there are no providers to send IPRPC rewards to
// so the IPRPC rewards transfer to the next month
func (k Keeper) handleNoIprpcRewardToProviders(ctx sdk.Context, iprpcFunds []types.Specfund) {
Expand Down
Loading