Skip to content

Commit eb0387e

Browse files
committed
feat: transient (per-block) counter that increments on each zero-gas EVM tx during DeliverTx
1 parent f89059d commit eb0387e

File tree

7 files changed

+147
-0
lines changed

7 files changed

+147
-0
lines changed

evm-e2e/.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,8 @@ types
22
artifacts
33
cache
44
.env
5+
6+
# local e2e chain data
7+
.nibid-testnet2/
8+
.passkey-testnet2-wallet.json
9+
.passkey-testnet2-privkey.txt

x/evm/const.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ const (
126126
NamespaceBlockTxIndex collections.Namespace = 2
127127
NamespaceBlockLogSize collections.Namespace = 3
128128
NamespaceBlockGasUsed collections.Namespace = 4
129+
// NamespaceBlockZeroGasTxCount: number of zero-gas EVM txs in the current block.
130+
NamespaceBlockZeroGasTxCount collections.Namespace = 5
129131
)
130132

131133
var KeyPrefixBzAccState = KeyPrefixAccState.Prefix()

x/evm/errors.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const (
2525
codeErrInvalidBaseFee
2626
codeErrInvalidAccount
2727
codeErrInactivePrecompile
28+
codeErrZeroGasBlockQuotaExceeded
2829
)
2930

3031
var (
@@ -55,6 +56,9 @@ var (
5556
// ErrInvalidAccount returns an error if the account is not an EVM compatible account
5657
ErrInvalidAccount = sdkioerrors.Register(ModuleName, codeErrInvalidAccount, "account type is not a valid ethereum account")
5758

59+
// ErrZeroGasBlockQuotaExceeded returns an error when the per-block quota for zero-gas EVM txs is exceeded.
60+
ErrZeroGasBlockQuotaExceeded = sdkioerrors.Register(ModuleName, codeErrZeroGasBlockQuotaExceeded, "zero-gas block quota exceeded")
61+
5862
ErrCanonicalWnibi = "canonical WNIBI address in state is a not a smart contract"
5963
)
6064

x/evm/evmante/all_evmante.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ func NewAnteHandlerEvm(
3131
EthSigVerification,
3232
AnteStepValidateBasic,
3333
AnteStepDetectZeroGas, // must run before MempoolGasPrice, VerifyEthAcc, CanTransfer, DeductGas
34+
AnteStepZeroGasBlockQuota,
3435
AnteStepMempoolGasPrice,
3536
AnteStepBlockGasMeter,
3637
AnteStepVerifyEthAcc,
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package evmante
2+
3+
// Copyright (c) 2023-2024 Nibi, Inc.
4+
//
5+
// evmante_zero_gas_quota.go: Per-block quota enforcement for zero-gas EVM txs.
6+
7+
import (
8+
sdkioerrors "cosmossdk.io/errors"
9+
10+
"github.com/NibiruChain/nibiru/v2/x/evm"
11+
evmstate "github.com/NibiruChain/nibiru/v2/x/evm/evmstate"
12+
)
13+
14+
var _ AnteStep = AnteStepZeroGasBlockQuota
15+
16+
// maxZeroGasTxsPerBlock is a consensus-critical constant.
17+
//
18+
// Set to 0 to disable quota enforcement.
19+
//
20+
// Note: For production, making this governance-controlled (e.g. via x/sudo state
21+
// or x/evm params) is preferable to a hardcoded constant.
22+
const maxZeroGasTxsPerBlock uint64 = 100
23+
24+
// AnteStepZeroGasBlockQuota enforces a per-block cap on the number of zero-gas
25+
// EVM transactions that can be included. It uses the EVM transient store, which
26+
// is reset at Commit (end of block).
27+
//
28+
// Rationale: Zero-gas txs bypass fee-based spam controls. A quota provides a
29+
// deterministic backstop against free blockspace consumption.
30+
//
31+
// Enforcement:
32+
// - DeliverTx only. (CheckTx/mempool admission is intentionally unaffected.)
33+
func AnteStepZeroGasBlockQuota(
34+
sdb *evmstate.SDB,
35+
k *evmstate.Keeper,
36+
msgEthTx *evm.MsgEthereumTx,
37+
simulate bool,
38+
opts AnteOptionsEVM,
39+
) (err error) {
40+
if maxZeroGasTxsPerBlock == 0 {
41+
return nil
42+
}
43+
if !evmstate.IsDeliverTx(sdb.Ctx()) {
44+
return nil
45+
}
46+
if !evm.IsZeroGasEthTx(sdb.Ctx()) {
47+
return nil
48+
}
49+
50+
count := k.EvmState.BlockZeroGasTxCount.GetOr(sdb.Ctx(), 0)
51+
if count >= maxZeroGasTxsPerBlock {
52+
return sdkioerrors.Wrapf(
53+
evm.ErrZeroGasBlockQuotaExceeded,
54+
"max=%d", maxZeroGasTxsPerBlock,
55+
)
56+
}
57+
58+
k.EvmState.BlockZeroGasTxCount.Set(sdb.Ctx(), count+1)
59+
return nil
60+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package evmante_test
2+
3+
import (
4+
"math/big"
5+
"testing"
6+
7+
gethcommon "github.com/ethereum/go-ethereum/common"
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/NibiruChain/nibiru/v2/x/evm"
11+
"github.com/NibiruChain/nibiru/v2/x/evm/evmante"
12+
"github.com/NibiruChain/nibiru/v2/x/evm/evmtest"
13+
"github.com/NibiruChain/nibiru/v2/x/sudo"
14+
)
15+
16+
func TestAnteStepZeroGasBlockQuota_PerBlockTxCount(t *testing.T) {
17+
deps := evmtest.NewTestDeps()
18+
19+
// Configure ZeroGasActors with an always_zero_gas_contracts entry.
20+
targetAddr := gethcommon.HexToAddress("0x2222222222222222222222222222222222222222")
21+
deps.App.SudoKeeper.ZeroGasActors.Set(deps.Ctx(), sudo.ZeroGasActors{
22+
AlwaysZeroGasContracts: []string{targetAddr.Hex()},
23+
})
24+
25+
// Create a tx that targets the allowlisted contract with zero value.
26+
to := targetAddr
27+
tx := evm.NewTx(&evm.EvmTxArgs{
28+
ChainID: deps.App.EvmKeeper.EthChainID(deps.Ctx()),
29+
Nonce: 0,
30+
GasLimit: 50_000,
31+
GasPrice: big.NewInt(0),
32+
To: &to,
33+
Amount: big.NewInt(0),
34+
})
35+
tx.From = deps.Sender.EthAddr.Hex()
36+
37+
// Fill the quota within the same block.
38+
for i := uint64(0); i < 100; i++ {
39+
sdb := deps.NewStateDB()
40+
err := evmante.AnteStepDetectZeroGas(
41+
sdb, sdb.Keeper(), tx, false, ANTE_OPTIONS_UNUSED,
42+
)
43+
require.NoError(t, err)
44+
require.True(t, evm.IsZeroGasEthTx(sdb.Ctx()))
45+
46+
err = evmante.AnteStepZeroGasBlockQuota(
47+
sdb, sdb.Keeper(), tx, false, ANTE_OPTIONS_UNUSED,
48+
)
49+
require.NoError(t, err)
50+
deps.Commit()
51+
}
52+
53+
// The next zero-gas tx in the same block should fail.
54+
{
55+
sdb := deps.NewStateDB()
56+
err := evmante.AnteStepDetectZeroGas(
57+
sdb, sdb.Keeper(), tx, false, ANTE_OPTIONS_UNUSED,
58+
)
59+
require.NoError(t, err)
60+
require.True(t, evm.IsZeroGasEthTx(sdb.Ctx()))
61+
62+
err = evmante.AnteStepZeroGasBlockQuota(
63+
sdb, sdb.Keeper(), tx, false, ANTE_OPTIONS_UNUSED,
64+
)
65+
require.Error(t, err)
66+
require.ErrorContains(t, err, "zero-gas block quota exceeded")
67+
}
68+
}

x/evm/evmstate/evm_state.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ type EvmState struct {
4747
BlockTxIndex collections.ItemTransient[uint64]
4848
// BlockBloom: Bloom filters.
4949
BlockBloom collections.ItemTransient[[]byte]
50+
// BlockZeroGasTxCount: number of zero-gas EVM txs in the current block (transient).
51+
BlockZeroGasTxCount collections.ItemTransient[uint64]
5052

5153
NetWeiBlockDelta collections.Item[sdkmath.Int]
5254
}
@@ -88,6 +90,11 @@ func NewEvmState(
8890
evm.NamespaceBlockTxIndex,
8991
collections.Uint64ValueEncoder,
9092
),
93+
BlockZeroGasTxCount: collections.NewItemTransient(
94+
storeKeyTransient,
95+
evm.NamespaceBlockZeroGasTxCount,
96+
collections.Uint64ValueEncoder,
97+
),
9198
NetWeiBlockDelta: collections.NewItem(
9299
storeKey,
93100
evm.KeyPrefixNetWeiBlockDelta,

0 commit comments

Comments
 (0)