github.com/fibonacci-chain/fbc@v0.0.0-20231124064014-c7636198c1e9/x/wasm/keeper/submsg_test.go (about) 1 package keeper 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io/ioutil" 7 "strconv" 8 "testing" 9 10 ibcadapter "github.com/fibonacci-chain/fbc/libs/cosmos-sdk/types/ibc-adapter" 11 "github.com/fibonacci-chain/fbc/x/wasm/keeper/testdata" 12 13 "github.com/fibonacci-chain/fbc/x/wasm/types" 14 15 "github.com/stretchr/testify/assert" 16 17 wasmvmtypes "github.com/CosmWasm/wasmvm/types" 18 sdk "github.com/fibonacci-chain/fbc/libs/cosmos-sdk/types" 19 "github.com/stretchr/testify/require" 20 ) 21 22 // test handing of submessages, very closely related to the reflect_test 23 24 // Try a simple send, no gas limit to for a sanity check before trying table tests 25 func TestDispatchSubMsgSuccessCase(t *testing.T) { 26 ctx, keepers := CreateTestInput(t, false, ReflectFeatures) 27 accKeeper, keeper, bankKeeper := keepers.AccountKeeper, keepers.WasmKeeper, keepers.BankKeeper 28 29 deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) 30 contractStart := sdk.NewCoins(sdk.NewInt64Coin("denom", 40000)) 31 32 creator := keepers.Faucet.NewFundedAccount(ctx, deposit...) 33 creatorBalance := deposit.Sub(contractStart) 34 _, _, fred := keyPubAddr() 35 36 // upload code 37 codeID, err := keepers.ContractKeeper.Create(ctx, creator, testdata.ReflectContractWasm(), nil) 38 require.NoError(t, err) 39 require.Equal(t, uint64(1), codeID) 40 41 // creator instantiates a contract and gives it tokens 42 contractAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, codeID, creator, nil, []byte("{}"), "reflect contract 1", contractStart) 43 require.NoError(t, err) 44 require.NotEmpty(t, contractAddr) 45 46 // check some account values 47 checkAccount(t, ctx, accKeeper, bankKeeper, contractAddr, contractStart) 48 checkAccount(t, ctx, accKeeper, bankKeeper, creator, creatorBalance) 49 checkAccount(t, ctx, accKeeper, bankKeeper, fred, nil) 50 51 // creator can send contract's tokens to fred (using SendMsg) 52 msg := wasmvmtypes.CosmosMsg{ 53 Bank: &wasmvmtypes.BankMsg{ 54 Send: &wasmvmtypes.SendMsg{ 55 ToAddress: fred.String(), 56 Amount: []wasmvmtypes.Coin{{ 57 Denom: "denom", 58 Amount: "15000000000000000000000", 59 }}, 60 }, 61 }, 62 } 63 reflectSend := testdata.ReflectHandleMsg{ 64 ReflectSubMsg: &testdata.ReflectSubPayload{ 65 Msgs: []wasmvmtypes.SubMsg{{ 66 ID: 7, 67 Msg: msg, 68 ReplyOn: wasmvmtypes.ReplyAlways, 69 }}, 70 }, 71 } 72 reflectSendBz, err := json.Marshal(reflectSend) 73 require.NoError(t, err) 74 _, err = keepers.ContractKeeper.Execute(ctx, contractAddr, creator, reflectSendBz, nil) 75 require.NoError(t, err) 76 77 // fred got coins 78 checkAccount(t, ctx, accKeeper, bankKeeper, fred, sdk.NewCoins(sdk.NewInt64Coin("denom", 15000))) 79 // contract lost them 80 checkAccount(t, ctx, accKeeper, bankKeeper, contractAddr, sdk.NewCoins(sdk.NewInt64Coin("denom", 25000))) 81 checkAccount(t, ctx, accKeeper, bankKeeper, creator, creatorBalance) 82 83 // query the reflect state to ensure the result was stored 84 query := testdata.ReflectQueryMsg{ 85 SubMsgResult: &testdata.SubCall{ID: 7}, 86 } 87 queryBz, err := json.Marshal(query) 88 require.NoError(t, err) 89 queryRes, err := keeper.QuerySmart(ctx, contractAddr, queryBz) 90 require.NoError(t, err) 91 92 var res wasmvmtypes.Reply 93 err = json.Unmarshal(queryRes, &res) 94 require.NoError(t, err) 95 assert.Equal(t, uint64(7), res.ID) 96 assert.Empty(t, res.Result.Err) 97 require.NotNil(t, res.Result.Ok) 98 sub := res.Result.Ok 99 // as of v0.28.0 we strip out all events that don't come from wasm contracts. can't trust the sdk. 100 require.Len(t, sub.Events, 0) 101 } 102 103 func TestDispatchSubMsgErrorHandling(t *testing.T) { 104 fundedDenom := "funds" 105 fundedAmount := 1_000_000 106 ctxGasLimit := uint64(1_000_000) 107 subGasLimit := uint64(300_000) 108 109 // prep - create one chain and upload the code 110 ctx, keepers := CreateTestInput(t, false, ReflectFeatures) 111 ctx.SetGasMeter(sdk.NewInfiniteGasMeter()) 112 ctx.SetBlockGasMeter(sdk.NewInfiniteGasMeter()) 113 keeper := keepers.WasmKeeper 114 contractStart := sdk.NewCoins(sdk.NewInt64Coin(fundedDenom, int64(fundedAmount))) 115 uploader := keepers.Faucet.NewFundedAccount(ctx, contractStart.Add(contractStart...)...) 116 117 // upload code 118 reflectID, err := keepers.ContractKeeper.Create(ctx, uploader, testdata.ReflectContractWasm(), nil) 119 require.NoError(t, err) 120 121 // create hackatom contract for testing (for infinite loop) 122 hackatomCode, err := ioutil.ReadFile("./testdata/hackatom.wasm") 123 require.NoError(t, err) 124 hackatomID, err := keepers.ContractKeeper.Create(ctx, uploader, hackatomCode, nil) 125 require.NoError(t, err) 126 _, _, bob := keyPubAddr() 127 _, _, fred := keyPubAddr() 128 initMsg := HackatomExampleInitMsg{ 129 Verifier: fred, 130 Beneficiary: bob, 131 } 132 initMsgBz, err := json.Marshal(initMsg) 133 require.NoError(t, err) 134 hackatomAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, hackatomID, uploader, nil, initMsgBz, "hackatom demo", contractStart) 135 require.NoError(t, err) 136 137 validBankSend := func(contract, emptyAccount string) wasmvmtypes.CosmosMsg { 138 return wasmvmtypes.CosmosMsg{ 139 Bank: &wasmvmtypes.BankMsg{ 140 Send: &wasmvmtypes.SendMsg{ 141 ToAddress: emptyAccount, 142 Amount: []wasmvmtypes.Coin{{ 143 Denom: fundedDenom, 144 Amount: strconv.Itoa(fundedAmount/2) + "000000000000000000", 145 }}, 146 }, 147 }, 148 } 149 } 150 151 invalidBankSend := func(contract, emptyAccount string) wasmvmtypes.CosmosMsg { 152 return wasmvmtypes.CosmosMsg{ 153 Bank: &wasmvmtypes.BankMsg{ 154 Send: &wasmvmtypes.SendMsg{ 155 ToAddress: emptyAccount, 156 Amount: []wasmvmtypes.Coin{{ 157 Denom: fundedDenom, 158 Amount: strconv.Itoa(fundedAmount*2) + "000000000000000000", 159 }}, 160 }, 161 }, 162 } 163 } 164 165 infiniteLoop := func(contract, emptyAccount string) wasmvmtypes.CosmosMsg { 166 return wasmvmtypes.CosmosMsg{ 167 Wasm: &wasmvmtypes.WasmMsg{ 168 Execute: &wasmvmtypes.ExecuteMsg{ 169 ContractAddr: hackatomAddr.String(), 170 Msg: []byte(`{"cpu_loop":{}}`), 171 }, 172 }, 173 } 174 } 175 176 instantiateContract := func(contract, emptyAccount string) wasmvmtypes.CosmosMsg { 177 return wasmvmtypes.CosmosMsg{ 178 Wasm: &wasmvmtypes.WasmMsg{ 179 Instantiate: &wasmvmtypes.InstantiateMsg{ 180 CodeID: reflectID, 181 Msg: []byte("{}"), 182 Label: "subcall reflect", 183 }, 184 }, 185 } 186 } 187 188 type assertion func(t *testing.T, ctx sdk.Context, contract, emptyAccount string, response wasmvmtypes.SubMsgResult) 189 190 assertReturnedEvents := func(expectedEvents int) assertion { 191 return func(t *testing.T, ctx sdk.Context, contract, emptyAccount string, response wasmvmtypes.SubMsgResult) { 192 require.Len(t, response.Ok.Events, expectedEvents) 193 } 194 } 195 196 assertGasUsed := func(minGas, maxGas uint64) assertion { 197 return func(t *testing.T, ctx sdk.Context, contract, emptyAccount string, response wasmvmtypes.SubMsgResult) { 198 gasUsed := ctx.GasMeter().GasConsumed() 199 assert.True(t, gasUsed >= minGas, "Used %d gas (less than expected %d)", gasUsed, minGas) 200 assert.True(t, gasUsed <= maxGas, "Used %d gas (more than expected %d)", gasUsed, maxGas) 201 } 202 } 203 204 assertErrorString := func(shouldContain string) assertion { 205 return func(t *testing.T, ctx sdk.Context, contract, emptyAccount string, response wasmvmtypes.SubMsgResult) { 206 assert.Contains(t, response.Err, shouldContain) 207 } 208 } 209 210 assertGotContractAddr := func(t *testing.T, ctx sdk.Context, contract, emptyAccount string, response wasmvmtypes.SubMsgResult) { 211 // should get the events emitted on new contract 212 event := response.Ok.Events[0] 213 require.Equal(t, event.Type, "instantiate") 214 assert.Equal(t, event.Attributes[0].Key, "_contract_address") 215 eventAddr := event.Attributes[0].Value 216 assert.NotEqual(t, contract, eventAddr) 217 218 var res types.MsgInstantiateContractResponse 219 keepers.EncodingConfig.Marshaler.GetProtocMarshal().MustUnmarshal(response.Ok.Data, &res) 220 assert.Equal(t, eventAddr, res.Address) 221 } 222 223 cases := map[string]struct { 224 submsgID uint64 225 // we will generate message from the 226 msg func(contract, emptyAccount string) wasmvmtypes.CosmosMsg 227 gasLimit *uint64 228 229 // true if we expect this to throw out of gas panic 230 isOutOfGasPanic bool 231 // true if we expect this execute to return an error (can be false when submessage errors) 232 executeError bool 233 // true if we expect submessage to return an error (but execute to return success) 234 subMsgError bool 235 // make assertions after dispatch 236 resultAssertions []assertion 237 }{ 238 "send tokens": { 239 submsgID: 5, 240 msg: validBankSend, 241 resultAssertions: []assertion{assertReturnedEvents(0), assertGasUsed(90000, 96000)}, 242 }, 243 "not enough tokens": { 244 submsgID: 6, 245 msg: invalidBankSend, 246 subMsgError: true, 247 // uses less gas than the send tokens (cost of bank transfer) 248 resultAssertions: []assertion{assertGasUsed(73000, 79000), assertErrorString("codespace: sdk, code: 5")}, 249 }, 250 "out of gas panic with no gas limit": { 251 submsgID: 7, 252 msg: infiniteLoop, 253 isOutOfGasPanic: true, 254 }, 255 256 "send tokens with limit": { 257 submsgID: 15, 258 msg: validBankSend, 259 gasLimit: &subGasLimit, 260 // uses same gas as call without limit (note we do not charge the 40k on reply) 261 resultAssertions: []assertion{assertReturnedEvents(0), assertGasUsed(90000, 96000)}, 262 }, 263 "not enough tokens with limit": { 264 submsgID: 16, 265 msg: invalidBankSend, 266 subMsgError: true, 267 gasLimit: &subGasLimit, 268 // uses same gas as call without limit (note we do not charge the 40k on reply) 269 resultAssertions: []assertion{assertGasUsed(73000, 79000), assertErrorString("codespace: sdk, code: 5")}, 270 }, 271 "out of gas caught with gas limit": { 272 submsgID: 17, 273 msg: infiniteLoop, 274 subMsgError: true, 275 gasLimit: &subGasLimit, 276 // uses all the subGasLimit, plus the 52k or so for the main contract 277 resultAssertions: []assertion{assertGasUsed(subGasLimit+71000, subGasLimit+77000), assertErrorString("codespace: sdk, code: 11")}, 278 }, 279 "instantiate contract gets address in data and events": { 280 submsgID: 21, 281 msg: instantiateContract, 282 resultAssertions: []assertion{assertReturnedEvents(1), assertGotContractAddr}, 283 }, 284 } 285 for name, tc := range cases { 286 t.Run(name, func(t *testing.T) { 287 creator := keepers.Faucet.NewFundedAccount(ctx, contractStart...) 288 _, _, empty := keyPubAddr() 289 290 contractAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, reflectID, creator, nil, []byte("{}"), fmt.Sprintf("contract %s", name), contractStart) 291 require.NoError(t, err) 292 293 msg := tc.msg(contractAddr.String(), empty.String()) 294 reflectSend := testdata.ReflectHandleMsg{ 295 ReflectSubMsg: &testdata.ReflectSubPayload{ 296 Msgs: []wasmvmtypes.SubMsg{{ 297 ID: tc.submsgID, 298 Msg: msg, 299 GasLimit: tc.gasLimit, 300 ReplyOn: wasmvmtypes.ReplyAlways, 301 }}, 302 }, 303 } 304 reflectSendBz, err := json.Marshal(reflectSend) 305 require.NoError(t, err) 306 307 execCtx := ctx 308 execCtx.SetGasMeter(sdk.NewGasMeter(ctxGasLimit)) 309 defer func() { 310 if tc.isOutOfGasPanic { 311 r := recover() 312 require.NotNil(t, r, "expected panic") 313 if _, ok := r.(sdk.ErrorOutOfGas); !ok { 314 t.Fatalf("Expected OutOfGas panic, got: %#v\n", r) 315 } 316 } 317 }() 318 _, err = keepers.ContractKeeper.Execute(execCtx, contractAddr, creator, reflectSendBz, nil) 319 320 if tc.executeError { 321 require.Error(t, err) 322 } else { 323 require.NoError(t, err) 324 325 // query the reply 326 query := testdata.ReflectQueryMsg{ 327 SubMsgResult: &testdata.SubCall{ID: tc.submsgID}, 328 } 329 queryBz, err := json.Marshal(query) 330 require.NoError(t, err) 331 queryRes, err := keeper.QuerySmart(ctx, contractAddr, queryBz) 332 require.NoError(t, err) 333 var res wasmvmtypes.Reply 334 err = json.Unmarshal(queryRes, &res) 335 require.NoError(t, err) 336 assert.Equal(t, tc.submsgID, res.ID) 337 338 if tc.subMsgError { 339 require.NotEmpty(t, res.Result.Err) 340 require.Nil(t, res.Result.Ok) 341 } else { 342 require.Empty(t, res.Result.Err) 343 require.NotNil(t, res.Result.Ok) 344 } 345 346 for _, assertion := range tc.resultAssertions { 347 assertion(t, execCtx, contractAddr.String(), empty.String(), res.Result) 348 } 349 350 } 351 }) 352 } 353 } 354 355 // Test an error case, where the Encoded doesn't return any sdk.Msg and we trigger(ed) a null pointer exception. 356 // This occurs with the IBC encoder. Test this. 357 func TestDispatchSubMsgEncodeToNoSdkMsg(t *testing.T) { 358 // fake out the bank handle to return success with no data 359 nilEncoder := func(sender sdk.AccAddress, msg *wasmvmtypes.BankMsg) ([]ibcadapter.Msg, error) { 360 return nil, nil 361 } 362 customEncoders := &MessageEncoders{ 363 Bank: nilEncoder, 364 } 365 366 ctx, keepers := CreateTestInput(t, false, ReflectFeatures, WithMessageHandler(NewSDKMessageHandler(nil, customEncoders))) 367 keeper := keepers.WasmKeeper 368 369 deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) 370 contractStart := sdk.NewCoins(sdk.NewInt64Coin("denom", 40000)) 371 372 creator := keepers.Faucet.NewFundedAccount(ctx, deposit...) 373 _, _, fred := keyPubAddr() 374 375 // upload code 376 codeID, err := keepers.ContractKeeper.Create(ctx, creator, testdata.ReflectContractWasm(), nil) 377 require.NoError(t, err) 378 379 // creator instantiates a contract and gives it tokens 380 contractAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, codeID, creator, nil, []byte("{}"), "reflect contract 1", contractStart) 381 require.NoError(t, err) 382 require.NotEmpty(t, contractAddr) 383 384 // creator can send contract's tokens to fred (using SendMsg) 385 msg := wasmvmtypes.CosmosMsg{ 386 Bank: &wasmvmtypes.BankMsg{ 387 Send: &wasmvmtypes.SendMsg{ 388 ToAddress: fred.String(), 389 Amount: []wasmvmtypes.Coin{{ 390 Denom: "denom", 391 Amount: "15000000000000000000000", 392 }}, 393 }, 394 }, 395 } 396 reflectSend := testdata.ReflectHandleMsg{ 397 ReflectSubMsg: &testdata.ReflectSubPayload{ 398 Msgs: []wasmvmtypes.SubMsg{{ 399 ID: 7, 400 Msg: msg, 401 ReplyOn: wasmvmtypes.ReplyAlways, 402 }}, 403 }, 404 } 405 reflectSendBz, err := json.Marshal(reflectSend) 406 require.NoError(t, err) 407 _, err = keepers.ContractKeeper.Execute(ctx, contractAddr, creator, reflectSendBz, nil) 408 require.NoError(t, err) 409 410 // query the reflect state to ensure the result was stored 411 query := testdata.ReflectQueryMsg{ 412 SubMsgResult: &testdata.SubCall{ID: 7}, 413 } 414 queryBz, err := json.Marshal(query) 415 require.NoError(t, err) 416 queryRes, err := keeper.QuerySmart(ctx, contractAddr, queryBz) 417 require.NoError(t, err) 418 419 var res wasmvmtypes.Reply 420 err = json.Unmarshal(queryRes, &res) 421 require.NoError(t, err) 422 assert.Equal(t, uint64(7), res.ID) 423 assert.Empty(t, res.Result.Err) 424 require.NotNil(t, res.Result.Ok) 425 sub := res.Result.Ok 426 assert.Empty(t, sub.Data) 427 require.Len(t, sub.Events, 0) 428 } 429 430 // Try a simple send, no gas limit to for a sanity check before trying table tests 431 func TestDispatchSubMsgConditionalReplyOn(t *testing.T) { 432 ctx, keepers := CreateTestInput(t, false, ReflectFeatures) 433 keeper := keepers.WasmKeeper 434 435 deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) 436 contractStart := sdk.NewCoins(sdk.NewInt64Coin("denom", 40000)) 437 438 creator := keepers.Faucet.NewFundedAccount(ctx, deposit...) 439 _, _, fred := keyPubAddr() 440 441 // upload code 442 codeID, err := keepers.ContractKeeper.Create(ctx, creator, testdata.ReflectContractWasm(), nil) 443 require.NoError(t, err) 444 445 // creator instantiates a contract and gives it tokens 446 contractAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, codeID, creator, nil, []byte("{}"), "reflect contract 1", contractStart) 447 require.NoError(t, err) 448 449 goodSend := wasmvmtypes.CosmosMsg{ 450 Bank: &wasmvmtypes.BankMsg{ 451 Send: &wasmvmtypes.SendMsg{ 452 ToAddress: fred.String(), 453 Amount: []wasmvmtypes.Coin{{ 454 Denom: "denom", 455 Amount: "1000", 456 }}, 457 }, 458 }, 459 } 460 failSend := wasmvmtypes.CosmosMsg{ 461 Bank: &wasmvmtypes.BankMsg{ 462 Send: &wasmvmtypes.SendMsg{ 463 ToAddress: fred.String(), 464 Amount: []wasmvmtypes.Coin{{ 465 Denom: "no-such-token", 466 Amount: "777777", 467 }}, 468 }, 469 }, 470 } 471 472 cases := map[string]struct { 473 // true for wasmvmtypes.ReplySuccess, false for wasmvmtypes.ReplyError 474 replyOnSuccess bool 475 msg wasmvmtypes.CosmosMsg 476 // true if the call should return an error (it wasn't handled) 477 expectError bool 478 // true if the reflect contract wrote the response (success or error) - it was captured 479 writeResult bool 480 }{ 481 "all good, reply success": { 482 replyOnSuccess: true, 483 msg: goodSend, 484 expectError: false, 485 writeResult: true, 486 }, 487 "all good, reply error": { 488 replyOnSuccess: false, 489 msg: goodSend, 490 expectError: false, 491 writeResult: false, 492 }, 493 "bad msg, reply success": { 494 replyOnSuccess: true, 495 msg: failSend, 496 expectError: true, 497 writeResult: false, 498 }, 499 "bad msg, reply error": { 500 replyOnSuccess: false, 501 msg: failSend, 502 expectError: false, 503 writeResult: true, 504 }, 505 } 506 507 var id uint64 = 0 508 for name, tc := range cases { 509 id++ 510 t.Run(name, func(t *testing.T) { 511 subMsg := wasmvmtypes.SubMsg{ 512 ID: id, 513 Msg: tc.msg, 514 ReplyOn: wasmvmtypes.ReplySuccess, 515 } 516 if !tc.replyOnSuccess { 517 subMsg.ReplyOn = wasmvmtypes.ReplyError 518 } 519 520 reflectSend := testdata.ReflectHandleMsg{ 521 ReflectSubMsg: &testdata.ReflectSubPayload{ 522 Msgs: []wasmvmtypes.SubMsg{subMsg}, 523 }, 524 } 525 reflectSendBz, err := json.Marshal(reflectSend) 526 require.NoError(t, err) 527 _, err = keepers.ContractKeeper.Execute(ctx, contractAddr, creator, reflectSendBz, nil) 528 529 if tc.expectError { 530 require.Error(t, err) 531 } else { 532 require.NoError(t, err) 533 } 534 535 // query the reflect state to check if the result was stored 536 query := testdata.ReflectQueryMsg{ 537 SubMsgResult: &testdata.SubCall{ID: id}, 538 } 539 queryBz, err := json.Marshal(query) 540 require.NoError(t, err) 541 queryRes, err := keeper.QuerySmart(ctx, contractAddr, queryBz) 542 if tc.writeResult { 543 // we got some data for this call 544 require.NoError(t, err) 545 var res wasmvmtypes.Reply 546 err = json.Unmarshal(queryRes, &res) 547 require.NoError(t, err) 548 require.Equal(t, id, res.ID) 549 } else { 550 // nothing should be there -> error 551 require.Error(t, err) 552 } 553 }) 554 } 555 }