Skip to content

Commit

Permalink
feat: add token factory module (#1318)
Browse files Browse the repository at this point in the history
# Related Github tickets

- VolumeFi#2440

# Background

This change adds a token factory module that extends Paloma with the
following features:

- New tokens may be created by anyone
- Tokens can only be minted by the admin of the token (by default, the
creator of a token is the admin)
- Tokens can only be burned by the admin of the token
- All data is respected for genesis import/export
- Skyway registrations against user tokens do not require governance
- Tokens can be bridged outside of Paloma
- Creating a token costs 10 GRAIN

# Testing completed

> [!WARNING]  
> While most of the code is greatly inspired by the COSMWASM
tokenfactory module and should be battle tested, I did not have time to
include proper e2e tests to cover all Paloma customizations. Successful
bridging still needs smoke testing.

- [ ] test coverage exists or has been added/updated
- [x] tested in a private testnet

# Breaking changes

- [x] I have checked my code for breaking changes
- [x] If there are breaking changes, there is a supporting migration.
  • Loading branch information
byte-bandit authored Dec 13, 2024
1 parent 1adfd7c commit 06191a2
Show file tree
Hide file tree
Showing 130 changed files with 14,002 additions and 2,376 deletions.
66 changes: 44 additions & 22 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@ import (
skywayclient "github.com/palomachain/paloma/v2/x/skyway/client"
skywaymodulekeeper "github.com/palomachain/paloma/v2/x/skyway/keeper"
skywaymoduletypes "github.com/palomachain/paloma/v2/x/skyway/types"
"github.com/palomachain/paloma/v2/x/tokenfactory"
tokenfactorymodulekeeper "github.com/palomachain/paloma/v2/x/tokenfactory/keeper"
tokenfactorymoduletypes "github.com/palomachain/paloma/v2/x/tokenfactory/types"
treasurymodule "github.com/palomachain/paloma/v2/x/treasury"
treasuryclient "github.com/palomachain/paloma/v2/x/treasury/client"
treasurymodulekeeper "github.com/palomachain/paloma/v2/x/treasury/keeper"
Expand Down Expand Up @@ -178,19 +181,20 @@ var (

// module account permissions
maccPerms = map[string][]string{
authtypes.FeeCollectorName: nil,
distrtypes.ModuleName: nil,
minttypes.ModuleName: {authtypes.Minter},
stakingtypes.BondedPoolName: {authtypes.Burner, authtypes.Staking},
stakingtypes.NotBondedPoolName: {authtypes.Burner, authtypes.Staking},
govtypes.ModuleName: {authtypes.Burner},
skywaymoduletypes.ModuleName: {authtypes.Minter, authtypes.Burner},
ibctransfertypes.ModuleName: {authtypes.Minter, authtypes.Burner},
ibcfeetypes.ModuleName: nil,
icatypes.ModuleName: nil,
wasmtypes.ModuleName: {authtypes.Burner},
treasurymoduletypes.ModuleName: {authtypes.Burner, authtypes.Minter},
palomamoduletypes.ModuleName: nil,
authtypes.FeeCollectorName: nil,
distrtypes.ModuleName: nil,
minttypes.ModuleName: {authtypes.Minter},
stakingtypes.BondedPoolName: {authtypes.Burner, authtypes.Staking},
stakingtypes.NotBondedPoolName: {authtypes.Burner, authtypes.Staking},
govtypes.ModuleName: {authtypes.Burner},
skywaymoduletypes.ModuleName: {authtypes.Minter, authtypes.Burner},
ibctransfertypes.ModuleName: {authtypes.Minter, authtypes.Burner},
ibcfeetypes.ModuleName: nil,
icatypes.ModuleName: nil,
wasmtypes.ModuleName: {authtypes.Burner},
treasurymoduletypes.ModuleName: {authtypes.Burner, authtypes.Minter},
palomamoduletypes.ModuleName: nil,
tokenfactorymoduletypes.ModuleName: {authtypes.Minter, authtypes.Burner},
}
)

Expand Down Expand Up @@ -250,15 +254,16 @@ type App struct {
ICAHostKeeper icahostkeeper.Keeper
TransferKeeper ibctransferkeeper.Keeper

SchedulerKeeper schedulermodulekeeper.Keeper
ConsensusKeeper consensusmodulekeeper.Keeper
ValsetKeeper valsetmodulekeeper.Keeper
PalomaKeeper palomamodulekeeper.Keeper
TreasuryKeeper treasurymodulekeeper.Keeper
EvmKeeper evmmodulekeeper.Keeper
SkywayKeeper skywaymodulekeeper.Keeper
wasmKeeper wasmkeeper.Keeper
MetrixKeeper metrixmodulekeeper.Keeper
SchedulerKeeper schedulermodulekeeper.Keeper
ConsensusKeeper consensusmodulekeeper.Keeper
ValsetKeeper valsetmodulekeeper.Keeper
PalomaKeeper palomamodulekeeper.Keeper
TreasuryKeeper treasurymodulekeeper.Keeper
EvmKeeper evmmodulekeeper.Keeper
SkywayKeeper skywaymodulekeeper.Keeper
wasmKeeper wasmkeeper.Keeper
MetrixKeeper metrixmodulekeeper.Keeper
TokenFactoryKeeper tokenfactorymodulekeeper.Keeper

// ModuleManager is the module manager
ModuleManager *module.Manager
Expand Down Expand Up @@ -342,6 +347,7 @@ func New(
wasmtypes.StoreKey,
palomamoduletypes.StoreKey,
authzkeeper.StoreKey,
tokenfactorymoduletypes.StoreKey,
)
tkeys := storetypes.NewTransientStoreKeys(paramstypes.TStoreKey)
memKeys := storetypes.NewMemoryStoreKeys(
Expand All @@ -353,6 +359,7 @@ func New(
treasurymoduletypes.MemStoreKey,
palomamoduletypes.MemStoreKey,
metrixmoduletypes.MemStoreKey,
tokenfactorymoduletypes.MemStoreKey,
)

app := &App{
Expand Down Expand Up @@ -618,6 +625,14 @@ func New(
app.EvmKeeper,
}

app.TokenFactoryKeeper = tokenfactorymodulekeeper.NewKeeper(
keys[tokenfactorymoduletypes.StoreKey],
app.GetSubspace(tokenfactorymoduletypes.ModuleName),
app.AccountKeeper,
app.BankKeeper,
app.DistrKeeper,
authorityAddress)

app.SkywayKeeper = skywaymodulekeeper.NewKeeper(
appCodec,
app.AccountKeeper,
Expand All @@ -629,6 +644,7 @@ func New(
app.EvmKeeper,
app.ConsensusKeeper,
app.PalomaKeeper,
app.TokenFactoryKeeper,
skywaymodulekeeper.NewSkywayStoreGetter(keys[skywaymoduletypes.StoreKey]),
authorityAddress,
authcodec.NewBech32Codec(chainparams.ValidatorAddressPrefix),
Expand Down Expand Up @@ -804,6 +820,7 @@ func New(
skywayModule := skywaymodule.NewAppModule(appCodec, app.SkywayKeeper, app.BankKeeper, app.GetSubspace(skywaymoduletypes.ModuleName), libcons.New(app.ValsetKeeper.GetCurrentSnapshot, appCodec))
treasuryModule := treasurymodule.NewAppModule(appCodec, app.TreasuryKeeper, app.AccountKeeper, app.BankKeeper)
metrixModule := metrix.NewAppModule(appCodec, app.MetrixKeeper)
tokenfactorymodule := tokenfactory.NewAppModule(app.TokenFactoryKeeper, app.AccountKeeper, app.BankKeeper)

stakingAppModule := staking.NewAppModule(appCodec, app.StakingKeeper, app.AccountKeeper, app.BankKeeper, app.GetSubspace(stakingtypes.ModuleName))

Expand Down Expand Up @@ -836,6 +853,7 @@ func New(
palomaModule,
treasuryModule,
metrixModule,
tokenfactorymodule,
wasm.NewAppModule(appCodec, &app.wasmKeeper, app.StakingKeeper, app.AccountKeeper, app.BankKeeper, app.MsgServiceRouter(), app.GetSubspace(wasmtypes.ModuleName)),
ibc.NewAppModule(app.IBCKeeper),
transfer.NewAppModule(app.TransferKeeper),
Expand Down Expand Up @@ -894,6 +912,7 @@ func New(
ibcfeetypes.ModuleName,
consensusparamtypes.ModuleName,
metrixmoduletypes.ModuleName,
tokenfactorymoduletypes.ModuleName,
)

app.ModuleManager.SetOrderEndBlockers(
Expand Down Expand Up @@ -927,6 +946,7 @@ func New(
treasurymoduletypes.ModuleName,
consensusparamtypes.ModuleName,
metrixmoduletypes.ModuleName,
tokenfactorymoduletypes.ModuleName,
)

// NOTE: The genutils module must occur after staking so that pools are
Expand Down Expand Up @@ -966,6 +986,7 @@ func New(
treasurymoduletypes.ModuleName,
consensusparamtypes.ModuleName,
metrixmoduletypes.ModuleName,
tokenfactorymoduletypes.ModuleName,
)

app.configurator = module.NewConfigurator(appCodec, app.MsgServiceRouter(), app.GRPCQueryRouter())
Expand Down Expand Up @@ -1238,6 +1259,7 @@ func initParamsKeeper(appCodec codec.BinaryCodec, legacyAmino *codec.LegacyAmino
paramsKeeper.Subspace(evmmoduletypes.ModuleName)
paramsKeeper.Subspace(skywaymoduletypes.ModuleName).WithKeyTable(skywaymoduletypes.ParamKeyTable())
paramsKeeper.Subspace(metrixmoduletypes.ModuleName)
paramsKeeper.Subspace(tokenfactorymoduletypes.ModuleName)

paramsKeeper.Subspace(ibcexported.ModuleName).WithKeyTable(keyTable)
paramsKeeper.Subspace(ibctransfertypes.ModuleName).WithKeyTable(ibctransfertypes.ParamKeyTable())
Expand Down
12 changes: 12 additions & 0 deletions app/upgrades.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
metrixmoduletypes "github.com/palomachain/paloma/v2/x/metrix/types"
palomamoduletypes "github.com/palomachain/paloma/v2/x/paloma/types"
skywaymoduletypes "github.com/palomachain/paloma/v2/x/skyway/types"
tokenfactorymoduletypes "github.com/palomachain/paloma/v2/x/tokenfactory/types"
)

var minCommissionRate = math.LegacyMustNewDecFromStr("0.05")
Expand Down Expand Up @@ -172,6 +173,17 @@ func (app *App) RegisterUpgradeHandlers(semverVersion string) {
app.SetStoreLoader(upgradetypes.UpgradeStoreLoader(upgradeInfo.Height, &storeUpgrades))
}

if upgradeInfo.Name == "v2.4.0" && !app.UpgradeKeeper.IsSkipHeight(upgradeInfo.Height) {
storeUpgrades := storetypes.StoreUpgrades{
Added: []string{
tokenfactorymoduletypes.StoreKey,
},
}

// configure store loader that checks if version == upgradeHeight and applies store upgrades
app.SetStoreLoader(upgradetypes.UpgradeStoreLoader(upgradeInfo.Height, &storeUpgrades))
}

app.UpgradeKeeper.SetUpgradeHandler(semverVersion, func(ctx context.Context, plan upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) {
return app.ModuleManager.RunMigrations(ctx, app.configurator, fromVM)
})
Expand Down
22 changes: 22 additions & 0 deletions proto/palomachain/paloma/skyway/msgs.proto
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ service Msg {
"/palomachain/paloma/skyway/light-node-sale-claim";
}

rpc SetERC20ToTokenDenom(MsgSetERC20ToTokenDenom)
returns (google.protobuf.Empty) {
option (google.api.http).post =
"/palomachain/paloma/skyway/erc20-to-token-denom";
}

rpc OverrideNonceProposal(MsgNonceOverrideProposal) returns (google.protobuf.Empty);
}

Expand Down Expand Up @@ -303,3 +309,19 @@ message MsgEstimateBatchGas {
string eth_signer = 4;
uint64 estimate = 5;
}

// MsgSetERC20ToTokenDenom is a message to set the mapping between an ERC20 token
// and a denom created by the token factory.
// Needs admin rights on the token to set the mapping.
message MsgSetERC20ToTokenDenom {
option (cosmos.msg.v1.signer) = "metadata";

palomachain.paloma.valset.MsgMetadata metadata = 1
[
(gogoproto.moretags) = "yaml:\"metadata\"",
(gogoproto.nullable) = false
];
string denom = 2 [ (gogoproto.moretags) = "yaml:\"denom\"" ];
string chain_reference_id = 3 [ (gogoproto.moretags) = "yaml:\"chain_reference_id\"" ];
string erc20 = 4 [ (gogoproto.moretags) = "yaml:\"erc20\"" ];
}
21 changes: 21 additions & 0 deletions proto/palomachain/paloma/tokenfactory/authorityMetadata.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
syntax = "proto3";
package palomachain.paloma.tokenfactory;

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

option go_package = "github.com/palomachain/paloma/v2/x/tokenfactory/types";

// DenomAuthorityMetadata specifies metadata for addresses that have specific
// capabilities over a token factory denom. Right now there is only one Admin
// permission, but is planned to be extended to the future.
message DenomAuthorityMetadata {
option (gogoproto.equal) = true;

// Can be empty for no admin, or a valid paloma address
string admin = 1 [
(gogoproto.moretags) = "yaml:\"admin\"",
(cosmos_proto.scalar) = "cosmos.AddressString"
];
}
32 changes: 32 additions & 0 deletions proto/palomachain/paloma/tokenfactory/genesis.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
syntax = "proto3";
package palomachain.paloma.tokenfactory;

import "gogoproto/gogo.proto";
import "palomachain/paloma/tokenfactory/authorityMetadata.proto";
import "palomachain/paloma/tokenfactory/params.proto";

option go_package = "github.com/palomachain/paloma/v2/x/tokenfactory/types";

// GenesisState defines the tokenfactory module's genesis state.
message GenesisState {
// params defines the paramaters of the module.
Params params = 1 [ (gogoproto.nullable) = false ];

repeated GenesisDenom factory_denoms = 2 [
(gogoproto.moretags) = "yaml:\"factory_denoms\"",
(gogoproto.nullable) = false
];
}

// GenesisDenom defines a tokenfactory denom that is defined within genesis
// state. The structure contains DenomAuthorityMetadata which defines the
// denom's admin.
message GenesisDenom {
option (gogoproto.equal) = true;

string denom = 1 [ (gogoproto.moretags) = "yaml:\"denom\"" ];
DenomAuthorityMetadata authority_metadata = 2 [
(gogoproto.moretags) = "yaml:\"authority_metadata\"",
(gogoproto.nullable) = false
];
}
18 changes: 18 additions & 0 deletions proto/palomachain/paloma/tokenfactory/params.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
syntax = "proto3";
package palomachain.paloma.tokenfactory;

import "gogoproto/gogo.proto";
import "cosmos_proto/cosmos.proto";
import "cosmos/base/v1beta1/coin.proto";
import "palomachain/paloma/tokenfactory/authorityMetadata.proto";

option go_package = "github.com/palomachain/paloma/v2/x/tokenfactory/types";

// Params defines the parameters for the tokenfactory module.
message Params {
repeated cosmos.base.v1beta1.Coin denom_creation_fee = 1 [
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins",
(gogoproto.moretags) = "yaml:\"denom_creation_fee\"",
(gogoproto.nullable) = false
];
}
72 changes: 72 additions & 0 deletions proto/palomachain/paloma/tokenfactory/query.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
syntax = "proto3";
package palomachain.paloma.tokenfactory;

import "gogoproto/gogo.proto";
import "google/api/annotations.proto";
import "cosmos/base/query/v1beta1/pagination.proto";
import "palomachain/paloma/tokenfactory/authorityMetadata.proto";
import "palomachain/paloma/tokenfactory/params.proto";

option go_package = "github.com/palomachain/paloma/v2/x/tokenfactory/types";

// Query defines the gRPC querier service.
service Query {
// Params defines a gRPC query method that returns the tokenfactory module's
// parameters.
rpc Params(QueryParamsRequest) returns (QueryParamsResponse) {
option (google.api.http).get = "/palomachain/paloma/tokenfactory/params";
}

// DenomAuthorityMetadata defines a gRPC query method for fetching
// DenomAuthorityMetadata for a particular denom.
rpc DenomAuthorityMetadata(QueryDenomAuthorityMetadataRequest)
returns (QueryDenomAuthorityMetadataResponse) {
option (google.api.http).get =
"/palomachain/paloma/tokenfactory/denoms/{denom}/authority_metadata";
}

// DenomsFromCreator defines a gRPC query method for fetching all
// denominations created by a specific admin/creator.
rpc DenomsFromCreator(QueryDenomsFromCreatorRequest)
returns (QueryDenomsFromCreatorResponse) {
option (google.api.http).get =
"/palomachain/paloma/tokenfactory/denoms_from_creator/{creator}";
}
}

// QueryParamsRequest is the request type for the Query/Params RPC method.
message QueryParamsRequest {}

// QueryParamsResponse is the response type for the Query/Params RPC method.
message QueryParamsResponse {
// params defines the parameters of the module.
Params params = 1 [ (gogoproto.nullable) = false ];
}

// QueryDenomAuthorityMetadataRequest defines the request structure for the
// DenomAuthorityMetadata gRPC query.
message QueryDenomAuthorityMetadataRequest {
string denom = 1 [ (gogoproto.moretags) = "yaml:\"denom\"" ];
}

// QueryDenomAuthorityMetadataResponse defines the response structure for the
// DenomAuthorityMetadata gRPC query.
message QueryDenomAuthorityMetadataResponse {
DenomAuthorityMetadata authority_metadata = 1 [
(gogoproto.moretags) = "yaml:\"authority_metadata\"",
(gogoproto.nullable) = false
];
}

// QueryDenomsFromCreatorRequest defines the request structure for the
// DenomsFromCreator gRPC query.
message QueryDenomsFromCreatorRequest {
string creator = 1 [ (gogoproto.moretags) = "yaml:\"creator\"" ];
}

// QueryDenomsFromCreatorRequest defines the response structure for the
// DenomsFromCreator gRPC query.
message QueryDenomsFromCreatorResponse {
repeated string denoms = 1 [ (gogoproto.moretags) = "yaml:\"denoms\"" ];
}

Loading

0 comments on commit 06191a2

Please sign in to comment.