From 5f2453ced546c218458658c44f2b0f2393bb95fa Mon Sep 17 00:00:00 2001 From: Sunny Aggarwal Date: Fri, 29 Apr 2022 00:17:47 -0400 Subject: [PATCH] x/tokenfactory: add type files (#1361) * add proto files * Update proto/osmosis/tokenfactory/v1beta1/authorityMetadata.proto Co-authored-by: Dev Ojha * update comments on proto * add other types files * Apply suggestions from code review Co-authored-by: Aleksandr Bezobchuk * update errors to not start from 1 * add more comments Co-authored-by: Dev Ojha Co-authored-by: Aleksandr Bezobchuk --- types/codec.go | 35 +++++++ types/denoms.go | 55 +++++++++++ types/denoms_test.go | 64 +++++++++++++ types/errors.go | 17 ++++ types/events.go | 15 +++ types/genesis.go | 45 +++++++++ types/genesis_test.go | 139 +++++++++++++++++++++++++++ types/keys.go | 49 ++++++++++ types/msgs.go | 216 ++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 635 insertions(+) create mode 100644 types/codec.go create mode 100644 types/denoms.go create mode 100644 types/denoms_test.go create mode 100644 types/errors.go create mode 100644 types/events.go create mode 100644 types/genesis.go create mode 100644 types/genesis_test.go create mode 100644 types/keys.go create mode 100644 types/msgs.go diff --git a/types/codec.go b/types/codec.go new file mode 100644 index 0000000000..6308a36fe5 --- /dev/null +++ b/types/codec.go @@ -0,0 +1,35 @@ +package types + +import ( + "github.com/cosmos/cosmos-sdk/codec" + cdctypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + + // this line is used by starport scaffolding # 1 + "github.com/cosmos/cosmos-sdk/types/msgservice" +) + +func RegisterCodec(cdc *codec.LegacyAmino) { + cdc.RegisterConcrete(&MsgCreateDenom{}, "osmosis/tokenfactory/create-denom", nil) + cdc.RegisterConcrete(&MsgMint{}, "osmosis/tokenfactory/mint", nil) + cdc.RegisterConcrete(&MsgBurn{}, "osmosis/tokenfactory/burn", nil) + // cdc.RegisterConcrete(&MsgForceTransfer{}, "osmosis/tokenfactory/force-transfer", nil) + cdc.RegisterConcrete(&MsgChangeAdmin{}, "osmosis/tokenfactory/change-admin", nil) +} + +func RegisterInterfaces(registry cdctypes.InterfaceRegistry) { + registry.RegisterImplementations( + (*sdk.Msg)(nil), + &MsgCreateDenom{}, + &MsgMint{}, + &MsgBurn{}, + // &MsgForceTransfer{}, + &MsgChangeAdmin{}, + ) + msgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc) +} + +var ( + amino = codec.NewLegacyAmino() + ModuleCdc = codec.NewProtoCodec(cdctypes.NewInterfaceRegistry()) +) diff --git a/types/denoms.go b/types/denoms.go new file mode 100644 index 0000000000..6e02a007c0 --- /dev/null +++ b/types/denoms.go @@ -0,0 +1,55 @@ +package types + +import ( + "strings" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +const ( + ModuleDenomPrefix = "factory" +) + +// GetTokenDenom constructs a denom string for tokens created by tokenfactory +// based on an input creator address and a nonce +// The denom constructed is factory/{creator}/{nonce} +func GetTokenDenom(creator, nonce string) (string, error) { + if strings.Contains(creator, "/") { + return "", ErrInvalidCreator + } + denom := strings.Join([]string{ModuleDenomPrefix, creator, nonce}, "/") + return denom, sdk.ValidateDenom(denom) +} + +// DeconstructDenom takes a token denom string and verifies that it is a valid +// denom of the tokenfactory module, and is of the form `factory/{creator}/{nonce}` +// If valid, it returns the creator address and nonce +func DeconstructDenom(denom string) (creator string, nonce string, err error) { + err = sdk.ValidateDenom(denom) + if err != nil { + return "", "", err + } + + strParts := strings.Split(denom, "/") + if len(strParts) < 3 { + return "", "", sdkerrors.Wrapf(ErrInvalidDenom, "not enough parts of denom %s", denom) + } + + if strParts[0] != ModuleDenomPrefix { + return "", "", sdkerrors.Wrapf(ErrInvalidDenom, "denom prefix is incorrect. Is: %s. Should be: %s", strParts[0], ModuleDenomPrefix) + } + + creator = strParts[1] + _, err = sdk.AccAddressFromBech32(creator) + if err != nil { + return "", "", sdkerrors.Wrapf(ErrInvalidDenom, "Invalid creator address (%s)", err) + } + + // Handle the case where a denom has a slash in its nonce. For example, + // when we did the split, we'd turn factory/sunnyaddr/atomderivative/sikka into ["factory", "sunnyaddr", "atomderivative", "sikka"] + // So we have to join [2:] with a "/" as the delimiter to get back the correct nonce which should be "atomderivative/sikka" + nonce = strings.Join(strParts[2:], "/") + + return creator, nonce, nil +} diff --git a/types/denoms_test.go b/types/denoms_test.go new file mode 100644 index 0000000000..b7f0fc0106 --- /dev/null +++ b/types/denoms_test.go @@ -0,0 +1,64 @@ +package types_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + appparams "github.com/osmosis-labs/osmosis/v7/app/params" + "github.com/osmosis-labs/osmosis/v7/x/tokenfactory/types" +) + +func TestDecomposeDenoms(t *testing.T) { + appparams.SetAddressPrefixes() + for _, tc := range []struct { + desc string + denom string + valid bool + }{ + { + desc: "empty is invalid", + denom: "", + valid: false, + }, + { + desc: "normal", + denom: "factory/osmo1t7egva48prqmzl59x5ngv4zx0dtrwewc9m7z44/bitcoin", + valid: true, + }, + { + desc: "multiple slashes in nonce", + denom: "factory/osmo1t7egva48prqmzl59x5ngv4zx0dtrwewc9m7z44/bitcoin/1", + valid: true, + }, + { + desc: "no nonce", + denom: "factory/osmo1t7egva48prqmzl59x5ngv4zx0dtrwewc9m7z44/", + valid: true, + }, + { + desc: "incorrect prefix", + denom: "ibc/osmo1t7egva48prqmzl59x5ngv4zx0dtrwewc9m7z44/bitcoin", + valid: false, + }, + { + desc: "nonce of only slashes", + denom: "factory/osmo1t7egva48prqmzl59x5ngv4zx0dtrwewc9m7z44/////", + valid: true, + }, + { + desc: "too long name", + denom: "factory/osmo1t7egva48prqmzl59x5ngv4zx0dtrwewc9m7z44/adsfadsfadsfadsfadsfadsfadsfadsfadsfadsfadsfadsfadsfadsfadsfadsfadsfadsfadsfadsfadsfadsfadsfadsfadsfadsfadsfadsfadsfadsfadsfadsfadsfadsfadsfadsfadsfadsfadsfadsfadsf", + valid: false, + }, + } { + t.Run(tc.desc, func(t *testing.T) { + _, _, err := types.DeconstructDenom(tc.denom) + if tc.valid { + require.NoError(t, err) + } else { + require.Error(t, err) + } + }) + } +} diff --git a/types/errors.go b/types/errors.go new file mode 100644 index 0000000000..89f8f3e355 --- /dev/null +++ b/types/errors.go @@ -0,0 +1,17 @@ +package types + +// DONTCOVER + +import ( + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +// x/tokenfactory module sentinel errors +var ( + ErrDenomExists = sdkerrors.Register(ModuleName, 2, "denom already exists") + ErrUnauthorized = sdkerrors.Register(ModuleName, 3, "unauthorized account") + ErrInvalidDenom = sdkerrors.Register(ModuleName, 4, "invalid denom") + ErrInvalidCreator = sdkerrors.Register(ModuleName, 5, "invalid creator") + ErrInvalidAuthorityMetadata = sdkerrors.Register(ModuleName, 6, "invalid authority metadata") + ErrInvalidGenesis = sdkerrors.Register(ModuleName, 7, "invalid genesis") +) diff --git a/types/events.go b/types/events.go new file mode 100644 index 0000000000..2474298395 --- /dev/null +++ b/types/events.go @@ -0,0 +1,15 @@ +package types + +// event types +const ( + AttributeAmount = "amount" + AttributeCreator = "creator" + AttributeNonce = "nonce" + AttributeNewTokenDenom = "new_token_denom" + AttributeMintToAddress = "mint_to_address" + AttributeBurnFromAddress = "burn_from_address" + AttributeTransferFromAddress = "transfer_from_address" + AttributeTransferToAddress = "transfer_to_address" + AttributeDenom = "denom" + AttributeNewAdmin = "new_admin" +) diff --git a/types/genesis.go b/types/genesis.go new file mode 100644 index 0000000000..7c27455541 --- /dev/null +++ b/types/genesis.go @@ -0,0 +1,45 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +// this line is used by starport scaffolding # genesis/types/import + +// DefaultIndex is the default capability global index +const DefaultIndex uint64 = 1 + +// DefaultGenesis returns the default Capability genesis state +func DefaultGenesis() *GenesisState { + return &GenesisState{ + FactoryDenoms: []GenesisDenom{}, + } +} + +// Validate performs basic genesis state validation returning an error upon any +// failure. +func (gs GenesisState) Validate() error { + seenDenoms := map[string]bool{} + + for _, denom := range gs.GetFactoryDenoms() { + if seenDenoms[denom.GetDenom()] { + return sdkerrors.Wrapf(ErrInvalidGenesis, "duplicate denom: %s", denom.GetDenom()) + } + seenDenoms[denom.GetDenom()] = true + + _, _, err := DeconstructDenom(denom.GetDenom()) + if err != nil { + return err + } + + if denom.AuthorityMetadata.Admin != "" { + _, err = sdk.AccAddressFromBech32(denom.AuthorityMetadata.Admin) + if err != nil { + return sdkerrors.Wrapf(ErrInvalidAuthorityMetadata, "Invalid admin address (%s)", err) + } + } + } + + return nil +} diff --git a/types/genesis_test.go b/types/genesis_test.go new file mode 100644 index 0000000000..6b6e3bf86f --- /dev/null +++ b/types/genesis_test.go @@ -0,0 +1,139 @@ +package types_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/osmosis-labs/osmosis/v7/x/tokenfactory/types" +) + +func TestGenesisState_Validate(t *testing.T) { + for _, tc := range []struct { + desc string + genState *types.GenesisState + valid bool + }{ + { + desc: "default is valid", + genState: types.DefaultGenesis(), + valid: true, + }, + { + desc: "valid genesis state", + genState: &types.GenesisState{ + FactoryDenoms: []types.GenesisDenom{ + { + Denom: "factory/osmo1t7egva48prqmzl59x5ngv4zx0dtrwewc9m7z44/bitcoin", + AuthorityMetadata: types.DenomAuthorityMetadata{ + Admin: "osmo1t7egva48prqmzl59x5ngv4zx0dtrwewc9m7z44", + }, + }, + }, + }, + valid: true, + }, + { + desc: "different admin from creator", + genState: &types.GenesisState{ + FactoryDenoms: []types.GenesisDenom{ + { + Denom: "factory/osmo1t7egva48prqmzl59x5ngv4zx0dtrwewc9m7z44/bitcoin", + AuthorityMetadata: types.DenomAuthorityMetadata{ + Admin: "osmo1ft6e5esdtdegnvcr3djd3ftk4kwpcr6jrx5fj9", + }, + }, + }, + }, + valid: true, + }, + { + desc: "empty admin", + genState: &types.GenesisState{ + FactoryDenoms: []types.GenesisDenom{ + { + Denom: "factory/osmo1t7egva48prqmzl59x5ngv4zx0dtrwewc9m7z44/bitcoin", + AuthorityMetadata: types.DenomAuthorityMetadata{ + Admin: "", + }, + }, + }, + }, + valid: true, + }, + { + desc: "no admin", + genState: &types.GenesisState{ + FactoryDenoms: []types.GenesisDenom{ + { + Denom: "factory/osmo1t7egva48prqmzl59x5ngv4zx0dtrwewc9m7z44/bitcoin", + }, + }, + }, + valid: true, + }, + { + desc: "invalid admin", + genState: &types.GenesisState{ + FactoryDenoms: []types.GenesisDenom{ + { + Denom: "factory/osmo1t7egva48prqmzl59x5ngv4zx0dtrwewc9m7z44/bitcoin", + AuthorityMetadata: types.DenomAuthorityMetadata{ + Admin: "moose", + }, + }, + }, + }, + valid: false, + }, + { + desc: "multiple denoms", + genState: &types.GenesisState{ + FactoryDenoms: []types.GenesisDenom{ + { + Denom: "factory/osmo1t7egva48prqmzl59x5ngv4zx0dtrwewc9m7z44/bitcoin", + AuthorityMetadata: types.DenomAuthorityMetadata{ + Admin: "", + }, + }, + { + Denom: "factory/osmo1t7egva48prqmzl59x5ngv4zx0dtrwewc9m7z44/litecoin", + AuthorityMetadata: types.DenomAuthorityMetadata{ + Admin: "", + }, + }, + }, + }, + valid: true, + }, + { + desc: "duplicate denoms", + genState: &types.GenesisState{ + FactoryDenoms: []types.GenesisDenom{ + { + Denom: "factory/osmo1t7egva48prqmzl59x5ngv4zx0dtrwewc9m7z44/bitcoin", + AuthorityMetadata: types.DenomAuthorityMetadata{ + Admin: "", + }, + }, + { + Denom: "factory/osmo1t7egva48prqmzl59x5ngv4zx0dtrwewc9m7z44/bitcoin", + AuthorityMetadata: types.DenomAuthorityMetadata{ + Admin: "", + }, + }, + }, + }, + valid: false, + }, + } { + t.Run(tc.desc, func(t *testing.T) { + err := tc.genState.Validate() + if tc.valid { + require.NoError(t, err) + } else { + require.Error(t, err) + } + }) + } +} diff --git a/types/keys.go b/types/keys.go new file mode 100644 index 0000000000..fac4a6e39c --- /dev/null +++ b/types/keys.go @@ -0,0 +1,49 @@ +package types + +import ( + "strings" +) + +const ( + // ModuleName defines the module name + ModuleName = "tokenfactory" + + // StoreKey defines the primary module store key + StoreKey = ModuleName + + // RouterKey is the message route for slashing + RouterKey = ModuleName + + // QuerierRoute defines the module's query routing key + QuerierRoute = ModuleName + + // MemStoreKey defines the in-memory store key + MemStoreKey = "mem_tokenfactory" +) + +// KeySeparator is used to combine parts of the keys in the store +const KeySeparator = "|" + +var ( + DenomAuthorityMetadataKey = "authoritymetadata" + DenomsPrefixKey = "denoms" + CreatorPrefixKey = "creator" + AdminPrefixKey = "admin" +) + +// GetDenomPrefixStore returns the store prefix where all the data associated with a specific denom +// is stored +func GetDenomPrefixStore(denom string) []byte { + return []byte(strings.Join([]string{DenomsPrefixKey, denom, ""}, KeySeparator)) +} + +// GetCreatorsPrefix returns the store prefix where the list of the denoms created by a specific +// creator are stored +func GetCreatorPrefix(creator string) []byte { + return []byte(strings.Join([]string{CreatorPrefixKey, creator, ""}, KeySeparator)) +} + +// GetCreatorsPrefix returns the store prefix where a list of all creator addresses are stored +func GetCreatorsPrefix() []byte { + return []byte(strings.Join([]string{CreatorPrefixKey, ""}, KeySeparator)) +} diff --git a/types/msgs.go b/types/msgs.go new file mode 100644 index 0000000000..753325e857 --- /dev/null +++ b/types/msgs.go @@ -0,0 +1,216 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +// constants +const ( + TypeMsgCreateDenom = "create_denom" + TypeMsgMint = "mint" + TypeMsgBurn = "burn" + TypeMsgForceTransfer = "force_transfer" + TypeMsgChangeAdmin = "change_admin" +) + +var _ sdk.Msg = &MsgCreateDenom{} + +// NewMsgCreateDenom creates a msg to create a new denom +func NewMsgCreateDenom(sender, nonce string) *MsgCreateDenom { + return &MsgCreateDenom{ + Sender: sender, + Nonce: nonce, + } +} + +func (m MsgCreateDenom) Route() string { return RouterKey } +func (m MsgCreateDenom) Type() string { return TypeMsgCreateDenom } +func (m MsgCreateDenom) ValidateBasic() error { + _, err := sdk.AccAddressFromBech32(m.Sender) + if err != nil { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "Invalid sender address (%s)", err) + } + + _, err = GetTokenDenom(m.Sender, m.Nonce) + if err != nil { + return sdkerrors.Wrap(ErrInvalidDenom, err.Error()) + } + + return nil +} + +func (m MsgCreateDenom) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&m)) +} + +func (m MsgCreateDenom) GetSigners() []sdk.AccAddress { + sender, _ := sdk.AccAddressFromBech32(m.Sender) + return []sdk.AccAddress{sender} +} + +var _ sdk.Msg = &MsgMint{} + +// NewMsgMint creates a message to mint tokens +func NewMsgMint(sender string, amount sdk.Coin, mintTo string) *MsgMint { + return &MsgMint{ + Sender: sender, + Amount: amount, + MintToAddress: mintTo, + } +} + +func (m MsgMint) Route() string { return RouterKey } +func (m MsgMint) Type() string { return TypeMsgMint } +func (m MsgMint) ValidateBasic() error { + _, err := sdk.AccAddressFromBech32(m.Sender) + if err != nil { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "Invalid sender address (%s)", err) + } + + _, err = sdk.AccAddressFromBech32(m.MintToAddress) + if err != nil { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "Invalid recipient address (%s)", err) + } + + if !m.Amount.IsValid() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, m.Amount.String()) + } + + return nil +} + +func (m MsgMint) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&m)) +} + +func (m MsgMint) GetSigners() []sdk.AccAddress { + sender, _ := sdk.AccAddressFromBech32(m.Sender) + return []sdk.AccAddress{sender} +} + +var _ sdk.Msg = &MsgBurn{} + +// NewMsgBurn creates a message to burn tokens +func NewMsgBurn(sender string, amount sdk.Coin, burnFrom string) *MsgBurn { + return &MsgBurn{ + Sender: sender, + Amount: amount, + BurnFromAddress: burnFrom, + } +} + +func (m MsgBurn) Route() string { return RouterKey } +func (m MsgBurn) Type() string { return TypeMsgBurn } +func (m MsgBurn) ValidateBasic() error { + _, err := sdk.AccAddressFromBech32(m.Sender) + if err != nil { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "Invalid sender address (%s)", err) + } + + _, err = sdk.AccAddressFromBech32(m.BurnFromAddress) + if err != nil { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "Invalid address (%s)", err) + } + + if !m.Amount.IsValid() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, m.Amount.String()) + } + + return nil +} + +func (m MsgBurn) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&m)) +} + +func (m MsgBurn) GetSigners() []sdk.AccAddress { + sender, _ := sdk.AccAddressFromBech32(m.Sender) + return []sdk.AccAddress{sender} +} + +// var _ sdk.Msg = &MsgForceTransfer{} + +// // NewMsgForceTransfer creates a transfer funds from one account to another +// func NewMsgForceTransfer(sender string, amount sdk.Coin, fromAddr, toAddr string) *MsgForceTransfer { +// return &MsgForceTransfer{ +// Sender: sender, +// Amount: amount, +// TransferFromAddress: fromAddr, +// TransferToAddress: toAddr, +// } +// } + +// func (m MsgForceTransfer) Route() string { return RouterKey } +// func (m MsgForceTransfer) Type() string { return TypeMsgForceTransfer } +// func (m MsgForceTransfer) ValidateBasic() error { +// _, err := sdk.AccAddressFromBech32(m.Sender) +// if err != nil { +// return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "Invalid sender address (%s)", err) +// } + +// _, err = sdk.AccAddressFromBech32(m.TransferFromAddress) +// if err != nil { +// return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "Invalid address (%s)", err) +// } +// _, err = sdk.AccAddressFromBech32(m.TransferToAddress) +// if err != nil { +// return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "Invalid address (%s)", err) +// } + +// if !m.Amount.IsValid() { +// return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, m.Amount.String()) +// } + +// return nil +// } + +// func (m MsgForceTransfer) GetSignBytes() []byte { +// return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&m)) +// } + +// func (m MsgForceTransfer) GetSigners() []sdk.AccAddress { +// sender, _ := sdk.AccAddressFromBech32(m.Sender) +// return []sdk.AccAddress{sender} +// } + +var _ sdk.Msg = &MsgChangeAdmin{} + +// NewMsgChangeAdmin creates a message to burn tokens +func NewMsgChangeAdmin(sender, denom, newAdmin string) *MsgChangeAdmin { + return &MsgChangeAdmin{ + Sender: sender, + Denom: denom, + NewAdmin: newAdmin, + } +} + +func (m MsgChangeAdmin) Route() string { return RouterKey } +func (m MsgChangeAdmin) Type() string { return TypeMsgChangeAdmin } +func (m MsgChangeAdmin) ValidateBasic() error { + _, err := sdk.AccAddressFromBech32(m.Sender) + if err != nil { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "Invalid sender address (%s)", err) + } + + _, err = sdk.AccAddressFromBech32(m.NewAdmin) + if err != nil { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "Invalid address (%s)", err) + } + + _, _, err = DeconstructDenom(m.Denom) + if err != nil { + return err + } + + return nil +} + +func (m MsgChangeAdmin) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&m)) +} + +func (m MsgChangeAdmin) GetSigners() []sdk.AccAddress { + sender, _ := sdk.AccAddressFromBech32(m.Sender) + return []sdk.AccAddress{sender} +}