Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add gas meter to tokenfactory trackBeforeSend #5927

Merged
merged 7 commits into from
Aug 1, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* [#5855](https://github.com/osmosis-labs/osmosis/pull/5855) feat(x/cosmwasmpool): Sending token_in_max_amount to the contract before running contract msg
* [#5893] (https://github.com/osmosis-labs/osmosis/pull/5893) Export createPosition method in CL so other modules can use it in testing
* [#5870] (https://github.com/osmosis-labs/osmosis/pull/5870) Remove v14/ separator in protorev rest endpoints
* [#5927] (https://github.com/osmosis-labs/osmosis/pull/5927) Add gas metering to x/tokenfactory trackBeforeSend hook

### Minor improvements & Bug Fixes

Expand Down
19 changes: 17 additions & 2 deletions x/tokenfactory/keeper/before_send.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,15 @@ func (h Hooks) BlockBeforeSend(ctx sdk.Context, from, to sdk.AccAddress, amount

// callBeforeSendListener iterates over each coin and sends corresponding sudo msg to the contract address stored in state.
// If blockBeforeSend is true, sudoMsg wraps BlockBeforeSendMsg, otherwise sudoMsg wraps TrackBeforeSendMsg.
func (k Keeper) callBeforeSendListener(ctx sdk.Context, from, to sdk.AccAddress, amount sdk.Coins, blockBeforeSend bool) error {
// Note that we gas meter trackBeforeSend to prevent infinite contract calls.
// CONTRACT: this should not be called in beginBlock or endBlock since out of gas will cause this method to panic.
func (k Keeper) callBeforeSendListener(ctx sdk.Context, from, to sdk.AccAddress, amount sdk.Coins, blockBeforeSend bool) (err error) {
defer func() {
if r := recover(); r != nil {
err = types.ErrTrackBeforeSendOutOfGas
}
}()

for _, coin := range amount {
cosmwasmAddress := k.GetBeforeSendHook(ctx, coin.Denom)
if cosmwasmAddress != "" {
Expand All @@ -98,6 +106,9 @@ func (k Keeper) callBeforeSendListener(ctx sdk.Context, from, to sdk.AccAddress,
var msgBz []byte

// get msgBz, either BlockBeforeSend or TrackBeforeSend
// Note that for trackBeforeSend, we need to gas meter computations to prevent infinite loop
// specifically because module to module sends are not gas metered.
// We don't need to do this for blockBeforeSend since blockBeforeSend is not called during module to module sends.
if blockBeforeSend {
msg := types.BlockBeforeSendSudoMsg{
BlockBeforeSend: types.BlockBeforeSendMsg{
Expand All @@ -121,8 +132,12 @@ func (k Keeper) callBeforeSendListener(ctx sdk.Context, from, to sdk.AccAddress,
return err
}

em := sdk.NewEventManager()
// if its track before send, apply gas meter to prevent infinite loop
if !blockBeforeSend {
ctx = ctx.WithGasMeter(sdk.NewGasMeter(types.TrackBeforeSendGasLimit))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want to write this consumed gas meter result to the parent gas meter though. (So if that panics, then its fine)

I think that looks like keeping parentCtx, and doing "parentCtx.GasMeter.Use(childCtx.GasMeter.GasUsed)"

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's because of panic catching, it's already fine as of right now since we recover thread from panic though using defer function above right? (Probably me missing something)

Copy link
Member

@ValarDragon ValarDragon Jul 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suppose the parent gas meter had say 5 million gas.

It does this + some other stuff. Work here should still contribute to that gas

We still want the gas consumed here to get added to that gas meter. Its totally fine if it can't out of gas error inside this function, the next thing that consumes gas will cause the gas panic

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Attempt in f8619ff.

}

em := sdk.NewEventManager()
_, err = k.contractKeeper.Sudo(ctx.WithEventManager(em), cwAddr, msgBz)
if err != nil {
return errorsmod.Wrapf(err, "failed to call before send hook for denom %s", coin.Denom)
Expand Down
66 changes: 66 additions & 0 deletions x/tokenfactory/keeper/before_send_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,69 @@ func (s *KeeperTestSuite) TestBeforeSendHook() {
})
}
}

// TestInfiniteTrackBeforeSend tests gas metering with infinite loop contract
// to properly test if we are gas metering trackBeforeSend properly.
func (s *KeeperTestSuite) TestInfiniteTrackBeforeSend() {
s.SkipIfWSL()

// load wasm file
wasmFile := "./testdata/infinite_track_beforesend.wasm"
wasmCode, err := os.ReadFile(wasmFile)
s.Require().NoError(err)

for _, tc := range []struct {
name string
useFactoryDenom bool
expectedError bool
}{
{
name: "sending tokenfactory denom from module to module with infinite contract should panic",
useFactoryDenom: true,
},
{
name: "sending non-tokenfactory denom from module to module with infinite contract should not panic",
useFactoryDenom: false,
},
} {
s.Run(fmt.Sprintf("Case %s", tc.name), func() {
// setup test
s.SetupTest()

// instantiate wasm code
codeID, _, err := s.contractKeeper.Create(s.Ctx, s.TestAccs[0], wasmCode, nil)
s.Require().NoError(err, "test: %v", tc.name)
cosmwasmAddress, _, err := s.contractKeeper.Instantiate(s.Ctx, codeID, s.TestAccs[0], s.TestAccs[0], []byte("{}"), "", sdk.NewCoins())
s.Require().NoError(err, "test: %v", tc.name)

// create new denom
res, err := s.msgServer.CreateDenom(sdk.WrapSDKContext(s.Ctx), types.NewMsgCreateDenom(s.TestAccs[0].String(), "bitcoin"))
s.Require().NoError(err, "test: %v", tc.name)
factoryDenom := res.GetNewTokenDenom()

var tokenToSend sdk.Coins
if tc.useFactoryDenom {
tokenToSend = sdk.NewCoins(sdk.NewInt64Coin(factoryDenom, 1000000))
} else {
tokenToSend = sdk.NewCoins(sdk.NewInt64Coin("foo", 1000000))
}

// send the mint module tokenToSend
s.FundModuleAcc("mint", tokenToSend)

// set beforesend hook to the new denom
// we register infinite loop contract here to test if we are gas metering properly
_, err = s.msgServer.SetBeforeSendHook(sdk.WrapSDKContext(s.Ctx), types.NewMsgSetBeforeSendHook(s.TestAccs[0].String(), factoryDenom, cosmwasmAddress.String()))
s.Require().NoError(err, "test: %v", tc.name)

// track before send suppresses in any case, thus we expect no error
err = s.App.BankKeeper.SendCoinsFromModuleToModule(s.Ctx, "mint", "distribution", tokenToSend)
s.Require().NoError(err)

// send should happen regardless of trackBeforeSend results
distributionModuleAddress := s.App.AccountKeeper.GetModuleAddress("distribution")
distributionModuleBalances := s.App.BankKeeper.GetAllBalances(s.Ctx, distributionModuleAddress)
s.Require().True(distributionModuleBalances.IsEqual(tokenToSend))
})
}
}
5 changes: 5 additions & 0 deletions x/tokenfactory/types/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package types

var (
TrackBeforeSendGasLimit = uint64(100_000)
)
1 change: 1 addition & 0 deletions x/tokenfactory/types/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ var (
ErrCreatorTooLong = errorsmod.Register(ModuleName, 9, fmt.Sprintf("creator too long, max length is %d bytes", MaxCreatorLength))
ErrDenomDoesNotExist = errorsmod.Register(ModuleName, 10, "denom does not exist")
ErrBurnFromModuleAccount = errorsmod.Register(ModuleName, 11, "burning from Module Account is not allowed")
ErrTrackBeforeSendOutOfGas = errorsmod.Register(ModuleName, 12, "gas meter hit maximum limit")
)