Skip to content

Commit 8ebcfbc

Browse files
committed
test(evm): make bank inflation guard actually reproduce the exploit
TestBankSend_NoSupplyInflation as first written was a false guard: a plain CallEVMWithData(bank.send) passes on pre-#332 (unfixed) bank code too, because the native-token inflation only triggers when the sender's EVM stateObject balance is authoritative at Commit. Give the sender code (EIP-7702-style delegation) and load its stateObject so the pre-fix reconciliation mints the debited amount back. Verified: on pre-#332 bank code the test now fails with supply += 5e8 (the sent amount); on #332 it stays flat. The SetCode is load-bearing.
1 parent dfacdee commit 8ebcfbc

1 file changed

Lines changed: 18 additions & 6 deletions

File tree

x/evm/precompiles/bank/tx_test.go

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,18 @@ func (s *PrecompileTestSuite) TestBankSend_EVMDispatchSuccess() {
8383
}
8484

8585
// TestBankSend_NoSupplyInflation is the regression guard for the native-token
86-
// inflation fixed by #332. bank.send is a transfer, so total supply MUST be
87-
// unchanged. Before the fix, the keeper coin move left the sender's EVM
88-
// stateObject balance stale; StateDB.Commit then reconciled it by minting the
89-
// debited amount back to the sender, inflating total supply. The BalanceHandler
90-
// (RunNativeAction) keeps the stateObject reconciled so Commit mints nothing.
86+
// inflation fixed by #332.
87+
//
88+
// The exploit needs the sender to be an EIP-7702-delegated EOA: giving the sender
89+
// code makes its EVM stateObject balance authoritative at StateDB.Commit. Without
90+
// the fix, the keeper coin move debits the sender's bank balance but leaves the
91+
// stale stateObject balance untouched; Commit then reconciles by minting the
92+
// debited amount back to the sender — total supply inflates by exactly the sent
93+
// amount. (Verified: on pre-#332 bank code this test fails with supply += 5e8;
94+
// a plain send without SetCode does NOT reproduce it, so the SetCode is load-bearing.)
95+
//
96+
// The BalanceHandler (RunNativeAction) keeps the stateObject reconciled during the
97+
// call, so Commit mints nothing and total supply stays flat.
9198
func (s *PrecompileTestSuite) TestBankSend_NoSupplyInflation() {
9299
s.mustEnableStaticPrecompiles()
93100

@@ -98,12 +105,17 @@ func (s *PrecompileTestSuite) TestBankSend_NoSupplyInflation() {
98105

99106
precompileAddr := bank.GetAddress()
100107
stateDB := statedb.New(s.ctx, s.app.EvmKeeper, statedb.NewEmptyTxConfig())
108+
// Make the sender a 7702-style delegated account (has code) and load its
109+
// stateObject, so its cached balance is authoritative at Commit — this is the
110+
// condition under which the pre-fix reconciliation minted the debited amount.
111+
stateDB.SetCode(s.address, []byte{0x60, 0x00})
112+
_ = stateDB.GetBalance(s.address)
101113
res, err := s.app.EvmKeeper.CallEVMWithData(s.ctx, stateDB, s.address, &precompileAddr, input, true, false, nil)
102114
s.Require().NoError(err)
103115
s.Require().False(res.Failed(), "evm call reverted: %s", res.VmError)
104116

105117
supplyAfter := s.app.BankKeeper.GetSupply(s.ctx, utils.BaseDenom).Amount
106-
s.Require().Equal(supplyBefore.String(), supplyAfter.String(), "bank.send must not change total supply (no inflation)")
118+
s.Require().Equal(supplyBefore.String(), supplyAfter.String(), "bank.send must not inflate total supply")
107119
}
108120

109121
// TestBankSend_FailureRevertsCleanly pins the native revert semantics: an

0 commit comments

Comments
 (0)