diff --git a/app/app.go b/app/app.go index f7084a10..7ad7eef5 100644 --- a/app/app.go +++ b/app/app.go @@ -7,12 +7,7 @@ import ( "github.com/spf13/viper" - abci "github.com/tendermint/tendermint/abci/types" - "github.com/tendermint/tendermint/libs/cli" - "github.com/tendermint/tendermint/libs/log" - tmos "github.com/tendermint/tendermint/libs/os" - dbm "github.com/tendermint/tm-db" - + "github.com/CosmWasm/wasmd/x/wasm" bam "github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/simapp" @@ -35,8 +30,11 @@ import ( "github.com/cosmos/cosmos-sdk/x/supply" "github.com/cosmos/cosmos-sdk/x/upgrade" upgradeclient "github.com/cosmos/cosmos-sdk/x/upgrade/client" - - "github.com/CosmWasm/wasmd/x/wasm" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/libs/cli" + "github.com/tendermint/tendermint/libs/log" + tmos "github.com/tendermint/tendermint/libs/os" + dbm "github.com/tendermint/tm-db" ) const appName = "WasmApp" @@ -228,10 +226,6 @@ func NewWasmApp( AddRoute(params.RouterKey, params.NewParamChangeProposalHandler(app.paramsKeeper)). AddRoute(distr.RouterKey, distr.NewCommunityPoolSpendProposalHandler(app.distrKeeper)). AddRoute(upgrade.RouterKey, upgrade.NewSoftwareUpgradeProposalHandler(app.upgradeKeeper)) - app.govKeeper = gov.NewKeeper( - app.cdc, keys[gov.StoreKey], app.subspaces[gov.ModuleName], - app.supplyKeeper, &stakingKeeper, govRouter, - ) // register the staking hooks // NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks @@ -257,6 +251,15 @@ func NewWasmApp( supportedFeatures := "staking" app.wasmKeeper = wasm.NewKeeper(app.cdc, keys[wasm.StoreKey], app.accountKeeper, app.bankKeeper, app.stakingKeeper, wasmRouter, wasmDir, wasmConfig, supportedFeatures, nil, nil) + if len(wasm.DefaultEnabledProposals) != 0 { + govRouter.AddRoute(wasm.RouterKey, wasm.NewWasmProposalHandler(app.wasmKeeper, wasm.DefaultEnabledProposals)) + } + + app.govKeeper = gov.NewKeeper( + app.cdc, keys[gov.StoreKey], app.subspaces[gov.ModuleName], + app.supplyKeeper, &stakingKeeper, govRouter, + ) + // NOTE: Any module instantiated in the module manager that is later modified // must be passed by reference here. app.mm = module.NewManager( diff --git a/x/wasm/alias.go b/x/wasm/alias.go index 182f406c..49653fcd 100644 --- a/x/wasm/alias.go +++ b/x/wasm/alias.go @@ -66,24 +66,26 @@ var ( MakeTestCodec = keeper.MakeTestCodec CreateTestInput = keeper.CreateTestInput TestHandler = keeper.TestHandler + NewWasmProposalHandler = keeper.NewWasmProposalHandler // variable aliases - ModuleCdc = types.ModuleCdc - DefaultCodespace = types.DefaultCodespace - ErrCreateFailed = types.ErrCreateFailed - ErrAccountExists = types.ErrAccountExists - ErrInstantiateFailed = types.ErrInstantiateFailed - ErrExecuteFailed = types.ErrExecuteFailed - ErrGasLimit = types.ErrGasLimit - ErrInvalidGenesis = types.ErrInvalidGenesis - ErrNotFound = types.ErrNotFound - ErrQueryFailed = types.ErrQueryFailed - ErrInvalidMsg = types.ErrInvalidMsg - KeyLastCodeID = types.KeyLastCodeID - KeyLastInstanceID = types.KeyLastInstanceID - CodeKeyPrefix = types.CodeKeyPrefix - ContractKeyPrefix = types.ContractKeyPrefix - ContractStorePrefix = types.ContractStorePrefix + ModuleCdc = types.ModuleCdc + DefaultCodespace = types.DefaultCodespace + ErrCreateFailed = types.ErrCreateFailed + ErrAccountExists = types.ErrAccountExists + ErrInstantiateFailed = types.ErrInstantiateFailed + ErrExecuteFailed = types.ErrExecuteFailed + ErrGasLimit = types.ErrGasLimit + ErrInvalidGenesis = types.ErrInvalidGenesis + ErrNotFound = types.ErrNotFound + ErrQueryFailed = types.ErrQueryFailed + ErrInvalidMsg = types.ErrInvalidMsg + KeyLastCodeID = types.KeyLastCodeID + KeyLastInstanceID = types.KeyLastInstanceID + CodeKeyPrefix = types.CodeKeyPrefix + ContractKeyPrefix = types.ContractKeyPrefix + ContractStorePrefix = types.ContractStorePrefix + DefaultEnabledProposals = types.DefaultEnabledProposals ) type ( diff --git a/x/wasm/internal/keeper/authz_policy.go b/x/wasm/internal/keeper/authz_policy.go new file mode 100644 index 00000000..9193cd9c --- /dev/null +++ b/x/wasm/internal/keeper/authz_policy.go @@ -0,0 +1,42 @@ +package keeper + +import ( + "github.com/CosmWasm/wasmd/x/wasm/internal/types" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type AuthorizationPolicy interface { + CanCreateCode(c types.AccessConfig, creator sdk.AccAddress) bool + CanInstantiateContract(c types.AccessConfig, actor sdk.AccAddress) bool + CanModifyContract(admin, actor sdk.AccAddress) bool +} + +type DefaultAuthorizationPolicy struct { +} + +func (p DefaultAuthorizationPolicy) CanCreateCode(config types.AccessConfig, actor sdk.AccAddress) bool { + return config.Allowed(actor) +} + +func (p DefaultAuthorizationPolicy) CanInstantiateContract(config types.AccessConfig, actor sdk.AccAddress) bool { + return config.Allowed(actor) +} + +func (p DefaultAuthorizationPolicy) CanModifyContract(admin, actor sdk.AccAddress) bool { + return admin != nil && admin.Equals(actor) +} + +type GovAuthorizationPolicy struct { +} + +func (p GovAuthorizationPolicy) CanCreateCode(types.AccessConfig, sdk.AccAddress) bool { + return true +} + +func (p GovAuthorizationPolicy) CanInstantiateContract(types.AccessConfig, sdk.AccAddress) bool { + return true +} + +func (p GovAuthorizationPolicy) CanModifyContract(sdk.AccAddress, sdk.AccAddress) bool { + return true +} diff --git a/x/wasm/internal/keeper/keeper.go b/x/wasm/internal/keeper/keeper.go index 774f4839..55c7168e 100644 --- a/x/wasm/internal/keeper/keeper.go +++ b/x/wasm/internal/keeper/keeper.go @@ -49,6 +49,7 @@ type Keeper struct { messenger MessageHandler // queryGasLimit is the max wasm gas that can be spent on executing a query with a contract queryGasLimit uint64 + authZPolicy AuthorizationPolicy } // NewKeeper creates a new contract Keeper instance @@ -71,6 +72,7 @@ func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, accountKeeper auth.Accou bankKeeper: bankKeeper, messenger: messenger, queryGasLimit: wasmConfig.SmartQueryGasLimit, + authZPolicy: DefaultAuthorizationPolicy{}, } keeper.queryPlugins = DefaultQueryPlugins(bankKeeper, stakingKeeper, keeper).Merge(customPlugins) return keeper @@ -241,18 +243,20 @@ func (k Keeper) Execute(ctx sdk.Context, contractAddress sdk.AccAddress, caller // Migrate allows to upgrade a contract to a new code with data migration. func (k Keeper) Migrate(ctx sdk.Context, contractAddress sdk.AccAddress, caller sdk.AccAddress, newCodeID uint64, msg []byte) (*sdk.Result, error) { + return k.migrate(ctx, contractAddress, caller, newCodeID, msg, k.authZPolicy) +} + +func (k Keeper) migrate(ctx sdk.Context, contractAddress sdk.AccAddress, caller sdk.AccAddress, newCodeID uint64, msg []byte, authZ AuthorizationPolicy) (*sdk.Result, error) { ctx.GasMeter().ConsumeGas(InstanceCost, "Loading CosmWasm module: migrate") contractInfo := k.GetContractInfo(ctx, contractAddress) if contractInfo == nil { return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "unknown contract") } - if contractInfo.Admin == nil { - return nil, sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "migration not supported by this contract") - } - if !contractInfo.Admin.Equals(caller) { - return nil, sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "no permission") + if !authZ.CanModifyContract(contractInfo.Admin, caller) { + return nil, sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "can not migrate") } + newCodeInfo := k.GetCodeInfo(ctx, newCodeID) if newCodeInfo == nil { return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "unknown code") @@ -294,34 +298,23 @@ func (k Keeper) Migrate(ctx sdk.Context, contractAddress sdk.AccAddress, caller // UpdateContractAdmin sets the admin value on the ContractInfo. It must be a valid address (use ClearContractAdmin to remove it) func (k Keeper) UpdateContractAdmin(ctx sdk.Context, contractAddress sdk.AccAddress, caller sdk.AccAddress, newAdmin sdk.AccAddress) error { - contractInfo := k.GetContractInfo(ctx, contractAddress) - if contractInfo == nil { - return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "unknown contract") - } - if contractInfo.Admin == nil { - return sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "migration not supported by this contract") - } - if !contractInfo.Admin.Equals(caller) { - return sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "no permission") - } - contractInfo.Admin = newAdmin - k.setContractInfo(ctx, contractAddress, contractInfo) - return nil + return k.setContractAdmin(ctx, contractAddress, caller, newAdmin, k.authZPolicy) } // ClearContractAdmin sets the admin value on the ContractInfo to nil, to disable further migrations/ updates. func (k Keeper) ClearContractAdmin(ctx sdk.Context, contractAddress sdk.AccAddress, caller sdk.AccAddress) error { + return k.setContractAdmin(ctx, contractAddress, caller, nil, k.authZPolicy) +} + +func (k Keeper) setContractAdmin(ctx sdk.Context, contractAddress, caller, newAdmin sdk.AccAddress, authZ AuthorizationPolicy) error { contractInfo := k.GetContractInfo(ctx, contractAddress) if contractInfo == nil { return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "unknown contract") } - if contractInfo.Admin == nil { - return sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "migration not supported by this contract") + if !authZ.CanModifyContract(contractInfo.Admin, caller) { + return sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "can not modify contract") } - if !contractInfo.Admin.Equals(caller) { - return sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "no permission") - } - contractInfo.Admin = nil + contractInfo.Admin = newAdmin k.setContractInfo(ctx, contractAddress, contractInfo) return nil } diff --git a/x/wasm/internal/keeper/proposal_handler.go b/x/wasm/internal/keeper/proposal_handler.go new file mode 100644 index 00000000..d2fb727b --- /dev/null +++ b/x/wasm/internal/keeper/proposal_handler.go @@ -0,0 +1,141 @@ +package keeper + +import ( + "fmt" + + "github.com/CosmWasm/wasmd/x/wasm/internal/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" +) + +const ( // TODO: same as in handler + + AttributeKeyContract = "contract_address" + AttributeKeyCodeID = "code_id" + AttributeSigner = "signer" +) + +// NewWasmProposalHandler creates a new governance Handler for wasm proposals +func NewWasmProposalHandler(k Keeper, enabledTypes map[string]struct{}) govtypes.Handler { + return func(ctx sdk.Context, content govtypes.Content) error { + if content == nil { + return sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, "content must not be empty") + } + if _, ok := enabledTypes[content.ProposalType()]; !ok { + return sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unsupported wasm proposal content type: %q", content.ProposalType()) + } + switch c := content.(type) { + case *types.StoreCodeProposal: + return handleStoreCodeProposal(ctx, k, *c) + case *types.InstantiateContractProposal: + return handleInstantiateProposal(ctx, k, *c) + case *types.MigrateContractProposal: + return handleMigrateProposal(ctx, k, *c) + case *types.UpdateAdminProposal: + return handleUpdateAdminProposal(ctx, k, *c) + case *types.ClearAdminProposal: + return handleClearAdminProposal(ctx, k, *c) + default: + return sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized wasm proposal content type: %T", c) + } + } +} + +func handleStoreCodeProposal(ctx sdk.Context, k Keeper, p types.StoreCodeProposal) error { + if err := p.ValidateBasic(); err != nil { + return err + } + + codeID, err := k.Create(ctx, p.Creator, p.WASMByteCode, p.Source, p.Builder) + if err != nil { + return err + } + + ourEvent := sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), + //sdk.NewAttribute(AttributeSigner, p.Creator.String()), // todo: creator is not signer. rename attribute? + sdk.NewAttribute(AttributeKeyCodeID, fmt.Sprintf("%d", codeID)), + ) + ctx.EventManager().EmitEvent(ourEvent) + return nil +} + +func handleInstantiateProposal(ctx sdk.Context, k Keeper, p types.InstantiateContractProposal) error { + if err := p.ValidateBasic(); err != nil { + return err + } + + contractAddr, err := k.Instantiate(ctx, p.Code, p.Creator, p.Admin, p.InitMsg, p.Label, p.InitFunds) + if err != nil { + return err + } + + ourEvent := sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), + //sdk.NewAttribute(AttributeSigner, p.Creator.String()), + sdk.NewAttribute(AttributeKeyCodeID, fmt.Sprintf("%d", p.Code)), + sdk.NewAttribute(AttributeKeyContract, contractAddr.String()), + ) + ctx.EventManager().EmitEvent(ourEvent) + return nil +} + +func handleMigrateProposal(ctx sdk.Context, k Keeper, p types.MigrateContractProposal) error { + if err := p.ValidateBasic(); err != nil { + return err + } + + res, err := k.migrate(ctx, p.Contract, p.Sender, p.Code, p.MigrateMsg, GovAuthorizationPolicy{}) + if err != nil { + return err + } + + ourEvent := sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), + //sdk.NewAttribute(AttributeSigner, p.Creator.String()), + sdk.NewAttribute(AttributeKeyContract, p.Contract.String()), + ) + ctx.EventManager().EmitEvents(append(res.Events, ourEvent)) + return nil +} + +func handleUpdateAdminProposal(ctx sdk.Context, k Keeper, p types.UpdateAdminProposal) error { + if err := p.ValidateBasic(); err != nil { + return err + } + + if err := k.setContractAdmin(ctx, p.Contract, p.Sender, p.NewAdmin, GovAuthorizationPolicy{}); err != nil { + return err + } + + ourEvent := sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), + //sdk.NewAttribute(AttributeSigner, p.Creator.String()), + sdk.NewAttribute(AttributeKeyContract, p.Contract.String()), + ) + ctx.EventManager().EmitEvent(ourEvent) + return nil +} + +func handleClearAdminProposal(ctx sdk.Context, k Keeper, p types.ClearAdminProposal) error { + if err := p.ValidateBasic(); err != nil { + return err + } + + if err := k.setContractAdmin(ctx, p.Contract, p.Sender, nil, GovAuthorizationPolicy{}); err != nil { + return err + } + ourEvent := sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), + //sdk.NewAttribute(AttributeSigner, p.Creator.String()), + sdk.NewAttribute(AttributeKeyContract, p.Contract.String()), + ) + ctx.EventManager().EmitEvent(ourEvent) + return nil +} diff --git a/x/wasm/internal/keeper/proposal_integration_test.go b/x/wasm/internal/keeper/proposal_integration_test.go new file mode 100644 index 00000000..bf10a7ef --- /dev/null +++ b/x/wasm/internal/keeper/proposal_integration_test.go @@ -0,0 +1,270 @@ +package keeper + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "io/ioutil" + "os" + "testing" + + "github.com/CosmWasm/wasmd/x/wasm/internal/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/gov" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestStoreCodeProposal(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + ctx, keepers := CreateTestInput(t, false, tempDir, "staking", nil, nil) + govKeeper, wasmKeeper := keepers.GovKeeper, keepers.WasmKeeper + + wasmCode, err := ioutil.ReadFile("./testdata/contract.wasm") + require.NoError(t, err) + + var anyAddress sdk.AccAddress = make([]byte, sdk.AddrLen) + + src := types.StoreCodeProposalFixture(func(p *types.StoreCodeProposal) { + p.Creator = anyAddress + p.WASMByteCode = wasmCode + p.Source = "https://example.com/mysource" + p.Builder = "foo/bar:v0.0.0" + }) + + // when stored + storedProposal, err := govKeeper.SubmitProposal(ctx, &src) + require.NoError(t, err) + + // and proposal execute + handler := govKeeper.Router().GetRoute(storedProposal.ProposalRoute()) + err = handler(ctx, storedProposal.Content) + require.NoError(t, err) + + // then + cInfo := wasmKeeper.GetCodeInfo(ctx, 1) + require.NotNil(t, cInfo) + assert.Equal(t, anyAddress, cInfo.Creator) + assert.Equal(t, "foo/bar:v0.0.0", cInfo.Builder) + assert.Equal(t, "https://example.com/mysource", cInfo.Source) + + storedCode, err := wasmKeeper.GetByteCode(ctx, 1) + require.NoError(t, err) + assert.Equal(t, wasmCode, storedCode) +} + +func TestInstantiateProposal(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + ctx, keepers := CreateTestInput(t, false, tempDir, "staking", nil, nil) + govKeeper, wasmKeeper := keepers.GovKeeper, keepers.WasmKeeper + + wasmCode, err := ioutil.ReadFile("./testdata/contract.wasm") + require.NoError(t, err) + + require.NoError(t, wasmKeeper.importCode(ctx, 1, + types.CodeInfoFixture(types.WithSHA256CodeHash(wasmCode)), + wasmCode), + ) + + var ( + oneAddress sdk.AccAddress = bytes.Repeat([]byte{0x1}, sdk.AddrLen) + otherAddress sdk.AccAddress = bytes.Repeat([]byte{0x2}, sdk.AddrLen) + ) + src := types.InstantiateContractProposalFixture(func(p *types.InstantiateContractProposal) { + p.Code = 1 + p.Creator = oneAddress + p.Admin = otherAddress + p.Label = "testing" + }) + + // when stored + storedProposal, err := govKeeper.SubmitProposal(ctx, &src) + require.NoError(t, err) + + // and proposal execute + handler := govKeeper.Router().GetRoute(storedProposal.ProposalRoute()) + err = handler(ctx, storedProposal.Content) + require.NoError(t, err) + + // then + contractAddr, err := sdk.AccAddressFromBech32("cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5") + require.NoError(t, err) + + cInfo := wasmKeeper.GetContractInfo(ctx, contractAddr) + require.NotNil(t, cInfo) + assert.Equal(t, uint64(1), cInfo.CodeID) + assert.Equal(t, oneAddress, cInfo.Creator) + assert.Equal(t, otherAddress, cInfo.Admin) + assert.Equal(t, "testing", cInfo.Label) + assert.Equal(t, src.InitMsg, cInfo.InitMsg) +} + +func TestMigrateProposal(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + ctx, keepers := CreateTestInput(t, false, tempDir, "staking", nil, nil) + govKeeper, wasmKeeper := keepers.GovKeeper, keepers.WasmKeeper + + wasmCode, err := ioutil.ReadFile("./testdata/contract.wasm") + require.NoError(t, err) + + codeInfoFixture := types.CodeInfoFixture(types.WithSHA256CodeHash(wasmCode)) + require.NoError(t, wasmKeeper.importCode(ctx, 1, codeInfoFixture, wasmCode)) + require.NoError(t, wasmKeeper.importCode(ctx, 2, codeInfoFixture, wasmCode)) + + var ( + anyAddress sdk.AccAddress = bytes.Repeat([]byte{0x1}, sdk.AddrLen) + otherAddress sdk.AccAddress = bytes.Repeat([]byte{0x2}, sdk.AddrLen) + contractAddr = contractAddress(1, 1) + ) + + contractInfoFixture := types.ContractInfoFixture(func(c *types.ContractInfo) { + c.Label = "testing" + c.Admin = anyAddress + }) + key, err := hex.DecodeString("636F6E666967") + require.NoError(t, err) + m := types.Model{Key: key, Value: []byte(`{"verifier":"AAAAAAAAAAAAAAAAAAAAAAAAAAA=","beneficiary":"AAAAAAAAAAAAAAAAAAAAAAAAAAA=","funder":"AQEBAQEBAQEBAQEBAQEBAQEBAQE="}`)} + require.NoError(t, wasmKeeper.importContract(ctx, contractAddr, &contractInfoFixture, []types.Model{m})) + + migMsg := struct { + Verifier sdk.AccAddress `json:"verifier"` + }{Verifier: otherAddress} + migMsgBz, err := json.Marshal(migMsg) + require.NoError(t, err) + + src := types.MigrateContractProposal{ + WasmProposal: types.WasmProposal{ + Title: "Foo", + Description: "Bar", + }, + Code: 2, + Contract: contractAddr, + MigrateMsg: migMsgBz, + Sender: otherAddress, + } + + // when stored + storedProposal, err := govKeeper.SubmitProposal(ctx, &src) + require.NoError(t, err) + + // and proposal execute + handler := govKeeper.Router().GetRoute(storedProposal.ProposalRoute()) + err = handler(ctx, storedProposal.Content) + require.NoError(t, err) + + // then + require.NoError(t, err) + cInfo := wasmKeeper.GetContractInfo(ctx, contractAddr) + require.NotNil(t, cInfo) + assert.Equal(t, uint64(2), cInfo.CodeID) + assert.Equal(t, uint64(1), cInfo.PreviousCodeID) + assert.Equal(t, anyAddress, cInfo.Admin) + assert.Equal(t, "testing", cInfo.Label) +} + +func TestAdminProposals(t *testing.T) { + var ( + anyAddress sdk.AccAddress = bytes.Repeat([]byte{0x1}, sdk.AddrLen) + otherAddress sdk.AccAddress = bytes.Repeat([]byte{0x2}, sdk.AddrLen) + contractAddr = contractAddress(1, 1) + ) + wasmCode, err := ioutil.ReadFile("./testdata/contract.wasm") + require.NoError(t, err) + + specs := map[string]struct { + state types.ContractInfo + srcProposal gov.Content + expAdmin sdk.AccAddress + }{ + "update with different admin": { + state: types.ContractInfoFixture(), + srcProposal: &types.UpdateAdminProposal{ + WasmProposal: types.WasmProposal{ + Title: "Foo", + Description: "Bar", + }, + Contract: contractAddr, + Sender: anyAddress, + NewAdmin: otherAddress, + }, + expAdmin: otherAddress, + }, + "update with old admin empty": { + state: types.ContractInfoFixture(func(info *types.ContractInfo) { + info.Admin = nil + }), + srcProposal: &types.UpdateAdminProposal{ + WasmProposal: types.WasmProposal{ + Title: "Foo", + Description: "Bar", + }, + Contract: contractAddr, + Sender: anyAddress, + NewAdmin: otherAddress, + }, + expAdmin: otherAddress, + }, + "clear admin": { + state: types.ContractInfoFixture(), + srcProposal: &types.ClearAdminProposal{ + WasmProposal: types.WasmProposal{ + Title: "Foo", + Description: "Bar", + }, + Contract: contractAddr, + Sender: anyAddress, + }, + expAdmin: nil, + }, + "clear with old admin empty": { + state: types.ContractInfoFixture(func(info *types.ContractInfo) { + info.Admin = nil + }), + srcProposal: &types.ClearAdminProposal{ + WasmProposal: types.WasmProposal{ + Title: "Foo", + Description: "Bar", + }, + Contract: contractAddr, + Sender: anyAddress, + }, + expAdmin: nil, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + ctx, keepers := CreateTestInput(t, false, tempDir, "staking", nil, nil) + govKeeper, wasmKeeper := keepers.GovKeeper, keepers.WasmKeeper + + codeInfoFixture := types.CodeInfoFixture(types.WithSHA256CodeHash(wasmCode)) + require.NoError(t, wasmKeeper.importCode(ctx, 1, codeInfoFixture, wasmCode)) + + require.NoError(t, wasmKeeper.importContract(ctx, contractAddr, &spec.state, []types.Model{})) + // when stored + storedProposal, err := govKeeper.SubmitProposal(ctx, spec.srcProposal) + require.NoError(t, err) + + // and execute proposal + handler := govKeeper.Router().GetRoute(storedProposal.ProposalRoute()) + err = handler(ctx, storedProposal.Content) + require.NoError(t, err) + + // then + cInfo := wasmKeeper.GetContractInfo(ctx, contractAddr) + require.NotNil(t, cInfo) + assert.Equal(t, spec.expAdmin, cInfo.Admin) + }) + } +} diff --git a/x/wasm/internal/keeper/test_common.go b/x/wasm/internal/keeper/test_common.go index ae60b7a4..25c5cfd5 100644 --- a/x/wasm/internal/keeper/test_common.go +++ b/x/wasm/internal/keeper/test_common.go @@ -2,10 +2,13 @@ package keeper import ( "fmt" - "github.com/cosmos/cosmos-sdk/x/distribution" "testing" "time" + "github.com/cosmos/cosmos-sdk/x/distribution" + "github.com/cosmos/cosmos-sdk/x/gov" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + "github.com/stretchr/testify/require" abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/libs/log" @@ -39,6 +42,7 @@ func MakeTestCodec() *codec.Codec { supply.AppModuleBasic{}.RegisterCodec(cdc) staking.AppModuleBasic{}.RegisterCodec(cdc) distribution.AppModuleBasic{}.RegisterCodec(cdc) + gov.RegisterCodec(cdc) wasmTypes.RegisterCodec(cdc) sdk.RegisterCodec(cdc) codec.RegisterCrypto(cdc) @@ -60,6 +64,8 @@ type TestKeepers struct { WasmKeeper Keeper DistKeeper distribution.Keeper SupplyKeeper supply.Keeper + GovKeeper gov.Keeper + BankKeeper bank.Keeper } // encoders can be nil to accept the defaults, or set it to override some of the message handlers (like default) @@ -71,6 +77,7 @@ func CreateTestInput(t *testing.T, isCheckTx bool, tempDir string, supportedFeat keyDistro := sdk.NewKVStoreKey(distribution.StoreKey) keyParams := sdk.NewKVStoreKey(params.StoreKey) tkeyParams := sdk.NewTransientStoreKey(params.TStoreKey) + keyGov := sdk.NewKVStoreKey(govtypes.StoreKey) db := dbm.NewMemDB() ms := store.NewCommitMultiStore(db) @@ -81,6 +88,7 @@ func CreateTestInput(t *testing.T, isCheckTx bool, tempDir string, supportedFeat ms.MountStoreWithDB(keySupply, sdk.StoreTypeIAVL, db) ms.MountStoreWithDB(keyDistro, sdk.StoreTypeIAVL, db) ms.MountStoreWithDB(tkeyParams, sdk.StoreTypeTransient, db) + ms.MountStoreWithDB(keyGov, sdk.StoreTypeIAVL, db) err := ms.LoadLatestVersion() require.Nil(t, err) @@ -113,7 +121,7 @@ func CreateTestInput(t *testing.T, isCheckTx bool, tempDir string, supportedFeat //mint.ModuleName: {supply.Minter}, staking.BondedPoolName: {supply.Burner, supply.Staking}, staking.NotBondedPoolName: {supply.Burner, supply.Staking}, - //gov.ModuleName: {supply.Burner}, + gov.ModuleName: {supply.Burner}, } supplyKeeper := supply.NewKeeper(cdc, keySupply, accountKeeper, bankKeeper, maccPerms) @@ -164,12 +172,27 @@ func CreateTestInput(t *testing.T, isCheckTx bool, tempDir string, supportedFeat // add wasm handler so we can loop-back (contracts calling contracts) router.AddRoute(wasmTypes.RouterKey, TestHandler(keeper)) + govRouter := gov.NewRouter(). + AddRoute(govtypes.RouterKey, govtypes.ProposalHandler). + AddRoute(wasmTypes.RouterKey, NewWasmProposalHandler(keeper, wasmTypes.DefaultEnabledProposals)) + + govKeeper := gov.NewKeeper( + cdc, keyGov, pk.Subspace(govtypes.DefaultParamspace).WithKeyTable(gov.ParamKeyTable()), supplyKeeper, stakingKeeper, govRouter, + ) + + govKeeper.SetProposalID(ctx, govtypes.DefaultStartingProposalID) + govKeeper.SetDepositParams(ctx, govtypes.DefaultDepositParams()) + govKeeper.SetVotingParams(ctx, govtypes.DefaultVotingParams()) + govKeeper.SetTallyParams(ctx, govtypes.DefaultTallyParams()) + keepers := TestKeepers{ AccountKeeper: accountKeeper, SupplyKeeper: supplyKeeper, StakingKeeper: stakingKeeper, DistKeeper: distKeeper, WasmKeeper: keeper, + GovKeeper: govKeeper, + BankKeeper: bankKeeper, } return ctx, keepers } diff --git a/x/wasm/internal/types/codec.go b/x/wasm/internal/types/codec.go index 2aa5f784..92dcf7b0 100644 --- a/x/wasm/internal/types/codec.go +++ b/x/wasm/internal/types/codec.go @@ -2,7 +2,6 @@ package types import ( "github.com/cosmos/cosmos-sdk/codec" - // "github.com/cosmos/cosmos-sdk/x/supply/exported" ) // RegisterCodec registers the account types and interface @@ -13,6 +12,12 @@ func RegisterCodec(cdc *codec.Codec) { cdc.RegisterConcrete(&MsgMigrateContract{}, "wasm/migrate", nil) cdc.RegisterConcrete(&MsgUpdateAdmin{}, "wasm/update-contract-admin", nil) cdc.RegisterConcrete(&MsgClearAdmin{}, "wasm/clear-contract-admin", nil) + + cdc.RegisterConcrete(&StoreCodeProposal{}, "wasm/store-proposal", nil) + cdc.RegisterConcrete(&InstantiateContractProposal{}, "wasm/instantiate-proposal", nil) + cdc.RegisterConcrete(&MigrateContractProposal{}, "wasm/migrate-proposal", nil) + cdc.RegisterConcrete(&UpdateAdminProposal{}, "wasm/update-admin-proposal", nil) + cdc.RegisterConcrete(&ClearAdminProposal{}, "wasm/clear-admin-proposal", nil) } // ModuleCdc generic sealed codec to be used throughout module diff --git a/x/wasm/internal/types/msg.go b/x/wasm/internal/types/msg.go index f638c286..36ce349c 100644 --- a/x/wasm/internal/types/msg.go +++ b/x/wasm/internal/types/msg.go @@ -83,8 +83,8 @@ func (msg MsgInstantiateContract) ValidateBasic() error { return err } - if msg.InitFunds.IsAnyNegative() { - return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, "negative InitFunds") + if !msg.InitFunds.IsValid() { + return sdkerrors.ErrInvalidCoins } if len(msg.Admin) != 0 { diff --git a/x/wasm/internal/types/params.go b/x/wasm/internal/types/params.go new file mode 100644 index 00000000..24139418 --- /dev/null +++ b/x/wasm/internal/types/params.go @@ -0,0 +1,107 @@ +package types + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/params" +) + +const ( + // DefaultParamspace for params keeper + DefaultParamspace = ModuleName +) + +var ParamStoreKeyUploadAccess = []byte("uploadAccess") +var ParamStoreKeyInstantiateAccess = []byte("instantiateAccess") + +type AccessType uint8 + +const ( + Undefined AccessType = 0 + Nobody AccessType = 1 + OnlyAddress AccessType = 2 + Everybody AccessType = 3 +) + +func (a AccessType) With(addr sdk.AccAddress) AccessConfig { + switch a { + case Nobody: + return AllowNobody + case OnlyAddress: + if err := sdk.VerifyAddressFormat(addr); err != nil { + panic(err) + } + return AccessConfig{Type: OnlyAddress, Address: addr} + case Everybody: + return AllowEverybody + } + panic("unsupported access type") +} + +type AccessConfig struct { + Type AccessType `json:"type"` + Address sdk.AccAddress `json:"address"` +} + +var ( + DefaultUploadAccess = AllowEverybody + AllowEverybody = AccessConfig{Type: Everybody} + AllowNobody = AccessConfig{Type: Nobody} +) + +// ParamKeyTable type declaration for parameters +func ParamKeyTable() params.KeyTable { + return params.NewKeyTable( + params.NewParamSetPair(ParamStoreKeyUploadAccess, AllowEverybody, validateAccessConfig), + params.NewParamSetPair(ParamStoreKeyInstantiateAccess, Everybody, validateAccessType), + ) +} + +func validateAccessConfig(i interface{}) error { + v, ok := i.(AccessConfig) + if !ok { + return fmt.Errorf("invalid parameter type: %T", i) + } + return v.ValidateBasic() +} + +func validateAccessType(i interface{}) error { + v, ok := i.(AccessType) + if !ok { + return fmt.Errorf("invalid parameter type: %T", i) + } + if v == Undefined { + return sdkerrors.Wrap(ErrEmpty, "type") + } + // TODO: should we prevent Nobody here? + return nil +} + +func (v AccessConfig) ValidateBasic() error { + switch v.Type { + case Undefined: + return sdkerrors.Wrap(ErrEmpty, "type") + case Nobody, Everybody: + if len(v.Address) != 0 { + return sdkerrors.Wrap(ErrInvalid, "address not allowed for this type") + } + case OnlyAddress: + return sdk.VerifyAddressFormat(v.Address) + } + return sdkerrors.Wrap(ErrInvalid, "unknown type") +} + +func (v AccessConfig) Allowed(actor sdk.AccAddress) bool { + switch v.Type { + case Nobody: + return false + case Everybody: + return true + case OnlyAddress: + return v.Address.Equals(actor) + default: + panic("unknown type") + } +} diff --git a/x/wasm/internal/types/proposal.go b/x/wasm/internal/types/proposal.go new file mode 100644 index 00000000..c283837a --- /dev/null +++ b/x/wasm/internal/types/proposal.go @@ -0,0 +1,292 @@ +package types + +import ( + "encoding/json" + "fmt" + "strings" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" +) + +const ( + ProposalTypeStoreCode = "StoreCode" + ProposalTypeStoreInstantiateContract = "InstantiateContract" + ProposalTypeMigrateContract = "MigrateContract" + ProposalTypeUpdateAdmin = "UpdateAdmin" + ProposalTypeClearAdmin = "ClearAdmin" +) + +var DefaultEnabledProposals = map[string]struct{}{ + ProposalTypeStoreCode: {}, + ProposalTypeStoreInstantiateContract: {}, + ProposalTypeMigrateContract: {}, + ProposalTypeUpdateAdmin: {}, + ProposalTypeClearAdmin: {}, +} + +type WasmProposal struct { + Title string `json:"title" yaml:"title"` + Description string `json:"description" yaml:"description"` +} + +// GetTitle returns the title of a parameter change proposal. +func (p WasmProposal) GetTitle() string { return p.Title } + +// GetDescription returns the description of a parameter change proposal. +func (p WasmProposal) GetDescription() string { return p.Description } + +// ProposalRoute returns the routing key of a parameter change proposal. +func (p WasmProposal) ProposalRoute() string { return RouterKey } + +// ValidateBasic validates the proposal +func (p WasmProposal) ValidateBasic() error { + if strings.TrimSpace(p.Title) != p.Title { + return sdkerrors.Wrap(govtypes.ErrInvalidProposalContent, "proposal title must not start/end with white spaces") + } + if len(p.Title) == 0 { + return sdkerrors.Wrap(govtypes.ErrInvalidProposalContent, "proposal title cannot be blank") + } + if len(p.Title) > govtypes.MaxTitleLength { + return sdkerrors.Wrapf(govtypes.ErrInvalidProposalContent, "proposal title is longer than max length of %d", govtypes.MaxTitleLength) + } + if strings.TrimSpace(p.Description) != p.Description { + return sdkerrors.Wrap(govtypes.ErrInvalidProposalContent, "proposal description must not start/end with white spaces") + } + if len(p.Description) == 0 { + return sdkerrors.Wrap(govtypes.ErrInvalidProposalContent, "proposal description cannot be blank") + } + if len(p.Description) > govtypes.MaxDescriptionLength { + return sdkerrors.Wrapf(govtypes.ErrInvalidProposalContent, "proposal description is longer than max length of %d", govtypes.MaxDescriptionLength) + } + return nil +} + +type StoreCodeProposal struct { + WasmProposal + // Creator is the address that "owns" the code object + Creator sdk.AccAddress `json:"creator" yaml:"creator"` + // WASMByteCode can be raw or gzip compressed + WASMByteCode []byte `json:"wasm_byte_code" yaml:"wasm_byte_code"` + // Source is a valid absolute HTTPS URI to the contract's source code, optional + Source string `json:"source" yaml:"source"` + // Builder is a valid docker image name with tag, optional + Builder string `json:"builder" yaml:"builder"` +} + +// ProposalType returns the type +func (p StoreCodeProposal) ProposalType() string { return ProposalTypeStoreCode } + +// ValidateBasic validates the proposal +func (p StoreCodeProposal) ValidateBasic() error { + if err := p.WasmProposal.ValidateBasic(); err != nil { + return err + } + if err := sdk.VerifyAddressFormat(p.Creator); err != nil { + return err + } + + if err := validateWasmCode(p.WASMByteCode); err != nil { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "code bytes %s", err.Error()) + } + + if err := validateSourceURL(p.Source); err != nil { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "source %s", err.Error()) + } + + if err := validateBuilder(p.Builder); err != nil { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "builder %s", err.Error()) + } + + return nil +} + +// String implements the Stringer interface. +func (p StoreCodeProposal) String() string { + return fmt.Sprintf(`Store Code Proposal: + Title: %s + Description: %s + Creator: %s + WasmCode: %X + Source: %s + Builder: %s +`, p.Title, p.Description, p.Creator, p.WASMByteCode, p.Source, p.Builder) +} + +type InstantiateContractProposal struct { + WasmProposal + // Creator is the address that pays the init funds + Creator sdk.AccAddress `json:"sender" yaml:"sender"` + // Admin is an optional address that can execute migrations + Admin sdk.AccAddress `json:"admin,omitempty" yaml:"admin"` + Code uint64 `json:"code_id" yaml:"code_id"` + Label string `json:"label" yaml:"label"` + InitMsg json.RawMessage `json:"init_msg" yaml:"init_msg"` + InitFunds sdk.Coins `json:"init_funds" yaml:"init_funds"` +} + +// ProposalType returns the type +func (p InstantiateContractProposal) ProposalType() string { + return ProposalTypeStoreInstantiateContract +} + +// ValidateBasic validates the proposal +func (p InstantiateContractProposal) ValidateBasic() error { + if err := p.WasmProposal.ValidateBasic(); err != nil { + return err + } + if err := sdk.VerifyAddressFormat(p.Creator); err != nil { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "creator is required") + } + + if p.Code == 0 { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "code_id is required") + } + + if err := validateLabel(p.Label); err != nil { + return err + } + + if !p.InitFunds.IsValid() { + return sdkerrors.ErrInvalidCoins + } + + if len(p.Admin) != 0 { + if err := sdk.VerifyAddressFormat(p.Admin); err != nil { + return err + } + } + return nil + +} + +// String implements the Stringer interface. +func (p InstantiateContractProposal) String() string { + return fmt.Sprintf(`Instantiate Code Proposal: + Title: %s + Description: %s + Creator: %s + Admin: %s + Code id: %d + Label: %s + InitMsg: %q + InitFunds: %s +`, p.Title, p.Description, p.Creator, p.Admin, p.Code, p.Label, p.InitMsg, p.InitFunds) + +} + +type MigrateContractProposal struct { + WasmProposal + Contract sdk.AccAddress `json:"contract" yaml:"contract"` + Code uint64 `json:"code_id" yaml:"code_id"` + MigrateMsg json.RawMessage `json:"msg" yaml:"msg"` + // Sender is the role that is passed to the contract's environment + Sender sdk.AccAddress `json:"sender" yaml:"sender"` +} + +// ProposalType returns the type +func (p MigrateContractProposal) ProposalType() string { return ProposalTypeMigrateContract } + +// ValidateBasic validates the proposal +func (p MigrateContractProposal) ValidateBasic() error { + if err := p.WasmProposal.ValidateBasic(); err != nil { + return err + } + if p.Code == 0 { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "code_id is required") + } + if err := sdk.VerifyAddressFormat(p.Contract); err != nil { + return sdkerrors.Wrap(err, "contract") + } + if err := sdk.VerifyAddressFormat(p.Sender); err != nil { + return sdkerrors.Wrap(err, "sender") + } + return nil +} + +// String implements the Stringer interface. +func (p MigrateContractProposal) String() string { + return fmt.Sprintf(`Migrate Contract Proposal: + Title: %s + Description: %s + Contract: %s + Code id: %d + Sender: %s + MigrateMsg %q +`, p.Title, p.Description, p.Contract, p.Code, p.Sender, p.MigrateMsg) +} + +type UpdateAdminProposal struct { + WasmProposal + NewAdmin sdk.AccAddress `json:"new_admin" yaml:"new_admin"` + Contract sdk.AccAddress `json:"contract" yaml:"contract"` + // Sender is the role that is passed to the contract's environment + Sender sdk.AccAddress `json:"sender" yaml:"sender"` +} + +// ProposalType returns the type +func (p UpdateAdminProposal) ProposalType() string { return ProposalTypeUpdateAdmin } + +// ValidateBasic validates the proposal +func (p UpdateAdminProposal) ValidateBasic() error { + if err := p.WasmProposal.ValidateBasic(); err != nil { + return err + } + if err := sdk.VerifyAddressFormat(p.Contract); err != nil { + return sdkerrors.Wrap(err, "contract") + } + if err := sdk.VerifyAddressFormat(p.NewAdmin); err != nil { + return sdkerrors.Wrap(err, "new admin") + } + if err := sdk.VerifyAddressFormat(p.Sender); err != nil { + return sdkerrors.Wrap(err, "sender") + } + return nil +} + +// String implements the Stringer interface. +func (p UpdateAdminProposal) String() string { + return fmt.Sprintf(`Update Contract Admin Proposal: + Title: %s + Description: %s + Contract: %s + Sender: %s + New Admin: %s +`, p.Title, p.Description, p.Contract, p.Sender, p.NewAdmin) +} + +type ClearAdminProposal struct { + WasmProposal + + Contract sdk.AccAddress `json:"contract" yaml:"contract"` + // Sender is the role that is passed to the contract's environment + Sender sdk.AccAddress `json:"sender" yaml:"sender"` +} + +// ProposalType returns the type +func (p ClearAdminProposal) ProposalType() string { return ProposalTypeClearAdmin } + +// ValidateBasic validates the proposal +func (p ClearAdminProposal) ValidateBasic() error { + if err := p.WasmProposal.ValidateBasic(); err != nil { + return err + } + if err := sdk.VerifyAddressFormat(p.Contract); err != nil { + return sdkerrors.Wrap(err, "contract") + } + if err := sdk.VerifyAddressFormat(p.Sender); err != nil { + return sdkerrors.Wrap(err, "sender") + } + return nil +} + +// String implements the Stringer interface. +func (p ClearAdminProposal) String() string { + return fmt.Sprintf(`Clear Contract Admin Proposal: + Title: %s + Description: %s + Contract: %s + Sender: %s +`, p.Title, p.Description, p.Contract, p.Sender) +} diff --git a/x/wasm/internal/types/proposal_test.go b/x/wasm/internal/types/proposal_test.go new file mode 100644 index 00000000..dce04846 --- /dev/null +++ b/x/wasm/internal/types/proposal_test.go @@ -0,0 +1,534 @@ +package types + +import ( + "bytes" + "strings" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/gov" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestValidateWasmProposal(t *testing.T) { + specs := map[string]struct { + src WasmProposal + expErr bool + }{ + "all good": {src: WasmProposal{ + Title: "Foo", + Description: "Bar", + }}, + "prevent empty title": { + src: WasmProposal{ + Description: "Bar", + }, + expErr: true, + }, + "prevent white space only title": { + src: WasmProposal{ + Title: " ", + Description: "Bar", + }, + expErr: true, + }, + "prevent leading white spaces in title": { + src: WasmProposal{ + Title: " Foo", + Description: "Bar", + }, + expErr: true, + }, + "prevent title exceeds max length ": { + src: WasmProposal{ + Title: strings.Repeat("a", govtypes.MaxTitleLength+1), + Description: "Bar", + }, + expErr: true, + }, + "prevent empty description": { + src: WasmProposal{ + Title: "Foo", + }, + expErr: true, + }, + "prevent leading white spaces in description": { + src: WasmProposal{ + Title: "Foo", + Description: " Bar", + }, + expErr: true, + }, + "prevent white space only description": { + src: WasmProposal{ + Title: "Foo", + Description: " ", + }, + expErr: true, + }, + "prevent descr exceeds max length ": { + src: WasmProposal{ + Title: "Foo", + Description: strings.Repeat("a", govtypes.MaxDescriptionLength+1), + }, + expErr: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + err := spec.src.ValidateBasic() + if spec.expErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestValidateStoreCodeProposal(t *testing.T) { + var ( + invalidAddress sdk.AccAddress = bytes.Repeat([]byte{0x1}, sdk.AddrLen-1) + ) + + specs := map[string]struct { + src StoreCodeProposal + expErr bool + }{ + "all good": { + src: StoreCodeProposalFixture(), + }, + "without source": { + src: StoreCodeProposalFixture(func(p *StoreCodeProposal) { + p.Source = "" + }), + }, + "base data missing": { + src: StoreCodeProposalFixture(func(p *StoreCodeProposal) { + p.WasmProposal = WasmProposal{} + }), + expErr: true, + }, + "creator missing": { + src: StoreCodeProposalFixture(func(p *StoreCodeProposal) { + p.Creator = nil + }), + expErr: true, + }, + "creator invalid": { + src: StoreCodeProposalFixture(func(p *StoreCodeProposal) { + p.Creator = invalidAddress + }), + expErr: true, + }, + "wasm code missing": { + src: StoreCodeProposalFixture(func(p *StoreCodeProposal) { + p.WASMByteCode = nil + }), + expErr: true, + }, + "wasm code invalid": { + src: StoreCodeProposalFixture(func(p *StoreCodeProposal) { + p.WASMByteCode = bytes.Repeat([]byte{0x0}, MaxWasmSize+1) + }), + expErr: true, + }, + "source invalid": { + src: StoreCodeProposalFixture(func(p *StoreCodeProposal) { + p.Source = "not an url" + }), + expErr: true, + }, + "builder invalid": { + src: StoreCodeProposalFixture(func(p *StoreCodeProposal) { + p.Builder = "not a builder" + }), + expErr: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + err := spec.src.ValidateBasic() + if spec.expErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestValidateInstantiateContractProposal(t *testing.T) { + var ( + invalidAddress sdk.AccAddress = bytes.Repeat([]byte{0x1}, sdk.AddrLen-1) + ) + + specs := map[string]struct { + src InstantiateContractProposal + expErr bool + }{ + "all good": { + src: InstantiateContractProposalFixture(), + }, + "without admin": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { + p.Admin = nil + }), + }, + "without init msg": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { + p.InitMsg = nil + }), + }, + "without init funds": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { + p.InitFunds = nil + }), + }, + "base data missing": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { + p.WasmProposal = WasmProposal{} + }), + expErr: true, + }, + "creator missing": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { + p.Creator = nil + }), + expErr: true, + }, + "creator invalid": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { + p.Creator = invalidAddress + }), + expErr: true, + }, + "admin invalid": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { + p.Admin = invalidAddress + }), + expErr: true, + }, + "code id empty": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { + p.Code = 0 + }), + expErr: true, + }, + "label empty": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { + p.Label = "" + }), + expErr: true, + }, + "init funds negative": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { + p.InitFunds = sdk.Coins{{Denom: "foo", Amount: sdk.NewInt(-1)}} + }), + expErr: true, + }, + "init funds with duplicates": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { + p.InitFunds = sdk.Coins{{Denom: "foo", Amount: sdk.NewInt(1)}, {Denom: "foo", Amount: sdk.NewInt(2)}} + }), + expErr: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + err := spec.src.ValidateBasic() + if spec.expErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestValidateMigrateContractProposal(t *testing.T) { + var ( + invalidAddress sdk.AccAddress = bytes.Repeat([]byte{0x1}, sdk.AddrLen-1) + ) + + specs := map[string]struct { + src MigrateContractProposal + expErr bool + }{ + "all good": { + src: MigrateContractProposalFixture(), + }, + "without migrate msg": { + src: MigrateContractProposalFixture(func(p *MigrateContractProposal) { + p.MigrateMsg = nil + }), + }, + "base data missing": { + src: MigrateContractProposalFixture(func(p *MigrateContractProposal) { + p.WasmProposal = WasmProposal{} + }), + expErr: true, + }, + "contract missing": { + src: MigrateContractProposalFixture(func(p *MigrateContractProposal) { + p.Contract = nil + }), + expErr: true, + }, + "contract invalid": { + src: MigrateContractProposalFixture(func(p *MigrateContractProposal) { + p.Contract = invalidAddress + }), + expErr: true, + }, + "code id empty": { + src: MigrateContractProposalFixture(func(p *MigrateContractProposal) { + p.Code = 0 + }), + expErr: true, + }, + "sender missing": { + src: MigrateContractProposalFixture(func(p *MigrateContractProposal) { + p.Sender = nil + }), + expErr: true, + }, + "sender invalid": { + src: MigrateContractProposalFixture(func(p *MigrateContractProposal) { + p.Sender = invalidAddress + }), + expErr: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + err := spec.src.ValidateBasic() + if spec.expErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestValidateUpdateAdminProposal(t *testing.T) { + var ( + invalidAddress sdk.AccAddress = bytes.Repeat([]byte{0x1}, sdk.AddrLen-1) + ) + + specs := map[string]struct { + src UpdateAdminProposal + expErr bool + }{ + "all good": { + src: UpdateAdminProposalFixture(), + }, + "base data missing": { + src: UpdateAdminProposalFixture(func(p *UpdateAdminProposal) { + p.WasmProposal = WasmProposal{} + }), + expErr: true, + }, + "contract missing": { + src: UpdateAdminProposalFixture(func(p *UpdateAdminProposal) { + p.Contract = nil + }), + expErr: true, + }, + "contract invalid": { + src: UpdateAdminProposalFixture(func(p *UpdateAdminProposal) { + p.Contract = invalidAddress + }), + expErr: true, + }, + "admin missing": { + src: UpdateAdminProposalFixture(func(p *UpdateAdminProposal) { + p.NewAdmin = nil + }), + expErr: true, + }, + "admin invalid": { + src: UpdateAdminProposalFixture(func(p *UpdateAdminProposal) { + p.NewAdmin = invalidAddress + }), + expErr: true, + }, + "sender missing": { + src: UpdateAdminProposalFixture(func(p *UpdateAdminProposal) { + p.Sender = nil + }), + expErr: true, + }, + "sender invalid": { + src: UpdateAdminProposalFixture(func(p *UpdateAdminProposal) { + p.Sender = invalidAddress + }), + expErr: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + err := spec.src.ValidateBasic() + if spec.expErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} +func TestValidateClearAdminProposal(t *testing.T) { + var ( + invalidAddress sdk.AccAddress = bytes.Repeat([]byte{0x1}, sdk.AddrLen-1) + ) + + specs := map[string]struct { + src ClearAdminProposal + expErr bool + }{ + "all good": { + src: ClearAdminProposalFixture(), + }, + "base data missing": { + src: ClearAdminProposalFixture(func(p *ClearAdminProposal) { + p.WasmProposal = WasmProposal{} + }), + expErr: true, + }, + "contract missing": { + src: ClearAdminProposalFixture(func(p *ClearAdminProposal) { + p.Contract = nil + }), + expErr: true, + }, + "contract invalid": { + src: ClearAdminProposalFixture(func(p *ClearAdminProposal) { + p.Contract = invalidAddress + }), + expErr: true, + }, + "sender missing": { + src: ClearAdminProposalFixture(func(p *ClearAdminProposal) { + p.Sender = nil + }), + expErr: true, + }, + "sender invalid": { + src: ClearAdminProposalFixture(func(p *ClearAdminProposal) { + p.Sender = invalidAddress + }), + expErr: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + err := spec.src.ValidateBasic() + if spec.expErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestProposalStrings(t *testing.T) { + specs := map[string]struct { + src gov.Content + exp string + }{ + "store code": { + src: StoreCodeProposalFixture(func(p *StoreCodeProposal) { + p.WASMByteCode = []byte{01, 02, 03, 04, 05, 06, 07, 0x08, 0x09, 0x0a} + }), + exp: `Store Code Proposal: + Title: Foo + Description: Bar + Creator: cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du + WasmCode: 0102030405060708090A + Source: https://example.com/code + Builder: foo/bar:latest +`, + }, + "instantiate contract": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { + p.InitFunds = sdk.Coins{{Denom: "foo", Amount: sdk.NewInt(1)}, {Denom: "bar", Amount: sdk.NewInt(2)}} + }), + exp: `Instantiate Code Proposal: + Title: Foo + Description: Bar + Creator: cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du + Admin: cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du + Code id: 1 + Label: testing + InitMsg: "{\"verifier\":\"cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du\",\"beneficiary\":\"cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du\"}" + InitFunds: 1foo,2bar +`, + }, + "instantiate contract without funds": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { p.InitFunds = nil }), + exp: `Instantiate Code Proposal: + Title: Foo + Description: Bar + Creator: cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du + Admin: cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du + Code id: 1 + Label: testing + InitMsg: "{\"verifier\":\"cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du\",\"beneficiary\":\"cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du\"}" + InitFunds: +`, + }, + "instantiate contract without admin": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { p.Admin = nil }), + exp: `Instantiate Code Proposal: + Title: Foo + Description: Bar + Creator: cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du + Admin: + Code id: 1 + Label: testing + InitMsg: "{\"verifier\":\"cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du\",\"beneficiary\":\"cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du\"}" + InitFunds: +`, + }, + "migrate contract": { + src: MigrateContractProposalFixture(), + exp: `Migrate Contract Proposal: + Title: Foo + Description: Bar + Contract: cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5 + Code id: 1 + Sender: cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du + MigrateMsg "{\"verifier\":\"cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du\"}" +`, + }, + "update admin": { + src: UpdateAdminProposalFixture(), + exp: `Update Contract Admin Proposal: + Title: Foo + Description: Bar + Contract: cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5 + Sender: cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du + New Admin: cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du +`, + }, + "clear admin": { + src: ClearAdminProposalFixture(), + exp: `Clear Contract Admin Proposal: + Title: Foo + Description: Bar + Contract: cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5 + Sender: cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du +`, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + assert.Equal(t, spec.exp, spec.src.String()) + }) + } + +} diff --git a/x/wasm/internal/types/test_fixtures.go b/x/wasm/internal/types/test_fixtures.go index 4bae1f0b..6e9a432d 100644 --- a/x/wasm/internal/types/test_fixtures.go +++ b/x/wasm/internal/types/test_fixtures.go @@ -3,7 +3,9 @@ package types import ( "bytes" "crypto/sha256" + "encoding/json" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/tendermint/tendermint/libs/rand" ) @@ -101,3 +103,144 @@ func ContractInfoFixture(mutators ...func(*ContractInfo)) ContractInfo { } return fixture } + +func WithSHA256CodeHash(wasmCode []byte) func(info *CodeInfo) { + return func(info *CodeInfo) { + codeHash := sha256.Sum256(wasmCode) + info.CodeHash = codeHash[:] + } +} + +func StoreCodeProposalFixture(mutators ...func(*StoreCodeProposal)) StoreCodeProposal { + var anyValidAddress sdk.AccAddress = bytes.Repeat([]byte{0x1}, sdk.AddrLen) + p := StoreCodeProposal{ + WasmProposal: WasmProposal{ + Title: "Foo", + Description: "Bar", + }, + Creator: anyValidAddress, + WASMByteCode: []byte{0x0}, + Source: "https://example.com/code", + Builder: "foo/bar:latest", + } + for _, m := range mutators { + m(&p) + } + return p +} + +func InstantiateContractProposalFixture(mutators ...func(p *InstantiateContractProposal)) InstantiateContractProposal { + var ( + anyValidAddress sdk.AccAddress = bytes.Repeat([]byte{0x1}, sdk.AddrLen) + + initMsg = struct { + Verifier sdk.AccAddress `json:"verifier"` + Beneficiary sdk.AccAddress `json:"beneficiary"` + }{ + Verifier: anyValidAddress, + Beneficiary: anyValidAddress, + } + ) + + initMsgBz, err := json.Marshal(initMsg) + if err != nil { + panic(err) + } + p := InstantiateContractProposal{ + WasmProposal: WasmProposal{ + Title: "Foo", + Description: "Bar", + }, + Creator: anyValidAddress, + Admin: anyValidAddress, + Code: 1, + Label: "testing", + InitMsg: initMsgBz, + InitFunds: nil, + } + + for _, m := range mutators { + m(&p) + } + return p +} + +func MigrateContractProposalFixture(mutators ...func(p *MigrateContractProposal)) MigrateContractProposal { + var ( + anyValidAddress sdk.AccAddress = bytes.Repeat([]byte{0x1}, sdk.AddrLen) + + migMsg = struct { + Verifier sdk.AccAddress `json:"verifier"` + }{Verifier: anyValidAddress} + ) + + migMsgBz, err := json.Marshal(migMsg) + if err != nil { + panic(err) + } + contractAddr, err := sdk.AccAddressFromBech32("cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5") + if err != nil { + panic(err) + } + + p := MigrateContractProposal{ + WasmProposal: WasmProposal{ + Title: "Foo", + Description: "Bar", + }, + Contract: contractAddr, + Code: 1, + MigrateMsg: migMsgBz, + Sender: anyValidAddress, + } + + for _, m := range mutators { + m(&p) + } + return p +} + +func UpdateAdminProposalFixture(mutators ...func(p *UpdateAdminProposal)) UpdateAdminProposal { + var anyValidAddress sdk.AccAddress = bytes.Repeat([]byte{0x1}, sdk.AddrLen) + + contractAddr, err := sdk.AccAddressFromBech32("cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5") + if err != nil { + panic(err) + } + + p := UpdateAdminProposal{ + WasmProposal: WasmProposal{ + Title: "Foo", + Description: "Bar", + }, + NewAdmin: anyValidAddress, + Contract: contractAddr, + Sender: anyValidAddress, + } + for _, m := range mutators { + m(&p) + } + return p +} + +func ClearAdminProposalFixture(mutators ...func(p *ClearAdminProposal)) ClearAdminProposal { + var anyValidAddress sdk.AccAddress = bytes.Repeat([]byte{0x1}, sdk.AddrLen) + + contractAddr, err := sdk.AccAddressFromBech32("cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5") + if err != nil { + panic(err) + } + + p := ClearAdminProposal{ + WasmProposal: WasmProposal{ + Title: "Foo", + Description: "Bar", + }, + Contract: contractAddr, + Sender: anyValidAddress, + } + for _, m := range mutators { + m(&p) + } + return p +}