From c2ff44522139be411c832159565fb358f99a3a26 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Wed, 3 Mar 2021 20:29:28 +0100 Subject: [PATCH 1/5] Expose Sudo on WasmEngine and Keeper --- x/wasm/internal/keeper/keeper.go | 39 +++++++++++++++++++ .../keeper/wasmtesting/mock_engine.go | 9 +++++ x/wasm/internal/types/wasmer_engine.go | 16 ++++++++ 3 files changed, 64 insertions(+) diff --git a/x/wasm/internal/keeper/keeper.go b/x/wasm/internal/keeper/keeper.go index b9425a2b2f..3b860ef7b2 100644 --- a/x/wasm/internal/keeper/keeper.go +++ b/x/wasm/internal/keeper/keeper.go @@ -440,6 +440,45 @@ func (k Keeper) migrate(ctx sdk.Context, contractAddress sdk.AccAddress, caller }, nil } +// Sudo allows priviledged access to a contract. This can never be called by governance or external tx, but only by +// another native Go module directly. Thus, the keeper doesn't place any access controls on it, that is the +// responsibility or the app developer (who passes the wasm.Keeper in app.go) +func (k Keeper) Sudo(ctx sdk.Context, contractAddress sdk.AccAddress, caller sdk.AccAddress, newCodeID uint64, msg []byte) (*sdk.Result, error) { + ctx.GasMeter().ConsumeGas(InstanceCost, "Loading CosmWasm module: sudo") + + contractInfo, codeInfo, prefixStore, err := k.contractInstance(ctx, contractAddress) + if err != nil { + return nil, err + } + + env := types.NewEnv(ctx, contractAddress) + + // prepare querier + querier := QueryHandler{ + Ctx: ctx, + Plugins: k.queryPlugins, + } + gas := gasForContract(ctx) + res, gasUsed, execErr := k.wasmer.Sudo(codeInfo.CodeHash, env, msg, prefixStore, cosmwasmAPI, querier, gasMeter(ctx), gas) + consumeGas(ctx, gasUsed) + if execErr != nil { + return nil, sdkerrors.Wrap(types.ErrExecuteFailed, execErr.Error()) + } + + // emit all events from this contract itself + events := types.ParseEvents(res.Attributes, contractAddress) + ctx.EventManager().EmitEvents(events) + + err = k.dispatchMessages(ctx, contractAddress, contractInfo.IBCPortID, res.Messages) + if err != nil { + return nil, sdkerrors.Wrap(err, "dispatch") + } + + return &sdk.Result{ + Data: res.Data, + }, nil +} + func (k Keeper) deleteContractSecondIndex(ctx sdk.Context, contractAddress sdk.AccAddress, contractInfo *types.ContractInfo) { ctx.KVStore(k.storeKey).Delete(types.GetContractByCreatedSecondaryIndexKey(contractAddress, contractInfo)) } diff --git a/x/wasm/internal/keeper/wasmtesting/mock_engine.go b/x/wasm/internal/keeper/wasmtesting/mock_engine.go index 368f3b23ac..9af8f45794 100644 --- a/x/wasm/internal/keeper/wasmtesting/mock_engine.go +++ b/x/wasm/internal/keeper/wasmtesting/mock_engine.go @@ -21,6 +21,7 @@ type MockWasmer struct { ExecuteFn func(codeID wasmvm.Checksum, env wasmvmtypes.Env, info wasmvmtypes.MessageInfo, executeMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64) (*wasmvmtypes.Response, uint64, error) QueryFn func(codeID wasmvm.Checksum, env wasmvmtypes.Env, queryMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64) ([]byte, uint64, error) MigrateFn func(codeID wasmvm.Checksum, env wasmvmtypes.Env, migrateMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64) (*wasmvmtypes.Response, uint64, error) + SudoFn func(codeID wasmvm.Checksum, env wasmvmtypes.Env, sudoMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64) (*wasmvmtypes.Response, uint64, error) GetCodeFn func(codeID wasmvm.Checksum) (wasmvm.WasmCode, error) CleanupFn func() IBCChannelOpenFn func(codeID wasmvm.Checksum, env wasmvmtypes.Env, channel wasmvmtypes.IBCChannel, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64) (uint64, error) @@ -115,6 +116,14 @@ func (m *MockWasmer) Migrate(codeID wasmvm.Checksum, env wasmvmtypes.Env, migrat return m.MigrateFn(codeID, env, migrateMsg, store, goapi, querier, gasMeter, gasLimit) } +func (m *MockWasmer) Sudo(codeID wasmvm.Checksum, env wasmvmtypes.Env, sudoMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64) (*wasmvmtypes.Response, uint64, error) { + if m.SudoFn == nil { + panic("not supposed to be called!") + } + return m.SudoFn(codeID, env, sudoMsg, store, goapi, querier, gasMeter, gasLimit) + +} + func (m *MockWasmer) GetCode(codeID wasmvm.Checksum) (wasmvm.WasmCode, error) { if m.GetCodeFn == nil { panic("not supposed to be called!") diff --git a/x/wasm/internal/types/wasmer_engine.go b/x/wasm/internal/types/wasmer_engine.go index bddb30140a..8aba3fd31c 100644 --- a/x/wasm/internal/types/wasmer_engine.go +++ b/x/wasm/internal/types/wasmer_engine.go @@ -91,6 +91,22 @@ type WasmerEngine interface { gasLimit uint64, ) (*wasmvmtypes.Response, uint64, error) + // Sudo runs an existing contract in read/write mode (like Execute), but is never exposed to external callers + // (either transactions or government proposals), but can only be called by other native Go modules directly. + // + // This allows a contract to expose custom "super user" functions or priviledged operations that can be + // deeply integrated with native modules. + Sudo( + codeID wasmvm.Checksum, + env wasmvmtypes.Env, + sudoMsg []byte, + store wasmvm.KVStore, + goapi wasmvm.GoAPI, + querier wasmvm.Querier, + gasMeter wasmvm.GasMeter, + gasLimit uint64, + ) (*wasmvmtypes.Response, uint64, error) + // GetCode will load the original wasm code for the given code id. // This will only succeed if that code id was previously returned from // a call to Create. From 1f2a7db818aa1f278c3a75290067457cea1fd197 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Wed, 3 Mar 2021 20:48:09 +0100 Subject: [PATCH 2/5] Add test coverage --- x/wasm/internal/keeper/keeper.go | 2 +- x/wasm/internal/keeper/keeper_test.go | 60 +++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/x/wasm/internal/keeper/keeper.go b/x/wasm/internal/keeper/keeper.go index 3b860ef7b2..89e4dcafaa 100644 --- a/x/wasm/internal/keeper/keeper.go +++ b/x/wasm/internal/keeper/keeper.go @@ -443,7 +443,7 @@ func (k Keeper) migrate(ctx sdk.Context, contractAddress sdk.AccAddress, caller // Sudo allows priviledged access to a contract. This can never be called by governance or external tx, but only by // another native Go module directly. Thus, the keeper doesn't place any access controls on it, that is the // responsibility or the app developer (who passes the wasm.Keeper in app.go) -func (k Keeper) Sudo(ctx sdk.Context, contractAddress sdk.AccAddress, caller sdk.AccAddress, newCodeID uint64, msg []byte) (*sdk.Result, error) { +func (k Keeper) Sudo(ctx sdk.Context, contractAddress sdk.AccAddress, msg []byte) (*sdk.Result, error) { ctx.GasMeter().ConsumeGas(InstanceCost, "Loading CosmWasm module: sudo") contractInfo, codeInfo, prefixStore, err := k.contractInstance(ctx, contractAddress) diff --git a/x/wasm/internal/keeper/keeper_test.go b/x/wasm/internal/keeper/keeper_test.go index bc79051d48..6b58520663 100644 --- a/x/wasm/internal/keeper/keeper_test.go +++ b/x/wasm/internal/keeper/keeper_test.go @@ -1025,6 +1025,66 @@ func TestMigrateWithDispatchedMessage(t *testing.T) { assert.Equal(t, deposit, balance) } +type sudoMsg struct { + StealFunds stealFundsMsg `json:"StealFunds"` +} + +type stealFundsMsg struct { + Recipient string `json:"recipient"` + Amount wasmvmtypes.Coins `json:"amount"` +} + +func TestSudo(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, SupportedFeatures, nil, nil) + accKeeper, keeper, bankKeeper := keepers.AccountKeeper, keepers.WasmKeeper, keepers.BankKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := createFakeFundedAccount(t, ctx, accKeeper, bankKeeper, deposit.Add(deposit...)) + + wasmCode, err := ioutil.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + contractID, err := keeper.Create(ctx, creator, wasmCode, "", "", nil) + require.NoError(t, err) + + _, _, bob := keyPubAddr() + _, _, fred := keyPubAddr() + initMsg := HackatomExampleInitMsg{ + Verifier: fred, + Beneficiary: bob, + } + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + + addr, _, err := keeper.Instantiate(ctx, contractID, creator, nil, initMsgBz, "demo contract 3", deposit) + require.NoError(t, err) + require.Equal(t, "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5", addr.String()) + + // the community is broke + _, _, community := keyPubAddr() + comAcct := accKeeper.GetAccount(ctx, community) + require.Nil(t, comAcct) + + // now the community wants to get paid via sudo + msg := sudoMsg{ + StealFunds: stealFundsMsg{ + Recipient: community.String(), + Amount: wasmvmtypes.Coins{wasmvmtypes.NewCoin(76543, "denom")}, + }, + } + sudoMsg, err := json.Marshal(msg) + require.NoError(t, err) + + res, err := keeper.Sudo(ctx, addr, sudoMsg) + require.NoError(t, err) + require.NotNil(t, res) + + // ensure community now exists and got paid + comAcct = accKeeper.GetAccount(ctx, community) + require.NotNil(t, comAcct) + balance := bankKeeper.GetBalance(ctx, comAcct.GetAddress(), "denom") + assert.Equal(t, sdk.NewInt64Coin("denom", 76543), balance) +} + func prettyEvents(t *testing.T, events sdk.Events) string { t.Helper() type prettyEvent struct { From 43430b751e182e40189133350139ef0af80ae844 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Wed, 3 Mar 2021 20:48:52 +0100 Subject: [PATCH 3/5] Update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2379f6e21d..64daa3b4ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ [Full Changelog](https://github.com/CosmWasm/wasmd/compare/v0.15.0...HEAD) +- Upgrade to CosmWasm v0.14.0 [\#432](https://github.com/CosmWasm/wasmd/pull/432) +- Expose Sudo contract entry point on Keeper [\#433](https://github.com/CosmWasm/wasmd/pull/433) - Support custom MessageHandler [\#327](https://github.com/CosmWasm/wasmd/issues/327) - 🎉 Implement IBC contract support [\#394](https://github.com/CosmWasm/wasmd/pull/394) From a50bafb78940520c8fe839d57f8475d2e074498d Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Wed, 3 Mar 2021 23:19:56 +0100 Subject: [PATCH 4/5] Update SudoMsg in test --- x/wasm/internal/keeper/keeper_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/wasm/internal/keeper/keeper_test.go b/x/wasm/internal/keeper/keeper_test.go index 6b58520663..9f29763ff8 100644 --- a/x/wasm/internal/keeper/keeper_test.go +++ b/x/wasm/internal/keeper/keeper_test.go @@ -1026,7 +1026,7 @@ func TestMigrateWithDispatchedMessage(t *testing.T) { } type sudoMsg struct { - StealFunds stealFundsMsg `json:"StealFunds"` + StealFunds stealFundsMsg `json:"steal_funds"` } type stealFundsMsg struct { From 311be37e90f8932d8754f53612732e87f9bb18ba Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Thu, 4 Mar 2021 10:57:09 +0100 Subject: [PATCH 5/5] Add comment on tongue-in-check StealFunds message --- x/wasm/internal/keeper/keeper_test.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/x/wasm/internal/keeper/keeper_test.go b/x/wasm/internal/keeper/keeper_test.go index 9f29763ff8..8afa6cd3f9 100644 --- a/x/wasm/internal/keeper/keeper_test.go +++ b/x/wasm/internal/keeper/keeper_test.go @@ -1026,6 +1026,14 @@ func TestMigrateWithDispatchedMessage(t *testing.T) { } type sudoMsg struct { + // This is a tongue-in-check demo command. This is not the intended purpose of Sudo. + // Here we show that some priviledged Go module can make a call that should never be exposed + // to end users (via Tx/Execute). + // + // The contract developer can choose to expose anything to sudo. This functionality is not a true + // backdoor (it can never be called by end users), but allows the developers of the native blockchain + // code to make special calls. This can also be used as an authentication mechanism, if you want to expose + // some callback that only can be triggered by some system module and not faked by external users. StealFunds stealFundsMsg `json:"steal_funds"` } @@ -1066,6 +1074,9 @@ func TestSudo(t *testing.T) { // now the community wants to get paid via sudo msg := sudoMsg{ + // This is a tongue-in-check demo command. This is not the intended purpose of Sudo. + // Here we show that some priviledged Go module can make a call that should never be exposed + // to end users (via Tx/Execute). StealFunds: stealFundsMsg{ Recipient: community.String(), Amount: wasmvmtypes.Coins{wasmvmtypes.NewCoin(76543, "denom")},