diff --git a/x/wasm/keeper/keeper.go b/x/wasm/keeper/keeper.go index 142562a40..40d9cc176 100644 --- a/x/wasm/keeper/keeper.go +++ b/x/wasm/keeper/keeper.go @@ -447,8 +447,6 @@ func (k Keeper) migrate( defer telemetry.MeasureSince(time.Now(), "wasm", "contract", "migrate") sdkCtx := sdk.UnwrapSDKContext(ctx) - setupCost := k.gasRegister.SetupContractCost(k.IsPinnedCode(ctx, newCodeID), len(msg)) - sdkCtx.GasMeter().ConsumeGas(setupCost, "Loading CosmWasm module: migrate") contractInfo := k.GetContractInfo(ctx, contractAddress) if contractInfo == nil { @@ -468,7 +466,8 @@ func (k Keeper) migrate( } // check for IBC flag - switch report, err := k.wasmVM.AnalyzeCode(newCodeInfo.CodeHash); { + report, err := k.wasmVM.AnalyzeCode(newCodeInfo.CodeHash) + switch { case err != nil: return nil, errorsmod.Wrap(types.ErrVMError, err.Error()) case !report.HasIBCEntryPoints && contractInfo.IBCPortID != "": @@ -483,26 +482,25 @@ func (k Keeper) migrate( contractInfo.IBCPortID = ibcPort } - env := types.NewEnv(sdkCtx, contractAddress) + var response *wasmvmtypes.Response - // prepare querier - querier := k.newQueryHandler(sdkCtx, contractAddress) - - prefixStoreKey := types.GetContractStorePrefix(contractAddress) - vmStore := types.NewStoreAdapter(prefix.NewStore(runtime.KVStoreAdapter(k.storeService.OpenKVStore(sdkCtx)), prefixStoreKey)) - gasLeft := k.runtimeGasForContract(sdkCtx) - res, gasUsed, err := k.wasmVM.Migrate(newCodeInfo.CodeHash, env, msg, vmStore, cosmwasmAPI, &querier, k.gasMeter(sdkCtx), gasLeft, costJSONDeserialization) - k.consumeRuntimeGas(sdkCtx, gasUsed) + // check for migrate version + oldCodeInfo := k.GetCodeInfo(ctx, contractInfo.CodeID) + oldReport, err := k.wasmVM.AnalyzeCode(oldCodeInfo.CodeHash) if err != nil { return nil, errorsmod.Wrap(types.ErrVMError, err.Error()) } - if res == nil { - // If this gets executed, that's a bug in wasmvm - return nil, errorsmod.Wrap(types.ErrVMError, "internal wasmvm error") - } - if res.Err != "" { - return nil, types.MarkErrorDeterministic(errorsmod.Wrap(types.ErrMigrationFailed, res.Err)) + + // call migrate entrypoint, except if both migrate versions are set and the same value + if report.ContractMigrateVersion == nil || + oldReport.ContractMigrateVersion == nil || + *report.ContractMigrateVersion != *oldReport.ContractMigrateVersion { + response, err = k.callMigrateEntrypoint(sdkCtx, contractAddress, wasmvmtypes.Checksum(newCodeInfo.CodeHash), msg, newCodeID) + if err != nil { + return nil, err + } } + // delete old secondary index entry err = k.removeFromContractCodeSecondaryIndex(ctx, contractAddress, k.mustGetLastContractHistoryEntry(sdkCtx, contractAddress)) if err != nil { @@ -526,15 +524,62 @@ func (k Keeper) migrate( sdk.NewAttribute(types.AttributeKeyContractAddr, contractAddress.String()), )) - sdkCtx = types.WithSubMsgAuthzPolicy(sdkCtx, authZ.SubMessageAuthorizationPolicy(types.AuthZActionMigrateContract)) - data, err := k.handleContractResponse(sdkCtx, contractAddress, contractInfo.IBCPortID, res.Ok.Messages, res.Ok.Attributes, res.Ok.Data, res.Ok.Events) - if err != nil { - return nil, errorsmod.Wrap(err, "dispatch") + var data []byte + + // if migrate entry point was called + if response != nil { + sdkCtx = types.WithSubMsgAuthzPolicy(sdkCtx, authZ.SubMessageAuthorizationPolicy(types.AuthZActionMigrateContract)) + data, err = k.handleContractResponse( + sdkCtx, + contractAddress, + contractInfo.IBCPortID, + response.Messages, + response.Attributes, + response.Data, + response.Events, + ) + if err != nil { + return nil, errorsmod.Wrap(err, "dispatch") + } + return data, nil } return data, nil } +func (k Keeper) callMigrateEntrypoint( + sdkCtx sdk.Context, + contractAddress sdk.AccAddress, + newChecksum wasmvmtypes.Checksum, + msg []byte, + newCodeID uint64, +) (*wasmvmtypes.Response, error) { + setupCost := k.gasRegister.SetupContractCost(k.IsPinnedCode(sdkCtx, newCodeID), len(msg)) + sdkCtx.GasMeter().ConsumeGas(setupCost, "Loading CosmWasm module: migrate") + + env := types.NewEnv(sdkCtx, contractAddress) + + // prepare querier + querier := k.newQueryHandler(sdkCtx, contractAddress) + + prefixStoreKey := types.GetContractStorePrefix(contractAddress) + vmStore := types.NewStoreAdapter(prefix.NewStore(runtime.KVStoreAdapter(k.storeService.OpenKVStore(sdkCtx)), prefixStoreKey)) + gasLeft := k.runtimeGasForContract(sdkCtx) + res, gasUsed, err := k.wasmVM.Migrate(newChecksum, env, msg, vmStore, cosmwasmAPI, &querier, k.gasMeter(sdkCtx), gasLeft, costJSONDeserialization) + k.consumeRuntimeGas(sdkCtx, gasUsed) + if err != nil { + return nil, errorsmod.Wrap(types.ErrVMError, err.Error()) + } + if res == nil { + // If this gets executed, that's a bug in wasmvm + return nil, errorsmod.Wrap(types.ErrVMError, "internal wasmvm error") + } + if res.Err != "" { + return nil, types.MarkErrorDeterministic(errorsmod.Wrap(types.ErrMigrationFailed, res.Err)) + } + return res.Ok, nil +} + // Sudo allows privileged access to a contract. This can never be called by an external tx, but only by // another native Go module directly, or on-chain governance (if sudo proposals are enabled). 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) diff --git a/x/wasm/keeper/keeper_test.go b/x/wasm/keeper/keeper_test.go index 374391118..9280a4ad9 100644 --- a/x/wasm/keeper/keeper_test.go +++ b/x/wasm/keeper/keeper_test.go @@ -1222,6 +1222,10 @@ func TestMigrate(t *testing.T) { require.NoError(t, keeper.SetAccessConfig(parentCtx, restrictedCodeExample.CodeID, restrictedCodeExample.CreatorAddr, types.AllowNobody)) require.NotEqual(t, originalCodeID, restrictedCodeExample.CodeID) + // store hackatom contracts with "migrate_version" attributes + hackatom42 := StoreExampleContract(t, parentCtx, keepers, "./testdata/hackatom_42.wasm") + hackatom420 := StoreExampleContract(t, parentCtx, keepers, "./testdata/hackatom_420.wasm") + anyAddr := RandomAccountAddress(t) newVerifierAddr := RandomAccountAddress(t) initMsgBz := HackatomExampleInitMsg{ @@ -1351,6 +1355,51 @@ func TestMigrate(t *testing.T) { migrateMsg: migMsgBz, expErr: types.ErrMigrationFailed, }, + "all good with migrate versions": { + admin: creator, + caller: creator, + initMsg: initMsgBz, + fromCodeID: hackatom42.CodeID, + toCodeID: hackatom420.CodeID, + migrateMsg: migMsgBz, + expVerifier: newVerifierAddr, + }, + "all good with no migrate version to migrate version contract": { + admin: creator, + caller: creator, + initMsg: initMsgBz, + fromCodeID: originalCodeID, + toCodeID: hackatom42.CodeID, + migrateMsg: migMsgBz, + expVerifier: newVerifierAddr, + }, + "all good with same migrate version": { + admin: creator, + caller: creator, + initMsg: initMsgBz, + fromCodeID: hackatom42.CodeID, + toCodeID: hackatom42.CodeID, + migrateMsg: migMsgBz, + expVerifier: fred, // not updated + }, + "all good with migrate version contract to no migrate version contract": { + admin: creator, + caller: creator, + initMsg: initMsgBz, + fromCodeID: hackatom42.CodeID, + toCodeID: originalCodeID, + migrateMsg: migMsgBz, + expVerifier: newVerifierAddr, + }, + "all good with migration to older migrate version": { + admin: creator, + caller: creator, + initMsg: initMsgBz, + fromCodeID: hackatom420.CodeID, + toCodeID: hackatom42.CodeID, + migrateMsg: migMsgBz, + expVerifier: newVerifierAddr, + }, } blockHeight := parentCtx.BlockHeight() diff --git a/x/wasm/keeper/testdata/hackatom_42.wasm b/x/wasm/keeper/testdata/hackatom_42.wasm new file mode 100644 index 000000000..e4b36b206 Binary files /dev/null and b/x/wasm/keeper/testdata/hackatom_42.wasm differ diff --git a/x/wasm/keeper/testdata/hackatom_420.wasm b/x/wasm/keeper/testdata/hackatom_420.wasm new file mode 100644 index 000000000..6baf02d83 Binary files /dev/null and b/x/wasm/keeper/testdata/hackatom_420.wasm differ