diff --git a/demo/app/app.go b/demo/app/app.go index 05fcb885..0d82bea3 100644 --- a/demo/app/app.go +++ b/demo/app/app.go @@ -129,6 +129,9 @@ import ( "github.com/osmosis-labs/mesh-security-sdk/x/meshsecurity" meshseckeeper "github.com/osmosis-labs/mesh-security-sdk/x/meshsecurity/keeper" meshsectypes "github.com/osmosis-labs/mesh-security-sdk/x/meshsecurity/types" + meshsecprov "github.com/osmosis-labs/mesh-security-sdk/x/meshsecurityprovider" + meshsecprovkeeper "github.com/osmosis-labs/mesh-security-sdk/x/meshsecurityprovider/keeper" + meshsecprovtypes "github.com/osmosis-labs/mesh-security-sdk/x/meshsecurityprovider/types" ) const appName = "MeshApp" @@ -200,6 +203,7 @@ var ( ica.AppModuleBasic{}, ibcfee.AppModuleBasic{}, meshsecurity.AppModuleBasic{}, + meshsecprov.AppModuleBasic{}, ) // module account permissions @@ -263,6 +267,7 @@ type MeshApp struct { TransferKeeper ibctransferkeeper.Keeper WasmKeeper wasmkeeper.Keeper MeshSecKeeper *meshseckeeper.Keeper + MeshSecProvKeeper *meshsecprovkeeper.Keeper ScopedIBCKeeper capabilitykeeper.ScopedKeeper ScopedICAHostKeeper capabilitykeeper.ScopedKeeper @@ -314,6 +319,7 @@ func NewMeshApp( wasmtypes.StoreKey, icahosttypes.StoreKey, icacontrollertypes.StoreKey, meshsectypes.StoreKey, + meshsecprovtypes.StoreKey, ) tkeys := sdk.NewTransientStoreKeys(paramstypes.TStoreKey) @@ -417,6 +423,15 @@ func NewMeshApp( authtypes.NewModuleAddress(govtypes.ModuleName).String(), ) + app.MeshSecProvKeeper = meshsecprovkeeper.NewKeeper( + appCodec, + keys[meshsecprovtypes.StoreKey], + authtypes.NewModuleAddress(govtypes.ModuleName).String(), + app.BankKeeper, + app.WasmKeeper, + app.StakingKeeper, + ) + app.SlashingKeeper = slashingkeeper.NewKeeper( appCodec, legacyAmino, @@ -589,6 +604,7 @@ func NewMeshApp( nested, // append our custom message handler for mesh-security meshseckeeper.NewDefaultCustomMsgHandler(app.MeshSecKeeper), + meshsecprovkeeper.CustomMessageDecorator(app.MeshSecProvKeeper), ) }) wasmOpts = append(wasmOpts, meshMessageHandler, @@ -694,6 +710,7 @@ func NewMeshApp( ibcfee.NewAppModule(app.IBCFeeKeeper), ica.NewAppModule(&app.ICAControllerKeeper, &app.ICAHostKeeper), meshsecurity.NewAppModule(appCodec, app.MeshSecKeeper), + meshsecprov.NewAppModule(app.MeshSecProvKeeper), crisis.NewAppModule(app.CrisisKeeper, skipGenesisInvariants, app.GetSubspace(crisistypes.ModuleName)), // always be last to make sure that it checks for all invariants and not only part of them ) @@ -715,6 +732,7 @@ func NewMeshApp( ibcfeetypes.ModuleName, wasmtypes.ModuleName, meshsectypes.ModuleName, + meshsecprovtypes.ModuleName, ) app.ModuleManager.SetOrderEndBlockers( @@ -731,6 +749,7 @@ func NewMeshApp( ibcfeetypes.ModuleName, wasmtypes.ModuleName, meshsectypes.ModuleName, // last to capture all chain events + meshsecprovtypes.ModuleName, ) // NOTE: The genutils module must occur after staking so that pools are @@ -755,6 +774,7 @@ func NewMeshApp( // wasm after ibc transfer wasmtypes.ModuleName, meshsectypes.ModuleName, + meshsecprovtypes.ModuleName, } app.ModuleManager.SetOrderInitGenesis(genesisModuleOrder...) app.ModuleManager.SetOrderExportGenesis(genesisModuleOrder...) diff --git a/demo/build/meshd b/demo/build/meshd new file mode 100755 index 00000000..c57f7961 Binary files /dev/null and b/demo/build/meshd differ diff --git a/tests/e2e/e2e.go b/tests/e2e/e2e.go index b8340734..cab70123 100644 --- a/tests/e2e/e2e.go +++ b/tests/e2e/e2e.go @@ -99,6 +99,7 @@ type example struct { ConsumerChain *ibctesting.TestChain ProviderChain *ibctesting.TestChain ConsumerApp *app.MeshApp + ProviderApp *app.MeshApp IbcPath *ibctesting.Path ProviderDenom string ConsumerDenom string @@ -114,6 +115,7 @@ func setupExampleChains(t *testing.T) example { ConsumerChain: consChain, ProviderChain: provChain, ConsumerApp: consChain.App.(*app.MeshApp), + ProviderApp: provChain.App.(*app.MeshApp), IbcPath: ibctesting.NewPath(consChain, provChain), ProviderDenom: sdk.DefaultBondDenom, ConsumerDenom: sdk.DefaultBondDenom, @@ -132,7 +134,7 @@ func setupMeshSecurity(t *testing.T, x example) (*TestConsumerClient, ConsumerCo x.ConsumerChain.DefaultMsgFees = sdk.NewCoins(sdk.NewCoin(x.ConsumerDenom, math.NewInt(1_000_000))) providerCli := NewProviderClient(t, x.ProviderChain) - providerContracts := providerCli.BootstrapContracts(x.IbcPath.EndpointA.ConnectionID, converterPortID) + providerContracts := providerCli.BootstrapContracts(x.ProviderApp, x.IbcPath.EndpointA.ConnectionID, converterPortID) // setup ibc control path: consumer -> provider (direction matters) x.IbcPath.EndpointB.ChannelConfig = &ibctesting2.ChannelConfig{ diff --git a/tests/e2e/mvp_test.go b/tests/e2e/mvp_test.go index 71e615c6..06981fe3 100644 --- a/tests/e2e/mvp_test.go +++ b/tests/e2e/mvp_test.go @@ -59,8 +59,8 @@ func TestMVP(t *testing.T) { // provider chain // ============== // Deposit - A user deposits the vault denom to provide some collateral to their account - execMsg := `{"bond":{}}` - providerCli.MustExecVault(execMsg, sdk.NewInt64Coin(x.ProviderDenom, 100_000_000)) + execMsg := fmt.Sprintf(`{"bond":{"amount":{"denom":"%s", "amount":"100000000"}}}`, x.ProviderDenom) + providerCli.MustExecVault(execMsg) // then query contract state assert.Equal(t, 100_000_000, providerCli.QueryVaultFreeBalance()) diff --git a/tests/e2e/slashing_test.go b/tests/e2e/slashing_test.go index d068a0ea..f9abdbab 100644 --- a/tests/e2e/slashing_test.go +++ b/tests/e2e/slashing_test.go @@ -21,8 +21,8 @@ func TestSlashingScenario1(t *testing.T) { // Provider chain // ============== // Deposit - A user deposits the vault denom to provide some collateral to their account - execMsg := `{"bond":{}}` - providerCli.MustExecVault(execMsg, sdk.NewInt64Coin(x.ProviderDenom, 200_000_000)) + execMsg := fmt.Sprintf(`{"bond":{"amount":{"denom":"%s", "amount":"200000000"}}}`, x.ProviderDenom) + providerCli.MustExecVault(execMsg) // Stake Locally - A user triggers a local staking action to a chosen validator. myLocalValidatorAddr := sdk.ValAddress(x.ProviderChain.Vals.Validators[0].Address).String() @@ -121,8 +121,8 @@ func TestSlashingScenario2(t *testing.T) { // Provider chain // ============== // Deposit - A user deposits the vault denom to provide some collateral to their account - execMsg := `{"bond":{}}` - providerCli.MustExecVault(execMsg, sdk.NewInt64Coin(x.ProviderDenom, 200_000_000)) + execMsg := fmt.Sprintf(`{"bond":{"amount":{"denom":"%s", "amount":"200000000"}}}`, x.ProviderDenom) + providerCli.MustExecVault(execMsg) // Stake Locally - A user triggers a local staking action to a chosen validator. myLocalValidatorAddr := sdk.ValAddress(x.ProviderChain.Vals.Validators[0].Address).String() @@ -208,8 +208,8 @@ func TestSlashingScenario3(t *testing.T) { // Provider chain // ============== // Deposit - A user deposits the vault denom to provide some collateral to their account - execMsg := `{"bond":{}}` - providerCli.MustExecVault(execMsg, sdk.NewInt64Coin(x.ProviderDenom, 200_000_000)) + execMsg := fmt.Sprintf(`{"bond":{"amount":{"denom":"%s", "amount":"200000000"}}}`, x.ProviderDenom) + providerCli.MustExecVault(execMsg) // Stake Locally - A user triggers a local staking action to a chosen validator. myLocalValidatorAddr := sdk.ValAddress(x.ProviderChain.Vals.Validators[0].Address).String() diff --git a/tests/e2e/test_client.go b/tests/e2e/test_client.go index 392f3077..a67917de 100644 --- a/tests/e2e/test_client.go +++ b/tests/e2e/test_client.go @@ -78,8 +78,7 @@ type ProviderContracts struct { externalStaking sdk.AccAddress } -func (p *TestProviderClient) BootstrapContracts(connId, portID string) ProviderContracts { - var ( +func (p *TestProviderClient) BootstrapContracts(provApp *app.MeshApp, connId, portID string) ProviderContracts { var ( unbondingPeriod = 21 * 24 * 60 * 60 // 21 days - make configurable? localSlashRatioDoubleSign = "0.20" localSlashRatioOffline = "0.10" @@ -95,7 +94,10 @@ func (p *TestProviderClient) BootstrapContracts(connId, portID string) ProviderC nativeInitMsg := []byte(fmt.Sprintf(`{"denom": %q, "proxy_code_id": %d, "slash_ratio_dsign": %q, "slash_ratio_offline": %q }`, localTokenDenom, proxyCodeID, localSlashRatioDoubleSign, localSlashRatioOffline)) initMsg := []byte(fmt.Sprintf(`{"denom": %q, "local_staking": {"code_id": %d, "msg": %q}}`, localTokenDenom, nativeStakingCodeID, base64.StdEncoding.EncodeToString(nativeInitMsg))) vaultContract := InstantiateContract(p.t, p.chain, vaultCodeID, initMsg) - + ctx := p.chain.GetContext() + params := provApp.MeshSecProvKeeper.GetParams(ctx) + params.VaultAddress = vaultContract.String() + provApp.MeshSecProvKeeper.SetParams(ctx, params) // external staking extStakingCodeID := p.chain.StoreCodeFile(buildPathToWasm("mesh_external_staking.wasm")).CodeID initMsg = []byte(fmt.Sprintf( diff --git a/tests/starship/mvp_test.go b/tests/starship/mvp_test.go index 524a797e..a0e29634 100644 --- a/tests/starship/mvp_test.go +++ b/tests/starship/mvp_test.go @@ -52,8 +52,8 @@ func Test2WayContract(t *testing.T) { // ============== // Deposit - A user deposits the vault denom to provide some collateral to their account fmt.Println("provider chain: deposit vault denom to provide some collateral to account") - execMsg := `{"bond":{}}` - vault, err := providerClient1.MustExecVault(execMsg, sdk.NewInt64Coin(providerClient1.Chain.Denom, 100_000_000)) + execMsg := fmt.Sprintf(`{"bond":{"amount":{"denom":"%s", "amount":"100000000"}}}`, providerClient1.Chain.Denom) + vault, err := providerClient1.MustExecVault(execMsg) require.NoError(t, err) require.NotEmpty(t, vault) @@ -130,8 +130,8 @@ func Test2WayContract(t *testing.T) { // ============== // Deposit - A user deposits the vault denom to provide some collateral to their account fmt.Println("provider chain: deposit vault denom to provide some collateral to account") - execMsg = `{"bond":{}}` - vault, err = providerClient2.MustExecVault(execMsg, sdk.NewInt64Coin(providerClient2.Chain.Denom, 100_000_000)) + execMsg = fmt.Sprintf(`{"bond":{"amount":{"denom":"%s", "amount":"100000000"}}}`, providerClient2.Chain.Denom) + vault, err = providerClient2.MustExecVault(execMsg) require.NoError(t, err) require.NotEmpty(t, vault) diff --git a/tests/testdata/copy_local_wasm.sh b/tests/testdata/copy_local_wasm.sh index 9754f26a..b85023cd 100755 --- a/tests/testdata/copy_local_wasm.sh +++ b/tests/testdata/copy_local_wasm.sh @@ -4,14 +4,14 @@ command -v shellcheck > /dev/null && shellcheck "$0" echo "DEV-only: copy from local built instead of downloading" -for contract in external_staking mesh_converter mesh_native_staking mesh_native_staking_proxy mesh_simple_price_feed \ +for contract in mesh_external_staking mesh_converter mesh_native_staking mesh_native_staking_proxy mesh_simple_price_feed \ mesh_vault mesh_virtual_staking ; do -cp -f ../../../../mesh-security/artifacts/${contract}.wasm . +cp -f ../../../mesh-security/artifacts/${contract}.wasm . gzip -fk ${contract}.wasm rm -f ${contract}.wasm done -cd ../../../../mesh-security +cd ../../../mesh-security tag=$(git rev-parse HEAD) cd - rm -f version.txt diff --git a/tests/testdata/mesh_converter.wasm.gz b/tests/testdata/mesh_converter.wasm.gz index 9a3e33d5..72a0e716 100644 Binary files a/tests/testdata/mesh_converter.wasm.gz and b/tests/testdata/mesh_converter.wasm.gz differ diff --git a/tests/testdata/mesh_external_staking.wasm.gz b/tests/testdata/mesh_external_staking.wasm.gz index 1381a305..63319fa2 100644 Binary files a/tests/testdata/mesh_external_staking.wasm.gz and b/tests/testdata/mesh_external_staking.wasm.gz differ diff --git a/tests/testdata/mesh_native_staking.wasm.gz b/tests/testdata/mesh_native_staking.wasm.gz index e59633de..6f4a87c3 100644 Binary files a/tests/testdata/mesh_native_staking.wasm.gz and b/tests/testdata/mesh_native_staking.wasm.gz differ diff --git a/tests/testdata/mesh_native_staking_proxy.wasm.gz b/tests/testdata/mesh_native_staking_proxy.wasm.gz index 2efacb69..e56378bf 100644 Binary files a/tests/testdata/mesh_native_staking_proxy.wasm.gz and b/tests/testdata/mesh_native_staking_proxy.wasm.gz differ diff --git a/tests/testdata/mesh_simple_price_feed.wasm.gz b/tests/testdata/mesh_simple_price_feed.wasm.gz index 6c0ee595..31a9944c 100644 Binary files a/tests/testdata/mesh_simple_price_feed.wasm.gz and b/tests/testdata/mesh_simple_price_feed.wasm.gz differ diff --git a/tests/testdata/mesh_vault.wasm.gz b/tests/testdata/mesh_vault.wasm.gz index a9333c18..117a1eab 100644 Binary files a/tests/testdata/mesh_vault.wasm.gz and b/tests/testdata/mesh_vault.wasm.gz differ diff --git a/tests/testdata/mesh_virtual_staking.wasm.gz b/tests/testdata/mesh_virtual_staking.wasm.gz index ceb80a0b..c2977a7f 100644 Binary files a/tests/testdata/mesh_virtual_staking.wasm.gz and b/tests/testdata/mesh_virtual_staking.wasm.gz differ diff --git a/tests/testdata/version.txt b/tests/testdata/version.txt index da48689d..d7bfc037 100644 --- a/tests/testdata/version.txt +++ b/tests/testdata/version.txt @@ -1 +1 @@ -v0.10.0-alpha.1 +34284a38601ff132e8d7b5594a87794faa71bbed diff --git a/x/meshsecurity/abci_test.go b/x/meshsecurity/abci_test.go index 677686bd..675d17d5 100644 --- a/x/meshsecurity/abci_test.go +++ b/x/meshsecurity/abci_test.go @@ -79,7 +79,7 @@ func TestEndBlocker(t *testing.T) { assert: func(t *testing.T, ctx sdk.Context) { require.Len(t, capturedCalls, 2) assert.Equal(t, myContractAddr, capturedCalls[0].contractAddress) - exp := fmt.Sprintf(`{"valset_update":{"additions":[{"address":"%s","commission":"0.000000000000000000","max_commission":"0.000000000000000000","max_change_rate":"0.000000000000000000"}],"removals":[],"updated":[],"jailed":[],"unjailed":[],"slashed":[],"tombstoned":[]}}`, val1.GetOperator()) + exp := fmt.Sprintf(`{"handle_valset_update":{"additions":[{"address":"%s","commission":"0.000000000000000000","max_commission":"0.000000000000000000","max_change_rate":"0.000000000000000000"}],"removals":[],"updated":[],"jailed":[],"unjailed":[],"slashed":[],"tombstoned":[]}}`, val1.GetOperator()) assert.JSONEq(t, exp, string(capturedCalls[0].msg)) assert.Equal(t, myOtherContractAddr, capturedCalls[1].contractAddress) diff --git a/x/meshsecurity/contract/out_message.go b/x/meshsecurity/contract/out_message.go index b94c37b3..224bd694 100644 --- a/x/meshsecurity/contract/out_message.go +++ b/x/meshsecurity/contract/out_message.go @@ -7,7 +7,7 @@ import ( type ( SudoMsg struct { HandleEpoch *struct{} `json:"handle_epoch,omitempty"` - ValsetUpdate *ValsetUpdate `json:"valset_update,omitempty"` + ValsetUpdate *ValsetUpdate `json:"handle_valset_update,omitempty"` } // Validator alias to wasmVM type diff --git a/x/meshsecurityprovider/contract/in_message.go b/x/meshsecurityprovider/contract/in_message.go new file mode 100644 index 00000000..37ac6a03 --- /dev/null +++ b/x/meshsecurityprovider/contract/in_message.go @@ -0,0 +1,21 @@ +package contract + +import wasmvmtypes "github.com/CosmWasm/wasmvm/types" + +type ( + CustomMsg struct { + Provider *ProviderMsg `json:"provider,omitempty"` + } + ProviderMsg struct { + Bond *BondMsg `json:"bond,omitempty"` + Unbond *UnbondMsg `json:"unbond,omitempty"` + } + BondMsg struct { + Amount wasmvmtypes.Coin `json:"amount"` + Delegator string `json:"delegator"` + } + UnbondMsg struct { + Amount wasmvmtypes.Coin `json:"amount"` + Delegator string `json:"delegator"` + } +) diff --git a/x/meshsecurityprovider/keeper/handle_plugin.go b/x/meshsecurityprovider/keeper/handle_plugin.go new file mode 100644 index 00000000..55358da2 --- /dev/null +++ b/x/meshsecurityprovider/keeper/handle_plugin.go @@ -0,0 +1,51 @@ +package keeper + +import ( + "encoding/json" + + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" + wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + + "github.com/osmosis-labs/mesh-security-sdk/x/meshsecurityprovider/contract" +) + +type CustomMessenger struct { + k *Keeper +} + + +// CustomMessageDecorator returns decorator for custom CosmWasm bindings messages +func CustomMessageDecorator(provKeeper *Keeper) *CustomMessenger { + return &CustomMessenger{ + k: provKeeper, + } +} + +var _ wasmkeeper.Messenger = (*CustomMessenger)(nil) + +// DispatchMsg executes on the contractMsg. +func (h CustomMessenger) DispatchMsg(ctx sdk.Context, contractAddr sdk.AccAddress, _ string, msg wasmvmtypes.CosmosMsg) ([]sdk.Event, [][]byte, error) { + if msg.Custom == nil { + return nil, nil, wasmtypes.ErrUnknownMsg + } + var customMsg contract.CustomMsg + if err := json.Unmarshal(msg.Custom, &customMsg); err != nil { + return nil, nil, sdkerrors.ErrJSONUnmarshal.Wrap("custom message") + } + if customMsg.Provider == nil { + // not our message type + return nil, nil, wasmtypes.ErrUnknownMsg + } + switch { + case customMsg.Provider.Bond != nil: + return h.k.HandleBondMsg(ctx, contractAddr, customMsg.Provider.Bond) + case customMsg.Provider.Unbond != nil: + return h.k.HandleUnbondMsg(ctx, contractAddr, customMsg.Provider.Unbond) + } + return nil, nil, wasmtypes.ErrUnknownMsg +} + diff --git a/x/meshsecurityprovider/keeper/keeper.go b/x/meshsecurityprovider/keeper/keeper.go index 9384d8bc..ed90d183 100644 --- a/x/meshsecurityprovider/keeper/keeper.go +++ b/x/meshsecurityprovider/keeper/keeper.go @@ -3,10 +3,13 @@ package keeper import ( "github.com/cometbft/cometbft/libs/log" + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" "github.com/cosmos/cosmos-sdk/codec" storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/osmosis-labs/mesh-security-sdk/x/meshsecurityprovider/contract" "github.com/osmosis-labs/mesh-security-sdk/x/meshsecurityprovider/types" ) @@ -88,3 +91,59 @@ func (k Keeper) ExportGenesis(ctx sdk.Context) *types.GenesisState { Params: k.GetParams(ctx), } } + +func (k Keeper) HandleBondMsg(ctx sdk.Context, actor sdk.AccAddress, bondMsg *contract.BondMsg) ([]sdk.Event, [][]byte, error) { + if actor.String() != k.VaultAddress(ctx) { + return nil, nil, sdkerrors.ErrUnauthorized.Wrapf("contract has no permission for mesh security operations") + } + + coin, err := wasmkeeper.ConvertWasmCoinToSdkCoin(bondMsg.Amount) + if err != nil { + return nil, nil, err + } + + delAddr, err := sdk.AccAddressFromBech32(bondMsg.Delegator) + if err != nil { + return nil, nil, err + } + + err = k.bankKeeper.DelegateCoins(ctx, delAddr, actor, sdk.NewCoins(coin)) + if err != nil { + return nil, nil, err + } + + return []sdk.Event{sdk.NewEvent( + types.EventTypeBond, + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), + sdk.NewAttribute(sdk.AttributeKeyAmount, coin.String()), + sdk.NewAttribute(types.AttributeKeyDelegator, delAddr.String()), + )}, nil, nil +} + +func (k Keeper) HandleUnbondMsg(ctx sdk.Context, actor sdk.AccAddress, unbondMsg *contract.UnbondMsg) ([]sdk.Event, [][]byte, error) { + if actor.String() != k.VaultAddress(ctx) { + return nil, nil, sdkerrors.ErrUnauthorized.Wrapf("contract has no permission for mesh security operations") + } + + coin, err := wasmkeeper.ConvertWasmCoinToSdkCoin(unbondMsg.Amount) + if err != nil { + return nil, nil, err + } + + delAddr, err := sdk.AccAddressFromBech32(unbondMsg.Delegator) + if err != nil { + return nil, nil, err + } + + err = k.bankKeeper.UndelegateCoins(ctx, actor, delAddr, sdk.NewCoins(coin)) + if err != nil { + return nil, nil, err + } + + return []sdk.Event{sdk.NewEvent( + types.EventTypeUnbond, + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), + sdk.NewAttribute(sdk.AttributeKeyAmount, coin.String()), + sdk.NewAttribute(types.AttributeKeyDelegator, delAddr.String()), + )}, nil, nil +}