Skip to content

Commit

Permalink
Be able to set platform send minimums (#271)
Browse files Browse the repository at this point in the history
Co-authored-by: Kehinde Faleye <Kenny.fale.kf@gmail.com>
  • Loading branch information
ash-burnt and Peartes authored Oct 21, 2024
1 parent 80609c0 commit 4c19477
Show file tree
Hide file tree
Showing 12 changed files with 746 additions and 64 deletions.
63 changes: 63 additions & 0 deletions integration_tests/minimum_fee_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,16 @@ package integration_tests

import (
"context"
"encoding/json"
"fmt"
"strconv"
"testing"
"time"

xiontypes "github.com/burnt-labs/xion/x/xion/types"
"github.com/cosmos/cosmos-sdk/codec"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
govv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1"

"cosmossdk.io/math"

Expand Down Expand Up @@ -97,6 +105,61 @@ func testMinimumFee(t *testing.T, td *TestData, assert assertionFn) {
require.NoError(t, err)
require.Equal(t, fundAmount, xionUserBalInitial)

cdc := codec.NewProtoCodec(xion.Config().EncodingConfig.InterfaceRegistry)
config := types.GetConfig()
config.SetBech32PrefixForAccount("xion", "xionpub")

setPlatformMinimumsMsg := xiontypes.MsgSetPlatformMinimum{
Authority: authtypes.NewModuleAddress("gov").String(),
Minimums: types.Coins{types.Coin{Amount: math.NewInt(10), Denom: "uxion"}},
}

msg, err := cdc.MarshalInterfaceJSON(&setPlatformMinimumsMsg)
require.NoError(t, err)

prop := cosmos.TxProposalv1{
Messages: []json.RawMessage{msg},
Metadata: "",
Deposit: "100uxion",
Title: "Set platform percentage to 5%",
Summary: "Ups the platform fee to 5% for the integration test",
}
paramChangeTx, err := xion.SubmitProposal(ctx, xionUser.KeyName(), prop)
require.NoError(t, err)
t.Logf("Platform percentage change proposal submitted with ID %s in transaction %s", paramChangeTx.ProposalID, paramChangeTx.TxHash)

proposalID, err := strconv.Atoi(paramChangeTx.ProposalID)
require.NoError(t, err)

require.Eventuallyf(t, func() bool {
proposalInfo, err := xion.GovQueryProposal(ctx, uint64(proposalID))
if err != nil {
require.NoError(t, err)
} else {
if proposalInfo.Status == govv1beta1.StatusVotingPeriod {
return true
}
t.Logf("Waiting for proposal to enter voting status VOTING, current status: %s", proposalInfo.Status)
}
return false
}, time.Second*11, time.Second, "failed to reach status VOTING after 11s")

err = xion.VoteOnProposalAllValidators(ctx, uint64(proposalID), cosmos.ProposalVoteYes)
require.NoError(t, err)

require.Eventuallyf(t, func() bool {
proposalInfo, err := xion.GovQueryProposal(ctx, uint64(proposalID))
if err != nil {
require.NoError(t, err)
} else {
if proposalInfo.Status == govv1beta1.StatusPassed {
return true
}
t.Logf("Waiting for proposal to enter voting status PASSED, current status: %s", proposalInfo.Status)
}
return false
}, time.Second*11, time.Second, "failed to reach status PASSED after 11s")

// step 1: send a xion message with default (0%) platform fee
recipientKeyName := "recipient-key"
err = xion.CreateKey(ctx, recipientKeyName)
Expand Down
73 changes: 66 additions & 7 deletions integration_tests/send_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,68 @@ func TestXionSendPlatformFee(t *testing.T) {
require.NoError(t, err)

cdc := codec.NewProtoCodec(xion.Config().EncodingConfig.InterfaceRegistry)
config := types.GetConfig()
config.SetBech32PrefixForAccount("xion", "xionpub")

setPlatformMinimumsMsg := xiontypes.MsgSetPlatformMinimum{
Authority: authtypes.NewModuleAddress("gov").String(),
Minimums: types.Coins{types.Coin{Amount: math.NewInt(10), Denom: "uxion"}},
}

msg, err := cdc.MarshalInterfaceJSON(&setPlatformMinimumsMsg)
require.NoError(t, err)

_, err = xion.GetNode().ExecTx(ctx,
xionUser.KeyName(),
"xion", "send", xionUser.KeyName(),
"--chain-id", xion.Config().ChainID,
recipientKeyAddress, fmt.Sprintf("%d%s", 100, xion.Config().Denom),
)
// platform minimums unset, so this should fail
require.Error(t, err)

prop := cosmos.TxProposalv1{
Messages: []json.RawMessage{msg},
Metadata: "",
Deposit: "100uxion",
Title: "Set platform percentage to 5%",
Summary: "Ups the platform fee to 5% for the integration test",
}
paramChangeTx, err := xion.SubmitProposal(ctx, xionUser.KeyName(), prop)
require.NoError(t, err)
t.Logf("Platform percentage change proposal submitted with ID %s in transaction %s", paramChangeTx.ProposalID, paramChangeTx.TxHash)

proposalID, err := strconv.Atoi(paramChangeTx.ProposalID)
require.NoError(t, err)

require.Eventuallyf(t, func() bool {
proposalInfo, err := xion.GovQueryProposal(ctx, uint64(proposalID))
if err != nil {
require.NoError(t, err)
} else {
if proposalInfo.Status == govv1beta1.StatusVotingPeriod {
return true
}
t.Logf("Waiting for proposal to enter voting status VOTING, current status: %s", proposalInfo.Status)
}
return false
}, time.Second*11, time.Second, "failed to reach status VOTING after 11s")

err = xion.VoteOnProposalAllValidators(ctx, uint64(proposalID), cosmos.ProposalVoteYes)
require.NoError(t, err)

require.Eventuallyf(t, func() bool {
proposalInfo, err := xion.GovQueryProposal(ctx, uint64(proposalID))
if err != nil {
require.NoError(t, err)
} else {
if proposalInfo.Status == govv1beta1.StatusPassed {
return true
}
t.Logf("Waiting for proposal to enter voting status PASSED, current status: %s", proposalInfo.Status)
}
return false
}, time.Second*11, time.Second, "failed to reach status PASSED after 11s")

_, err = xion.GetNode().ExecTx(ctx,
xionUser.KeyName(),
Expand All @@ -74,29 +136,26 @@ func TestXionSendPlatformFee(t *testing.T) {
"balance never correctly changed")

// step 2: update the platform percentage to 5%
config := types.GetConfig()
config.SetBech32PrefixForAccount("xion", "xionpub")

setPlatformPercentageMsg := xiontypes.MsgSetPlatformPercentage{
Authority: authtypes.NewModuleAddress("gov").String(),
PlatformPercentage: 500,
}

msg, err := cdc.MarshalInterfaceJSON(&setPlatformPercentageMsg)
msg, err = cdc.MarshalInterfaceJSON(&setPlatformPercentageMsg)
require.NoError(t, err)

prop := cosmos.TxProposalv1{
prop = cosmos.TxProposalv1{
Messages: []json.RawMessage{msg},
Metadata: "",
Deposit: "100uxion",
Title: "Set platform percentage to 5%",
Summary: "Ups the platform fee to 5% for the integration test",
}
paramChangeTx, err := xion.SubmitProposal(ctx, xionUser.KeyName(), prop)
paramChangeTx, err = xion.SubmitProposal(ctx, xionUser.KeyName(), prop)
require.NoError(t, err)
t.Logf("Platform percentage change proposal submitted with ID %s in transaction %s", paramChangeTx.ProposalID, paramChangeTx.TxHash)

proposalID, err := strconv.Atoi(paramChangeTx.ProposalID)
proposalID, err = strconv.Atoi(paramChangeTx.ProposalID)
require.NoError(t, err)

require.Eventuallyf(t, func() bool {
Expand Down
11 changes: 10 additions & 1 deletion proto/xion/v1/genesis.proto
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
syntax = "proto3";
package xion.v1;

import "cosmos/base/v1beta1/coin.proto";
import "gogoproto/gogo.proto";

option go_package = "github.com/burnt-labs/xion/x/xion/types";

message GenesisState { uint32 platform_percentage = 1; }
message GenesisState {
uint32 platform_percentage = 1;
repeated cosmos.base.v1beta1.Coin platform_minimums = 2 [
(gogoproto.nullable) = false,
(gogoproto.jsontag) = "platform_minimums,omitempty",
(gogoproto.moretags) = "yaml:\"platform_minimums\"",
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"
];
}
20 changes: 20 additions & 0 deletions proto/xion/v1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ service Msg {
// percentage fee
rpc SetPlatformPercentage(MsgSetPlatformPercentage)
returns (MsgSetPlatformPercentageResponse);

// SetPlatformMinimum defines the method for updating the platform
// percentage fee
rpc SetPlatformMinimum(MsgSetPlatformMinimum)
returns (MsgSetPlatformMinimumResponse);
}

// MsgSend represents a message to send coins from one account to another.
Expand Down Expand Up @@ -76,3 +81,18 @@ message MsgSetPlatformPercentage {
}

message MsgSetPlatformPercentageResponse {}

message MsgSetPlatformMinimum {
option (cosmos.msg.v1.signer) = "authority";
option (amino.name) = "xion/MsgSetPlatformMinimum";

string authority = 1 [ (cosmos_proto.scalar) = "cosmos.AddressString" ];

repeated cosmos.base.v1beta1.Coin minimums = 3 [
(gogoproto.nullable) = false,
(amino.dont_omitempty) = true,
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"
];
}

message MsgSetPlatformMinimumResponse {}
11 changes: 10 additions & 1 deletion x/xion/keeper/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,21 @@ import (
// InitGenesis initializes the bank module's state from a given genesis state.
func (k Keeper) InitGenesis(ctx sdk.Context, genState *types.GenesisState) {
k.OverwritePlatformPercentage(ctx, genState.PlatformPercentage)
err := k.OverwritePlatformMinimum(ctx, genState.PlatformMinimums)
if err != nil {
panic(err)
}
}

// ExportGenesis returns the bank module's genesis state.
func (k Keeper) ExportGenesis(ctx sdk.Context) *types.GenesisState {
bz := ctx.KVStore(k.storeKey).Get(types.PlatformPercentageKey)
platformPercentage := binary.BigEndian.Uint32(bz)
rv := types.NewGenesisState(platformPercentage)

platformMinimums, err := k.GetPlatformMinimums(ctx)
if err != nil {
panic(err)
}
rv := types.NewGenesisState(platformPercentage, platformMinimums)
return rv
}
21 changes: 21 additions & 0 deletions x/xion/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package keeper

import (
"context"
"encoding/json"

wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types"

Expand Down Expand Up @@ -70,6 +71,26 @@ func (k Keeper) OverwritePlatformPercentage(ctx sdktypes.Context, percentage uin
ctx.KVStore(k.storeKey).Set(types.PlatformPercentageKey, sdktypes.Uint64ToBigEndian(uint64(percentage)))
}

// Platform Minimum
func (k Keeper) GetPlatformMinimums(ctx sdktypes.Context) (coins sdktypes.Coins, err error) {
bz := ctx.KVStore(k.storeKey).Get(types.PlatformMinimumKey)

if len(bz) != 0 {
err = json.Unmarshal(bz, &coins)
}

return coins, err
}

func (k Keeper) OverwritePlatformMinimum(ctx sdktypes.Context, coins sdktypes.Coins) error {
bz, err := json.Marshal(coins)
if err != nil {
return err
}
ctx.KVStore(k.storeKey).Set(types.PlatformMinimumKey, bz)
return nil
}

// Authority

// GetAuthority returns the x/xion module's authority.
Expand Down
30 changes: 29 additions & 1 deletion x/xion/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,15 @@ func (k msgServer) Send(goCtx context.Context, msg *types.MsgSend) (*types.MsgSe
}

percentage := k.GetPlatformPercentage(ctx)
minimums, err := k.GetPlatformMinimums(ctx)
if err != nil {
return nil, err
}
throughCoins := msg.Amount
if !msg.Amount.IsAnyGT(minimums) {
// minimum has not been met. no coin in msg.Amount exceeds a minimum that has been set
return nil, errorsmod.Wrapf(types.ErrMinimumNotMet, "received %v, needed at least %v", msg.Amount, minimums)
}

if !percentage.IsZero() {
platformCoins := msg.Amount.MulInt(percentage).QuoInt(math.NewInt(10000))
Expand Down Expand Up @@ -94,9 +102,18 @@ func (k msgServer) MultiSend(goCtx context.Context, msg *types.MsgMultiSend) (*t
}

percentage := k.GetPlatformPercentage(ctx)
minimums, err := k.GetPlatformMinimums(ctx)
if err != nil {
return nil, err
}
var outputs []banktypes.Output
totalPlatformCoins := sdk.NewCoins()

if !msg.Inputs[0].Coins.IsAnyGT(minimums) {
// minimum has not been met. no coin in msg.Amount exceeds a minimum that has been set
return nil, errorsmod.Wrapf(types.ErrMinimumNotMet, "received %v, needed at least %v", msg.Inputs[0].Coins, minimums)
}

for _, out := range msg.Outputs {
accAddr := sdk.MustAccAddressFromBech32(out.Address)

Expand Down Expand Up @@ -125,7 +142,7 @@ func (k msgServer) MultiSend(goCtx context.Context, msg *types.MsgMultiSend) (*t
outputs = append(outputs, banktypes.NewOutput(feeCollectorAcc, totalPlatformCoins))
}

err := k.bankKeeper.InputOutputCoins(ctx, msg.Inputs[0], outputs)
err = k.bankKeeper.InputOutputCoins(ctx, msg.Inputs[0], outputs)
if err != nil {
return nil, err
}
Expand All @@ -143,3 +160,14 @@ func (k msgServer) SetPlatformPercentage(goCtx context.Context, msg *types.MsgSe

return &types.MsgSetPlatformPercentageResponse{}, nil
}

func (k msgServer) SetPlatformMinimum(goCtx context.Context, msg *types.MsgSetPlatformMinimum) (*types.MsgSetPlatformMinimumResponse, error) {
if k.GetAuthority() != msg.Authority {
return nil, errorsmod.Wrapf(govtypes.ErrInvalidSigner, "invalid authority; expected %s, got %s", k.GetAuthority(), msg.Authority)
}

ctx := sdk.UnwrapSDKContext(goCtx)
err := k.OverwritePlatformMinimum(ctx, msg.Minimums)

return &types.MsgSetPlatformMinimumResponse{}, err
}
1 change: 1 addition & 0 deletions x/xion/types/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ var (
ErrNoAllowedContracts = errorsmod.Register(DefaultCodespace, 2, "no contract addresses specified")
ErrNoValidAllowances = errorsmod.Register(DefaultCodespace, 3, "none of the allowances accepted the msg")
ErrInconsistentExpiry = errorsmod.Register(DefaultCodespace, 4, "multi allowances must all expire together")
ErrMinimumNotMet = errorsmod.Register(DefaultCodespace, 5, "minimum send amount not met")
)
6 changes: 4 additions & 2 deletions x/xion/types/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"

"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/types"
)

// Validate performs basic validation of supply genesis data returning an
Expand All @@ -18,16 +19,17 @@ func (gs GenesisState) Validate() error {
}

// NewGenesisState creates a new genesis state.
func NewGenesisState(platformPercentage uint32) *GenesisState {
func NewGenesisState(platformPercentage uint32, platformMinimums types.Coins) *GenesisState {
rv := &GenesisState{
PlatformPercentage: platformPercentage,
PlatformMinimums: platformMinimums,
}
return rv
}

// DefaultGenesisState returns a default bank module genesis state.
func DefaultGenesisState() *GenesisState {
return NewGenesisState(0)
return NewGenesisState(0, types.Coins{})
}

// GetGenesisStateFromAppState returns x/bank GenesisState given raw application
Expand Down
Loading

0 comments on commit 4c19477

Please sign in to comment.