Skip to content

Commit

Permalink
fix!(manifest): only poa is allowed to burn tokens
Browse files Browse the repository at this point in the history
  • Loading branch information
fmorency committed May 29, 2024
1 parent 218c0f5 commit 476da13
Show file tree
Hide file tree
Showing 13 changed files with 264 additions and 173 deletions.
7 changes: 4 additions & 3 deletions api/manifest/v1/genesis.pulsar.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

153 changes: 78 additions & 75 deletions api/manifest/v1/tx.pulsar.go

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions interchaintest/helpers/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,10 @@ func ManifestQueryParams(ctx context.Context, node *cosmos.ChainNode) (*manifest
res, err := manifesttypes.NewQueryClient(node.GrpcConn).Params(ctx, &manifesttypes.QueryParamsRequest{})
return res.GetParams(), err
}

func ManifestBurnTokens(t *testing.T, ctx context.Context, chain *cosmos.CosmosChain, keyName string, amount string, flags ...string) (sdk.TxResponse, error) {
txCmd := []string{"tx", "manifest", "burn-coins", amount}
fmt.Println("ManifestBurnTokens", txCmd)
cmd := TxCommandBuilder(ctx, chain, txCmd, keyName, flags...)
return ExecuteTransaction(ctx, chain, cmd)
}
55 changes: 55 additions & 0 deletions interchaintest/mainfest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,61 @@ func TestManifestModule(t *testing.T) {
require.ErrorContains(t, err, "invalid decimal coin expression")
})

t.Run("fail: invalid burn authority", func(t *testing.T) {
accBal, err := appChain.GetBalance(ctx, uaddr, Denom)
require.NoError(t, err)
o, err := helpers.ManifestBurnTokens(t, ctx, appChain, uaddr, "1"+Denom)
require.NoError(t, err) // The tx is successful but the burn fails
tx, err := appChain.GetTransaction(o.TxHash)
require.NoError(t, err)
require.NotEqual(t, tx.Code, uint32(0x0)) // The burn failed
require.Contains(t, tx.RawLog, "invalid authority")
accBal2, err := appChain.GetBalance(ctx, uaddr, Denom)
require.NoError(t, err)
require.EqualValues(t, accBal, accBal2)
})

t.Run("success: burn tokens as poa admin", func(t *testing.T) {
poaAdminAddr := poaAdmin.FormattedAddress()
accBal, err := appChain.GetBalance(ctx, poaAdminAddr, Denom)
require.NoError(t, err)
o, err := helpers.ManifestBurnTokens(t, ctx, appChain, poaAdminAddr, "1"+Denom)
require.NoError(t, err)
tx, err := appChain.GetTransaction(o.TxHash)
require.NoError(t, err)
require.Equal(t, tx.Code, uint32(0x0))
accBal2, err := appChain.GetBalance(ctx, poaAdminAddr, Denom)
require.NoError(t, err)
require.EqualValues(t, accBal2, accBal.Sub(sdkmath.OneInt()))
})

t.Run("fail: burn unknown denom as poa admin", func(t *testing.T) {
poaAdminAddr := poaAdmin.FormattedAddress()
accBal, err := appChain.GetBalance(ctx, poaAdminAddr, Denom)
require.NoError(t, err)
o, err := helpers.ManifestBurnTokens(t, ctx, appChain, poaAdminAddr, "1foobar")
require.NoError(t, err) // The tx is successful but the burn fails
tx, err := appChain.GetTransaction(o.TxHash)
require.NoError(t, err)
require.NotEqual(t, tx.Code, uint32(0x0)) // The burn failed
require.Contains(t, tx.RawLog, "insufficient funds ")
accBal2, err := appChain.GetBalance(ctx, poaAdminAddr, Denom)
require.NoError(t, err)
require.EqualValues(t, accBal, accBal2)
})

t.Run("fail: burn invalid coin expression as poa admin", func(t *testing.T) {
poaAdminAddr := poaAdmin.FormattedAddress()
accBal, err := appChain.GetBalance(ctx, poaAdminAddr, Denom)
require.NoError(t, err)
_, err = helpers.ManifestBurnTokens(t, ctx, appChain, poaAdminAddr, "foobar")
require.Error(t, err)
require.Contains(t, err.Error(), "invalid decimal coin expression")
accBal2, err := appChain.GetBalance(ctx, poaAdminAddr, Denom)
require.NoError(t, err)
require.EqualValues(t, accBal, accBal2)
})

