Skip to content

Commit

Permalink
Merge pull request #433 from CosmWasm/sudo-entry-point
Browse files Browse the repository at this point in the history
Sudo entry point
  • Loading branch information
ethanfrey authored Mar 4, 2021
2 parents 4307e9b + 311be37 commit b09a925
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
39 changes: 39 additions & 0 deletions x/wasm/internal/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, 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))
}
Expand Down
71 changes: 71 additions & 0 deletions x/wasm/internal/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1025,6 +1025,77 @@ func TestMigrateWithDispatchedMessage(t *testing.T) {
assert.Equal(t, deposit, balance)
}

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"`
}

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{
// 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")},
},
}
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 {
Expand Down
9 changes: 9 additions & 0 deletions x/wasm/internal/keeper/wasmtesting/mock_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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!")
Expand Down
16 changes: 16 additions & 0 deletions x/wasm/internal/types/wasmer_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit b09a925

Please sign in to comment.