diff --git a/app/ante.go b/app/ante.go deleted file mode 100644 index f3536450..00000000 --- a/app/ante.go +++ /dev/null @@ -1,67 +0,0 @@ -package app - -import ( - errorsmod "cosmossdk.io/errors" - sdk "github.com/cosmos/cosmos-sdk/types" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" - "github.com/cosmos/cosmos-sdk/x/auth/ante" - - ibcante "github.com/cosmos/ibc-go/v6/modules/core/ante" - ibckeeper "github.com/cosmos/ibc-go/v6/modules/core/keeper" -) - -// HandlerOptions are the options required for constructing a default SDK AnteHandler. -type HandlerOptions struct { - ante.HandlerOptions - - IBCKeeper *ibckeeper.Keeper -} - -func GetAnteDecorators(options HandlerOptions) []sdk.AnteDecorator { - sigGasConsumer := options.SigGasConsumer - if sigGasConsumer == nil { - sigGasConsumer = ante.DefaultSigVerificationGasConsumer - } - - anteDecorators := []sdk.AnteDecorator{ - ante.NewSetUpContextDecorator(), // outermost AnteDecorator. SetUpContext must be called first - - ante.NewExtensionOptionsDecorator(options.ExtensionOptionChecker), - - ante.NewValidateBasicDecorator(), - ante.NewTxTimeoutHeightDecorator(), - - ante.NewValidateMemoDecorator(options.AccountKeeper), - ante.NewConsumeGasForTxSizeDecorator(options.AccountKeeper), - ante.NewDeductFeeDecorator(options.AccountKeeper, options.BankKeeper, options.FeegrantKeeper, options.TxFeeChecker), - ante.NewSetPubKeyDecorator(options.AccountKeeper), // SetPubKeyDecorator must be called before all signature verification decorators - ante.NewValidateSigCountDecorator(options.AccountKeeper), - ante.NewSigGasConsumeDecorator(options.AccountKeeper, sigGasConsumer), - ante.NewSigVerificationDecorator(options.AccountKeeper, options.SignModeHandler), - ante.NewIncrementSequenceDecorator(options.AccountKeeper), - } - - anteDecorators = append(anteDecorators, ibcante.NewRedundantRelayDecorator(options.IBCKeeper)) - - return anteDecorators -} - -// NewAnteHandler returns an AnteHandler that checks and increments sequence -// numbers, checks signatures & account numbers, and deducts fees from the first -// signer. -func NewAnteHandler(options HandlerOptions) (sdk.AnteHandler, error) { - //From x/auth/ante.go - if options.AccountKeeper == nil { - return nil, errorsmod.Wrap(sdkerrors.ErrLogic, "account keeper is required for ante builder") - } - - if options.BankKeeper == nil { - return nil, errorsmod.Wrap(sdkerrors.ErrLogic, "bank keeper is required for ante builder") - } - - if options.SignModeHandler == nil { - return nil, errorsmod.Wrap(sdkerrors.ErrLogic, "sign mode handler is required for ante builder") - } - - return sdk.ChainAnteDecorators(GetAnteDecorators(options)...), nil -} diff --git a/app/ante/ante.go b/app/ante/ante.go new file mode 100644 index 00000000..25cf34ee --- /dev/null +++ b/app/ante/ante.go @@ -0,0 +1,196 @@ +package ante + +import ( + "fmt" + "runtime/debug" + + cosmosante "github.com/evmos/evmos/v12/app/ante/cosmos" + + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + + "github.com/cosmos/cosmos-sdk/codec" + + errorsmod "cosmossdk.io/errors" + + "github.com/cosmos/cosmos-sdk/client" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + authante "github.com/cosmos/cosmos-sdk/x/auth/ante" + ibckeeper "github.com/cosmos/ibc-go/v6/modules/core/keeper" + evmosante "github.com/evmos/evmos/v12/app/ante" + evmosanteevm "github.com/evmos/evmos/v12/app/ante/evm" + anteutils "github.com/evmos/evmos/v12/app/ante/utils" + evmostypes "github.com/evmos/evmos/v12/types" + evmtypes "github.com/evmos/evmos/v12/x/evm/types" + evmosvestingtypes "github.com/evmos/evmos/v12/x/vesting/types" + tmlog "github.com/tendermint/tendermint/libs/log" +) + +type HasPermission = func(ctx sdk.Context, accAddr sdk.AccAddress, perm string) bool + +func MustCreateHandler(codec codec.BinaryCodec, + txConfig client.TxConfig, + maxGasWanted uint64, + hasPermission HasPermission, + accountKeeper evmtypes.AccountKeeper, + stakingKeeper evmosvestingtypes.StakingKeeper, + bankKeeper evmtypes.BankKeeper, + feeMarketKeeper evmosanteevm.FeeMarketKeeper, + evmKeeper evmosanteevm.EVMKeeper, + ibcKeeper *ibckeeper.Keeper, + distrKeeper anteutils.DistributionKeeper, +) sdk.AnteHandler { + ethOpts := evmosante.HandlerOptions{ + Cdc: codec, + AccountKeeper: accountKeeper, + BankKeeper: bankKeeper, + EvmKeeper: evmKeeper, + StakingKeeper: stakingKeeper, + FeegrantKeeper: nil, + DistributionKeeper: distrKeeper, + IBCKeeper: ibcKeeper, + FeeMarketKeeper: feeMarketKeeper, + SignModeHandler: txConfig.SignModeHandler(), + SigGasConsumer: evmosante.SigVerificationGasConsumer, + MaxTxGasWanted: maxGasWanted, + TxFeeChecker: evmosanteevm.NewDynamicFeeChecker(evmKeeper), + } + + opts := HandlerOptions{ + HandlerOptions: ethOpts, + hasPermission: hasPermission, + } + + h, err := NewHandler(opts) + if err != nil { + panic(fmt.Errorf("new ante handler: %w", err)) + } + return h +} + +// HandlerOptions are the options required for constructing a default SDK AnteHandler. +type HandlerOptions struct { + evmosante.HandlerOptions + hasPermission HasPermission +} + +func (o HandlerOptions) validate() error { + /* + First check the eth stuff - the validate method is not exported so this is copy-pasted + */ + if o.AccountKeeper == nil { + return errorsmod.Wrap(sdkerrors.ErrLogic, "account keeper missing") + } + if o.BankKeeper == nil { + return errorsmod.Wrap(sdkerrors.ErrLogic, "bank keeper missing") + } + if o.SignModeHandler == nil { + return errorsmod.Wrap(sdkerrors.ErrLogic, "sign mode handler missing") + } + if o.FeeMarketKeeper == nil { + return errorsmod.Wrap(sdkerrors.ErrLogic, "fee market keeper missing") + } + if o.EvmKeeper == nil { + return errorsmod.Wrap(sdkerrors.ErrLogic, "evm keeper missing") + } + if o.DistributionKeeper == nil { + return errorsmod.Wrap(sdkerrors.ErrLogic, "distribution keeper missing") + } + if o.StakingKeeper == nil { + return errorsmod.Wrap(sdkerrors.ErrLogic, "staking keeper missing") + } + + /* + Our stuff + */ + if o.hasPermission == nil { + return errorsmod.Wrap(sdkerrors.ErrLogic, "permission checker missing") + } + if o.IBCKeeper == nil { + return errorsmod.Wrap(sdkerrors.ErrLogic, "IBC keeper missing") + } + return nil +} + +func NewHandler(options HandlerOptions) (sdk.AnteHandler, error) { + if err := options.validate(); err != nil { + return nil, fmt.Errorf("options validate: %w", err) + } + + return func( + ctx sdk.Context, tx sdk.Tx, sim bool, + ) (newCtx sdk.Context, err error) { + var anteHandler sdk.AnteHandler + + defer Recover(ctx.Logger(), &err) + + txWithExtensions, ok := tx.(authante.HasExtensionOptionsTx) + if ok { + opts := txWithExtensions.GetExtensionOptions() + if len(opts) > 0 { + switch typeURL := opts[0].GetTypeUrl(); typeURL { + case "/ethermint.evm.v1.ExtensionOptionsEthereumTx": + // handle as *evmtypes.MsgEthereumTx. It will get checked by the EVM handler to make sure it is. + anteHandler = newEVMAnteHandler(options) + case "/ethermint.types.v1.ExtensionOptionsWeb3Tx": + // Deprecated: Handle as normal Cosmos SDK tx, except signature is checked for Legacy EIP712 representation + options.ExtensionOptionChecker = func(c *codectypes.Any) bool { + _, ok := c.GetCachedValue().(*evmostypes.ExtensionOptionsWeb3Tx) + return ok + } + anteHandler = cosmosHandler( + options, + cosmosante.NewLegacyEip712SigVerificationDecorator(options.AccountKeeper, options.SignModeHandler), // Use old signature verification: uses EIP instead of the cosmos signature validator + ) + case "/ethermint.types.v1.ExtensionOptionDynamicFeeTx": // TODO: can delete? + // cosmos-sdk tx with dynamic fee extension + options.ExtensionOptionChecker = evmostypes.HasDynamicFeeExtensionOption + anteHandler = cosmosHandler( + options, + authante.NewSigVerificationDecorator(options.AccountKeeper, options.SignModeHandler), // Use modern signature verification + ) + default: + return ctx, errorsmod.Wrapf( + sdkerrors.ErrUnknownExtensionOptions, + "rejecting tx with unsupported extension option: %s", typeURL, + ) + } + + return anteHandler(ctx, tx, sim) + } + } + + // handle as totally normal Cosmos SDK tx + switch tx.(type) { + case sdk.Tx: + // we reject any extension + anteHandler = cosmosHandler( + options, + authante.NewSigVerificationDecorator(options.AccountKeeper, options.SignModeHandler), // Use modern signature verification + ) + default: + return ctx, errorsmod.Wrapf(sdkerrors.ErrUnknownRequest, "invalid transaction type: %T", tx) + } + + return anteHandler(ctx, tx, sim) + }, nil +} + +func Recover(logger tmlog.Logger, err *error) { + if r := recover(); r != nil { + *err = errorsmod.Wrapf(sdkerrors.ErrPanic, "%v", r) + + if e, ok := r.(error); ok { + logger.Error( + "ante handler panicked", + "error", e, + "stack trace", string(debug.Stack()), + ) + } else { + logger.Error( + "ante handler panicked", + "recover", fmt.Sprintf("%v", r), + ) + } + } +} diff --git a/app/ante/decorator_permissioned_urls.go b/app/ante/decorator_permissioned_urls.go new file mode 100644 index 00000000..4c4911b7 --- /dev/null +++ b/app/ante/decorator_permissioned_urls.go @@ -0,0 +1,39 @@ +package ante + +import ( + errorsmod "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "golang.org/x/exp/slices" +) + +// PermissionedURLsDecorator prevents invalid msg types from being executed +type PermissionedURLsDecorator struct { + hasPermission func(ctx sdk.Context, accAddr sdk.AccAddress) bool + permissionedURls []string +} + +func NewPermissionedURLsDecorator(hasPermission func(ctx sdk.Context, accAddr sdk.AccAddress) bool, msgTypeURLs []string) PermissionedURLsDecorator { + return PermissionedURLsDecorator{ + hasPermission: hasPermission, + permissionedURls: msgTypeURLs, + } +} + +// AnteHandle rejects vesting messages that signer does not have permission +func (d PermissionedURLsDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) { + for _, msg := range tx.GetMsgs() { + if slices.Contains(d.permissionedURls, sdk.MsgTypeURL(msg)) { + // Check if vesting tx signer is 1 + if len(msg.GetSigners()) != 1 { + return ctx, errorsmod.Wrapf(sdkerrors.ErrInvalidAddress, "expect 1 signer: signers: %v", msg.GetSigners()) + } + + signer := msg.GetSigners()[0] + if !d.hasPermission(ctx, signer) { + return ctx, sdkerrors.ErrUnauthorized + } + } + } + return next(ctx, tx, simulate) +} diff --git a/app/ante/handlers.go b/app/ante/handlers.go new file mode 100644 index 00000000..951e6beb --- /dev/null +++ b/app/ante/handlers.go @@ -0,0 +1,79 @@ +package ante + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth/ante" + authante "github.com/cosmos/cosmos-sdk/x/auth/ante" + sdkvestingtypes "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" + vestingtypes "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" + ibcante "github.com/cosmos/ibc-go/v6/modules/core/ante" + cosmosante "github.com/evmos/evmos/v12/app/ante/cosmos" + evmante "github.com/evmos/evmos/v12/app/ante/evm" + evmtypes "github.com/evmos/evmos/v12/x/evm/types" +) + +// NOTE: this function is copied from evmos +func newEVMAnteHandler(options HandlerOptions) sdk.AnteHandler { + return sdk.ChainAnteDecorators( + // outermost AnteDecorator. SetUpContext must be called first + evmante.NewEthSetUpContextDecorator(options.EvmKeeper), + // Check eth effective gas price against the node's minimal-gas-prices config + evmante.NewEthMempoolFeeDecorator(options.EvmKeeper), + // Check eth effective gas price against the global MinGasPrice + evmante.NewEthMinGasPriceDecorator(options.FeeMarketKeeper, options.EvmKeeper), + evmante.NewEthValidateBasicDecorator(options.EvmKeeper), + evmante.NewEthSigVerificationDecorator(options.EvmKeeper), + evmante.NewEthAccountVerificationDecorator(options.AccountKeeper, options.EvmKeeper), + evmante.NewCanTransferDecorator(options.EvmKeeper), + // we intentionally omit the eth vesting transaction decorator + evmante.NewEthGasConsumeDecorator(options.BankKeeper, options.DistributionKeeper, options.EvmKeeper, options.StakingKeeper, options.MaxTxGasWanted), + evmante.NewEthIncrementSenderSequenceDecorator(options.AccountKeeper), + evmante.NewGasWantedDecorator(options.EvmKeeper, options.FeeMarketKeeper), + // emit eth tx hash and index at the very last ante handler. + evmante.NewEthEmitEventDecorator(options.EvmKeeper), + ) +} + +func cosmosHandler(options HandlerOptions, sigChecker sdk.AnteDecorator) sdk.AnteHandler { + sigGasConsumer := options.SigGasConsumer + if sigGasConsumer == nil { + sigGasConsumer = authante.DefaultSigVerificationGasConsumer + } + return sdk.ChainAnteDecorators( + cosmosante.NewRejectMessagesDecorator( + []string{ + sdk.MsgTypeURL(&evmtypes.MsgEthereumTx{}), + }, + ), + cosmosante.NewAuthzLimiterDecorator( // disable the Msg types that cannot be included on an authz.MsgExec msgs field + sdk.MsgTypeURL(&evmtypes.MsgEthereumTx{}), + sdk.MsgTypeURL(&sdkvestingtypes.MsgCreateVestingAccount{}), + sdk.MsgTypeURL(&sdkvestingtypes.MsgCreatePermanentLockedAccount{}), + sdk.MsgTypeURL(&sdkvestingtypes.MsgCreatePeriodicVestingAccount{}), + ), + ante.NewSetUpContextDecorator(), + ante.NewExtensionOptionsDecorator(options.ExtensionOptionChecker), + ante.NewValidateBasicDecorator(), + ante.NewTxTimeoutHeightDecorator(), + NewPermissionedURLsDecorator( + func(ctx sdk.Context, accAddr sdk.AccAddress) bool { + return options.hasPermission(ctx, accAddr, vestingtypes.ModuleName) + }, []string{ + sdk.MsgTypeURL(&vestingtypes.MsgCreateVestingAccount{}), + sdk.MsgTypeURL(&vestingtypes.MsgCreatePermanentLockedAccount{}), + sdk.MsgTypeURL(&vestingtypes.MsgCreatePeriodicVestingAccount{}), + }), + ante.NewValidateMemoDecorator(options.AccountKeeper), + cosmosante.NewMinGasPriceDecorator(options.FeeMarketKeeper, options.EvmKeeper), + ante.NewConsumeGasForTxSizeDecorator(options.AccountKeeper), + cosmosante.NewDeductFeeDecorator(options.AccountKeeper, options.BankKeeper, options.DistributionKeeper, options.FeegrantKeeper, options.StakingKeeper, options.TxFeeChecker), + // SetPubKeyDecorator must be called before all signature verification decorators + ante.NewSetPubKeyDecorator(options.AccountKeeper), + ante.NewValidateSigCountDecorator(options.AccountKeeper), + ante.NewSigGasConsumeDecorator(options.AccountKeeper, sigGasConsumer), + sigChecker, + ante.NewIncrementSequenceDecorator(options.AccountKeeper), + ibcante.NewRedundantRelayDecorator(options.IBCKeeper), + evmante.NewGasWantedDecorator(options.EvmKeeper, options.FeeMarketKeeper), + ) +} diff --git a/app/app.go b/app/app.go index 98999df5..08f9d69e 100644 --- a/app/app.go +++ b/app/app.go @@ -7,6 +7,8 @@ import ( "path/filepath" "sort" + "github.com/dymensionxyz/rollapp-evm/app/ante" + "github.com/gorilla/mux" "github.com/rakyll/statik/fs" "github.com/spf13/cast" @@ -111,8 +113,6 @@ import ( "github.com/dymensionxyz/dymension-rdk/x/denommetadata" denommetadatamodulekeeper "github.com/dymensionxyz/dymension-rdk/x/denommetadata/keeper" denommetadatamoduletypes "github.com/dymensionxyz/dymension-rdk/x/denommetadata/types" - ethante "github.com/evmos/evmos/v12/app/ante" - ethanteevm "github.com/evmos/evmos/v12/app/ante/evm" "github.com/evmos/evmos/v12/ethereum/eip712" ethermint "github.com/evmos/evmos/v12/types" "github.com/evmos/evmos/v12/x/claims" @@ -323,7 +323,6 @@ func NewRollapp( appOpts servertypes.AppOptions, baseAppOptions ...func(*baseapp.BaseApp), ) *App { - appCodec := encodingConfig.Codec cdc := encodingConfig.Amino interfaceRegistry := encodingConfig.InterfaceRegistry @@ -723,7 +722,28 @@ func NewRollapp( app.SetEndBlocker(app.EndBlocker) maxGasWanted := cast.ToUint64(appOpts.Get(srvflags.EVMMaxTxGasWanted)) - app.setAnteHandler(encodingConfig.TxConfig, maxGasWanted) + h := ante.MustCreateHandler( + app.appCodec, + encodingConfig.TxConfig, + maxGasWanted, + func(ctx sdk.Context, accAddr sdk.AccAddress, perm string) bool { + /* + TODO: + We had a plan to use the sequencers module to manager permissions, but that idea was changed + For now, we just assume the only account with permission is the denom one + We will eventually replace with something more substantial + */ + return app.DenomMetadataKeeper.IsAddressPermissioned(ctx, accAddr.String()) + }, + app.AccountKeeper, + app.StakingKeeper, + app.BankKeeper, + app.FeeMarketKeeper, + app.EvmKeeper, + app.IBCKeeper, + app.DistrKeeper, + ) + app.SetAnteHandler(h) app.setPostHandler() if loadLatest { @@ -738,31 +758,6 @@ func NewRollapp( return app } -func (app *App) setAnteHandler(txConfig client.TxConfig, maxGasWanted uint64) { - options := ethante.HandlerOptions{ - Cdc: app.appCodec, - AccountKeeper: app.AccountKeeper, - BankKeeper: app.BankKeeper, - ExtensionOptionChecker: ethermint.HasDynamicFeeExtensionOption, - EvmKeeper: app.EvmKeeper, - StakingKeeper: app.StakingKeeper, - FeegrantKeeper: nil, - DistributionKeeper: app.DistrKeeper, - IBCKeeper: app.IBCKeeper, - FeeMarketKeeper: app.FeeMarketKeeper, - SignModeHandler: txConfig.SignModeHandler(), - SigGasConsumer: ethante.SigVerificationGasConsumer, - MaxTxGasWanted: maxGasWanted, - TxFeeChecker: ethanteevm.NewDynamicFeeChecker(app.EvmKeeper), - } - - if err := options.Validate(); err != nil { - panic(err) - } - - app.SetAnteHandler(ethante.NewAnteHandler(options)) -} - func (app *App) setPostHandler() { postHandler, err := posthandler.NewPostHandler( posthandler.HandlerOptions{}, @@ -794,7 +789,7 @@ func (app *App) InitChainer(ctx sdk.Context, req abci.RequestInitChain) abci.Res panic(err) } - //Passing the dymint sequencers to the sequencer module from RequestInitChain + // Passing the dymint sequencers to the sequencer module from RequestInitChain if len(req.Validators) == 0 { panic("Dymint have no sequencers defined on InitChain") } diff --git a/go.mod b/go.mod index 1f79e525..8d07b3c0 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/spf13/cobra v1.8.0 github.com/tendermint/tendermint v0.34.29 github.com/tendermint/tm-db v0.6.8-0.20220506192307-f628bb5dc95b + golang.org/x/exp v0.0.0-20240213143201-ec583247a57a google.golang.org/grpc v1.61.0 ) @@ -289,7 +290,6 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/crypto v0.22.0 // indirect - golang.org/x/exp v0.0.0-20240213143201-ec583247a57a // indirect golang.org/x/mod v0.15.0 // indirect golang.org/x/net v0.24.0 // indirect golang.org/x/oauth2 v0.16.0 // indirect diff --git a/go.sum b/go.sum index 5b6bba9d..f84b4d99 100644 --- a/go.sum +++ b/go.sum @@ -295,10 +295,17 @@ github.com/aws/aws-sdk-go-v2/service/route53 v1.1.1/go.mod h1:rLiOUrPLW/Er5kRcQ7 github.com/aws/aws-sdk-go-v2/service/sso v1.1.1/go.mod h1:SuZJxklHxLAXgLTc1iFXbEWkXs7QRTQpCLGaKIprQW0= github.com/aws/aws-sdk-go-v2/service/sts v1.1.1/go.mod h1:Wi0EBZwiz/K44YliU0EKxqTCJGUfYTWXrrBwkq736bM= github.com/aws/smithy-go v1.1.0/go.mod h1:EzMw8dbp/YJL4A5/sbhGddag+NPT7q084agLbB9LgIw= +<<<<<<< HEAD github.com/bcdevtools/block-explorer-rpc-cosmos v1.2.3 h1:nF9aC1/fTW4hb/DWO+/D4+FWD+NIh1QHp7sVjzWdJZY= github.com/bcdevtools/block-explorer-rpc-cosmos v1.2.3/go.mod h1:AWXHI5ICXK4wB+A59dNddzq5Xdc1wtQDRiIXfMw8cwc= github.com/bcdevtools/evm-block-explorer-rpc-cosmos v1.1.3 h1:j/tzqUrYOL+Uwopfl4p56nHhoIzHSldTP8VLqzhmuus= github.com/bcdevtools/evm-block-explorer-rpc-cosmos v1.1.3/go.mod h1:OFU5T7Zc38gC45ZxBwX9lQT997G553lj++j6SQTipcE= +======= +github.com/bcdevtools/block-explorer-rpc-cosmos v1.1.4 h1:RvGu0kR6VTImQYF0bhjWcyJltDFj/gg9tjCln2++sgo= +github.com/bcdevtools/block-explorer-rpc-cosmos v1.1.4/go.mod h1:AWXHI5ICXK4wB+A59dNddzq5Xdc1wtQDRiIXfMw8cwc= +github.com/bcdevtools/evm-block-explorer-rpc-cosmos v1.1.2 h1:EOsjOIaqlqgIz1V2zpIf4Av2OkPZsbGY2WQbhFTorr0= +github.com/bcdevtools/evm-block-explorer-rpc-cosmos v1.1.2/go.mod h1:krYTATKzetOs629qD/rHKaBxRc4eDmNgI9KmrEv5aXQ= +>>>>>>> e1c968f (feat(ante): allow doing vesting txs based on whitelist (#216)) github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=