t.Cleanup(func() {
CopyCoverageFromContainer(ctx, t, client, appChain.GetNode().ContainerID(), appChain.HomeDir())
_ = ic.Close()
Expand Down
2 changes: 1 addition & 1 deletion interchaintest/poa_group_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -565,7 +565,7 @@ func createManifestPayoutProposal(authority string, payout sdk.Coin) manifesttyp

func createManifestBurnProposal(sender string, amounts sdk.Coins) manifesttypes.MsgBurnHeldBalance {
return manifesttypes.MsgBurnHeldBalance{
Sender: sender,
Authority: sender,
BurnCoins: amounts,
}
}
Expand Down
9 changes: 5 additions & 4 deletions proto/manifest/v1/genesis.proto
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ option go_package = "github.com/liftedinit/manifest-ledger/x/manifest/types";
// GenesisState defines the module genesis state
message GenesisState {
// Params defines all the paramaters of the module.
Params params = 1 [(gogoproto.nullable) = false];
Params params = 1 [ (gogoproto.nullable) = false ];
}

// Params defines the set of module parameters.
Expand All @@ -23,15 +23,16 @@ message Params {
Inflation inflation = 2;
}

// StakeHolders is the list of addresses and their percentage of the inflation distribution
// StakeHolders is the list of addresses and their percentage of the inflation
// distribution
message StakeHolders {
option (gogoproto.equal) = true;

// manifest address
string address = 1;

// percentage is the micro denom % of tokens this address gets on a distribution.
// 100% = 100_000_000 total. so 1_000000 = 1%.
// percentage is the micro denom % of tokens this address gets on a
// distribution. 100% = 100_000_000 total. so 1_000000 = 1%.
int32 percentage = 2;
}

Expand Down
31 changes: 17 additions & 14 deletions proto/manifest/v1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ service Msg {
rpc UpdateParams(MsgUpdateParams) returns (MsgUpdateParamsResponse);

// PayoutStakeholders allows the authority to manually pay out stakeholders.
rpc PayoutStakeholders(MsgPayoutStakeholders) returns (MsgPayoutStakeholdersResponse);
rpc PayoutStakeholders(MsgPayoutStakeholders)
returns (MsgPayoutStakeholdersResponse);

// BurnHeldBalance allows a tokenholder to burn coins they own.
rpc BurnHeldBalance(MsgBurnHeldBalance) returns (MsgBurnHeldBalanceResponse);
Expand All @@ -33,12 +34,12 @@ message MsgUpdateParams {
option (cosmos.msg.v1.signer) = "authority";

// authority is the address of the controlling account.
string authority = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
string authority = 1 [ (cosmos_proto.scalar) = "cosmos.AddressString" ];

// params defines the parameters to update.
//
// NOTE: All parameters must be supplied.
Params params = 2 [(gogoproto.nullable) = false];
Params params = 2 [ (gogoproto.nullable) = false ];
}

// MsgUpdateParamsResponse defines the response structure for executing a
Expand All @@ -53,36 +54,38 @@ message MsgPayoutStakeholders {
option (gogoproto.equal) = false;

// authority is the address of the controlling account.
string authority = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
string authority = 1 [ (cosmos_proto.scalar) = "cosmos.AddressString" ];

// payout is the amount of tokens paid to the current stakeholders.
cosmos.base.v1beta1.Coin payout = 2 [
(gogoproto.nullable) = false,
(amino.dont_omitempty) = true,
(amino.encoding) = "legacy_coins",
(gogoproto.nullable) = false,
(amino.dont_omitempty) = true,
(amino.encoding) = "legacy_coins",
(gogoproto.castrepeated) = "cosmossdk.io/api/cosmos/base/v1beta1.Coins"
];
}

// MsgPayoutStakeholdersResponse defines the response structure for executing a MsgPayoutStakeholders message.
// MsgPayoutStakeholdersResponse defines the response structure for executing a
// MsgPayoutStakeholders message.
message MsgPayoutStakeholdersResponse {}

// MsgPayoutStakeholders is the Msg/BurnHeldBalance request type.
message MsgBurnHeldBalance {
option (cosmos.msg.v1.signer) = "sender";
option (cosmos.msg.v1.signer) = "authority";
option (gogoproto.equal) = false;

// sender is the address of the tokenholder.
string sender = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
string authority = 1 [ (cosmos_proto.scalar) = "cosmos.AddressString" ];

// burn_coins are the coins to be burned by the tokenholder.
repeated cosmos.base.v1beta1.Coin burn_coins = 2 [
(gogoproto.nullable) = false,
(amino.dont_omitempty) = true,
(amino.encoding) = "legacy_coins",
(gogoproto.nullable) = false,
(amino.dont_omitempty) = true,
(amino.encoding) = "legacy_coins",
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"
];
}

// MsgBurnHeldBalanceResponse defines the response structure for executing a MsgBurnHeldBalance message.
// MsgBurnHeldBalanceResponse defines the response structure for executing a
// MsgBurnHeldBalance message.
message MsgBurnHeldBalanceResponse {}
2 changes: 1 addition & 1 deletion x/manifest/client/cli/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ func MsgBurnCoins() *cobra.Command {
}

msg := &types.MsgBurnHeldBalance{
Sender: sender.String(),
Authority: sender.String(),
BurnCoins: coins,
}

Expand Down
13 changes: 8 additions & 5 deletions x/manifest/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,18 @@ func (ms msgServer) PayoutStakeholders(ctx context.Context, req *types.MsgPayout
}

// BurnHeldBalance implements types.MsgServer.
func (ms msgServer) BurnHeldBalance(ctx context.Context, msg *types.MsgBurnHeldBalance) (*types.MsgBurnHeldBalanceResponse, error) {
addr, err := sdk.AccAddressFromBech32(msg.Sender)
func (ms msgServer) BurnHeldBalance(ctx context.Context, req *types.MsgBurnHeldBalance) (*types.MsgBurnHeldBalanceResponse, error) {
if ms.k.authority != req.Authority {
return nil, fmt.Errorf("invalid authority; expected %s, got %s", ms.k.authority, req.Authority)
}
addr, err := sdk.AccAddressFromBech32(req.Authority)
if err != nil {
return nil, err
}

if err := ms.k.bankKeeper.SendCoinsFromAccountToModule(ctx, addr, types.ModuleName, msg.BurnCoins); err != nil {
return nil, fmt.Errorf("not enough balance to burn %s: %w", msg.BurnCoins, err)
if err := ms.k.bankKeeper.SendCoinsFromAccountToModule(ctx, addr, types.ModuleName, req.BurnCoins); err != nil {
return nil, fmt.Errorf("not enough balance to burn %s: %w", req.BurnCoins, err)
}

return &types.MsgBurnHeldBalanceResponse{}, ms.k.bankKeeper.BurnCoins(ctx, types.ModuleName, msg.BurnCoins)
return &types.MsgBurnHeldBalanceResponse{}, ms.k.bankKeeper.BurnCoins(ctx, types.ModuleName, req.BurnCoins)
}
40 changes: 28 additions & 12 deletions x/manifest/keeper/msg_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,13 +244,14 @@ func TestBurnCoins(t *testing.T) {
k := f.App.ManifestKeeper
k.SetAuthority(authority.String())
ms := keeper.NewMsgServerImpl(k)
_, _, acc := testdata.KeyTestPubAddr()

type tc struct {
name string
initial sdk.Coins
burn sdk.Coins
expected sdk.Coins
address string
address sdk.AccAddress
success bool
}

Expand All @@ -263,50 +264,55 @@ func TestBurnCoins(t *testing.T) {
initial: sdk.NewCoins(),
burn: sdk.NewCoins(sdk.NewCoin("stake", sdkmath.NewInt(7))),
expected: sdk.NewCoins(),
address: authority,
},
{
name: "fail; bad address",
initial: sdk.NewCoins(),
burn: sdk.NewCoins(sdk.NewCoin("stake", sdkmath.NewInt(7))),
expected: sdk.NewCoins(),
address: "xyz",
address: sdk.AccAddress{0x0},
},
{
name: "success; burn 1 token successfully",
name: "success; burn tokens successfully",
initial: sdk.NewCoins(stake, mfx),
burn: sdk.NewCoins(sdk.NewCoin("stake", sdkmath.NewInt(7))),
expected: sdk.NewCoins(mfx, stake.SubAmount(sdkmath.NewInt(7))),
address: authority,
success: true,
},
{
name: "success; burn many tokens successfully",
initial: sdk.NewCoins(stake, mfx),
burn: sdk.NewCoins(sdk.NewCoin("umfx", sdkmath.NewInt(9)), sdk.NewCoin("stake", sdkmath.NewInt(7))),
expected: sdk.NewCoins(mfx.SubAmount(sdkmath.NewInt(9)), stake.SubAmount(sdkmath.NewInt(7))),
address: authority,
success: true,
},
{
name: "fail; invalid authority",
initial: sdk.NewCoins(stake, mfx),
burn: sdk.NewCoins(sdk.NewCoin("stake", sdkmath.NewInt(7))),
expected: sdk.NewCoins(stake, mfx),
address: acc,
},
}

for _, c := range cases {
c := c
t.Run(c.name, func(t *testing.T) {
_, _, acc := testdata.KeyTestPubAddr()
if c.address == "" {
c.address = acc.String()
}

// setup initial balances for the new account
if len(c.initial) > 0 {
require.NoError(t, f.App.BankKeeper.MintCoins(f.Ctx, "mint", c.initial))
require.NoError(t, f.App.BankKeeper.SendCoinsFromModuleToAccount(f.Ctx, "mint", acc, c.initial))
require.NoError(t, f.App.BankKeeper.SendCoinsFromModuleToAccount(f.Ctx, "mint", c.address, c.initial))
}

// validate initial balance
require.Equal(t, c.initial, f.App.BankKeeper.GetAllBalances(f.Ctx, acc))
require.Equal(t, c.initial, f.App.BankKeeper.GetAllBalances(f.Ctx, c.address))

// burn coins
_, err := ms.BurnHeldBalance(f.Ctx, &types.MsgBurnHeldBalance{
Sender: c.address,
Authority: c.address.String(),
BurnCoins: c.burn,
})
if c.success {
Expand All @@ -315,7 +321,17 @@ func TestBurnCoins(t *testing.T) {
require.Error(t, err)
}

require.Equal(t, c.expected, f.App.BankKeeper.GetAllBalances(f.Ctx, acc))
allBalance := f.App.BankKeeper.GetAllBalances(f.Ctx, c.address)
require.Equal(t, c.expected, allBalance)

// burn the rest of the coins to reset the balance to 0 for the next test if the test was successful
if c.success {
_, err = ms.BurnHeldBalance(f.Ctx, &types.MsgBurnHeldBalance{
Authority: c.address.String(),
BurnCoins: allBalance,
})
require.NoError(t, err)
}
})
}
}
7 changes: 4 additions & 3 deletions x/manifest/types/genesis.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions x/manifest/types/msgs.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func NewMsgBurnHeldBalance(
coins sdk.Coins,
) *MsgBurnHeldBalance {
return &MsgBurnHeldBalance{
Sender: sender.String(),
Authority: sender.String(),
BurnCoins: coins,
}
}
Expand All @@ -112,13 +112,13 @@ func (msg MsgBurnHeldBalance) GetSignBytes() []byte {

// GetSigners returns the expected signers for the message.
func (msg *MsgBurnHeldBalance) GetSigners() []sdk.AccAddress {
addr, _ := sdk.AccAddressFromBech32(msg.Sender)
addr, _ := sdk.AccAddressFromBech32(msg.Authority)

Check warning on line 115 in x/manifest/types/msgs.go

View check run for this annotation

Codecov / codecov/patch

x/manifest/types/msgs.go#L115

Added line #L115 was not covered by tests
return []sdk.AccAddress{addr}
}

// ValidateBasic does a sanity check on the provided data.
func (msg *MsgBurnHeldBalance) Validate() error {
if _, err := sdk.AccAddressFromBech32(msg.Sender); err != nil {
if _, err := sdk.AccAddressFromBech32(msg.Authority); err != nil {
return errors.Wrap(err, "invalid authority address")
}

Expand Down
Loading

0 comments on commit 476da13

Please sign in to comment.