Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
18 changes: 16 additions & 2 deletions ethereum/eip712/encoding.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"strconv"

apitypes "github.com/ethereum/go-ethereum/signer/core/apitypes"

Expand Down Expand Up @@ -109,7 +110,7 @@ func decodeAminoSignDoc(signDocBytes []byte) (apitypes.TypedData, error) {
}

typedData, err := WrapTxToTypedData(
eip155ChainID,
chainIDFromSignDoc(aminoDoc.ChainID),
signDocBytes,
)
if err != nil {
Expand Down Expand Up @@ -203,7 +204,7 @@ func decodeProtobufSignDoc(signDocBytes []byte) (apitypes.TypedData, error) {
}

typedData, err := WrapTxToTypedData(
eip155ChainID,
chainIDFromSignDoc(signDoc.ChainId),
signBytes,
)
if err != nil {
Expand All @@ -213,6 +214,19 @@ func decodeProtobufSignDoc(signDocBytes []byte) (apitypes.TypedData, error) {
return typedData, nil
}

// chainIDFromSignDoc uses the chain id on the sign doc when it parses as a uint64.
// Otherwise it falls back to the configured EVM chain id.
func chainIDFromSignDoc(chainIDStr string) uint64 {
if chainIDStr == "" {
return eip155ChainID
}
parsed, err := strconv.ParseUint(chainIDStr, 10, 64)
if err != nil {
return eip155ChainID
}
return parsed
}

// validateCodecInit ensures that both Amino and Protobuf encoding codecs have been set on app init,
// so the module does not panic if either codec is not found.
func validateCodecInit() error {
Expand Down
1 change: 1 addition & 0 deletions mempool/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type VMKeeperI interface {
GetParams(ctx sdk.Context) (params vmtypes.Params)
GetEvmCoinInfo(ctx sdk.Context) (coinInfo vmtypes.EvmCoinInfo)
GetAccount(ctx sdk.Context, addr common.Address) *statedb.Account
IsBaseAccountOrEmpty(ctx sdk.Context, addr common.Address) bool
GetState(ctx sdk.Context, addr common.Address, key common.Hash) common.Hash
GetCode(ctx sdk.Context, codeHash common.Hash) []byte
GetCodeHash(ctx sdk.Context, addr common.Address) common.Hash
Expand Down
5 changes: 4 additions & 1 deletion mempool/mempool_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
errorsmod "cosmossdk.io/errors"

sdk "github.com/cosmos/cosmos-sdk/types"

"github.com/cosmos/evm/mempool/txpool"
)

const (
Expand Down Expand Up @@ -88,7 +90,8 @@ func (m *Mempool) NewCheckTxHandler(txDecoder sdk.TxDecoder, timeout time.Durati

// ErrAsCheckTxResponse converts an error to a ResponseCheckTx object, respecting error wrapping.
func ErrAsCheckTxResponse(err error) *abci.ResponseCheckTx {
if err == nil {
if err == nil || errors.Is(err, txpool.ErrAlreadyKnown) {
// Already-known is success so a promoted nonce-gap tx can be re-gossiped.
return &abci.ResponseCheckTx{Code: abci.CodeTypeOK}
}

Expand Down
18 changes: 18 additions & 0 deletions mempool/mocks/VMKeeperI.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 8 additions & 4 deletions precompiles/common/balance_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ func (bh *BalanceHandler) AfterBalanceChange(ctx sdk.Context, stateDB *statedb.S
if err != nil {
return fmt.Errorf("failed to parse spender address from event %q: %w", banktypes.EventTypeCoinSpent, err)
}
if bh.bankKeeper.BlockedAddr(spenderAddr) {
// Bypass blocked addresses
if bh.bankKeeper.BlockedAddr(spenderAddr) || !isMirrorableEVMAddress(spenderAddr) {
// Bypass blocked addresses and accounts that are not 20 bytes.
continue
}

Expand All @@ -93,8 +93,8 @@ func (bh *BalanceHandler) AfterBalanceChange(ctx sdk.Context, stateDB *statedb.S
if err != nil {
return fmt.Errorf("failed to parse receiver address from event %q: %w", banktypes.EventTypeCoinReceived, err)
}
if bh.bankKeeper.BlockedAddr(receiverAddr) {
// Bypass blocked addresses
if bh.bankKeeper.BlockedAddr(receiverAddr) || !isMirrorableEVMAddress(receiverAddr) {
// Bypass blocked addresses and accounts that are not 20 bytes.
continue
}

Expand All @@ -113,3 +113,7 @@ func (bh *BalanceHandler) AfterBalanceChange(ctx sdk.Context, stateDB *statedb.S

return nil
}

func isMirrorableEVMAddress(addr sdk.AccAddress) bool {
return len(addr) == common.AddressLength
}
8 changes: 8 additions & 0 deletions precompiles/distribution/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"

"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/vm"

cmn "github.com/cosmos/evm/precompiles/common"
Expand Down Expand Up @@ -102,6 +103,13 @@ func (p Precompile) SetWithdrawAddress(
return nil, fmt.Errorf(cmn.ErrRequesterIsNotMsgSender, msgSender.String(), delegatorHexAddr.String())
}

if withdrawAddr, decErr := p.addrCdc.StringToBytes(msg.WithdrawAddress); decErr == nil && len(withdrawAddr) != common.AddressLength {
return nil, fmt.Errorf(
"withdraw address must be exactly %d bytes to be EVM-compatible; got %d bytes: %s",
common.AddressLength, len(withdrawAddr), msg.WithdrawAddress,
)
}

if _, err = p.distributionMsgServer.SetWithdrawAddress(ctx, msg); err != nil {
return nil, err
}
Expand Down
8 changes: 8 additions & 0 deletions rpc/backend/call_tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

"github.com/cosmos/evm/mempool/txpool"
rpctypes "github.com/cosmos/evm/rpc/types"
evmtrace "github.com/cosmos/evm/trace"
evmtypes "github.com/cosmos/evm/x/vm/types"
Expand Down Expand Up @@ -147,6 +148,13 @@ func (b *Backend) SendRawTransaction(ctx context.Context, data hexutil.Bytes) (r

txHash := tx.Hash()

// User-submitted RPC txs should get "already known" instead of a second insert.
if b.Mempool != nil {
if pool := b.Mempool.GetTxPool(); pool != nil && pool.Has(txHash) {
return txHash, txpool.ErrAlreadyKnown
}
}

// publish tx directly to app-side mempool, avoiding broadcasting to
// consensus layer.
// we are directly calling into the mempool rather than the ABCI
Expand Down
1 change: 1 addition & 0 deletions tests/integration/x/vm/test_state_transition.go
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,7 @@ func (s *KeeperTestSuite) TestRefundGas() {
unitNetwork.GetContext(),
*coreMsg,
refund,
gasUsed,
unitNetwork.GetBaseDenom(),
)

Expand Down
1 change: 1 addition & 0 deletions x/erc20/types/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ type EVMKeeper interface {
SetCode(ctx sdk.Context, hash []byte, bytecode []byte)
SetAccount(ctx sdk.Context, address common.Address, account statedb.Account) error
GetAccount(ctx sdk.Context, address common.Address) *statedb.Account
IsBaseAccountOrEmpty(ctx sdk.Context, address common.Address) bool
IsContract(ctx sdk.Context, address common.Address) bool
GetState(ctx sdk.Context, addr common.Address, key common.Hash) common.Hash
GetCodeHash(ctx sdk.Context, addr common.Address) common.Hash
Expand Down
18 changes: 18 additions & 0 deletions x/erc20/types/mocks/EVMKeeper.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions x/ibc/callbacks/types/expected_keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type EVMKeeper interface {
CallEVMWithData(ctx sdk.Context, stateDB *statedb.StateDB, from common.Address, contract *common.Address, data []byte, commit bool, callFromPrecompile bool, gasCap *big.Int) (*evmtypes.MsgEthereumTxResponse, error)
GetAccountOrEmpty(ctx sdk.Context, addr common.Address) statedb.Account
GetAccount(ctx sdk.Context, addr common.Address) *statedb.Account
IsBaseAccountOrEmpty(ctx sdk.Context, addr common.Address) bool
IsContract(ctx sdk.Context, addr common.Address) bool
GetState(ctx sdk.Context, addr common.Address, key common.Hash) common.Hash
GetCode(ctx sdk.Context, codeHash common.Hash) []byte
Expand Down
32 changes: 31 additions & 1 deletion x/vm/keeper/gas.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ import (
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
)

// ContextPaidFeesKey stores the coins actually charged for an EVM tx.
// Fee abstraction sets this so the refund uses that denom, capped by the amount paid.
type ContextPaidFeesKey struct{}

// GetEthIntrinsicGas returns the intrinsic gas cost for the transaction
func (k *Keeper) GetEthIntrinsicGas(ctx sdk.Context, msg core.Message, cfg *params.ChainConfig,
isContractCreation bool,
Expand All @@ -40,10 +44,15 @@ func (k *Keeper) GetEthIntrinsicGas(ctx sdk.Context, msg core.Message, cfg *para
// consumed in the transaction. Additionally, the function sets the total gas consumed to the value
// returned by the EVM execution, thus ignoring the previous intrinsic gas consumed during in the
// AnteHandler.
func (k *Keeper) RefundGas(ctx sdk.Context, msg core.Message, leftoverGas uint64, denom string) (err error) {
func (k *Keeper) RefundGas(ctx sdk.Context, msg core.Message, leftoverGas, gasUsed uint64, denom string) (err error) {
ctx, span := ctx.StartSpan(tracer, "RefundGas", trace.WithAttributes(attribute.Int64("leftover_gas", int64(leftoverGas)))) //nolint:gosec // G115
defer func() { evmtrace.EndSpanErr(span, err) }()

// No gas was consumed, so there is nothing to refund against.
if gasUsed == 0 {
return nil
}

// Return EVM tokens for remaining gas, exchanged at the original rate.
remaining := new(big.Int).Mul(new(big.Int).SetUint64(leftoverGas), msg.GasPrice)

Expand All @@ -52,6 +61,27 @@ func (k *Keeper) RefundGas(ctx sdk.Context, msg core.Message, leftoverGas uint64
// negative refund errors
return errorsmod.Wrapf(types.ErrInvalidRefund, "refunded amount value cannot be negative %d", remaining.Int64())
case 1:
// Fee abstraction records the coin that was actually charged.
// Refund that coin, scaled by leftover gas over the tx gas limit,
// so the sender never receives more than was paid.
if val := ctx.Value(ContextPaidFeesKey{}); val != nil {
if paidCoins, ok := val.(sdk.Coins); ok && !paidCoins.IsZero() {
if len(paidCoins) != 1 {
return errorsmod.Wrapf(types.ErrInvalidRefund, "expected a single coin for EVM refunds, got %d", len(paidCoins))
}
paidCoin := paidCoins[0]
denom = paidCoin.Denom
remaining = new(big.Int).Div(
new(big.Int).Mul(paidCoin.Amount.BigInt(), new(big.Int).SetUint64(leftoverGas)),
new(big.Int).SetUint64(msg.GasLimit),
)
}
}

if remaining.Sign() <= 0 {
return nil
}

// positive amount refund
refundedCoins := sdk.Coins{sdk.NewCoin(denom, sdkmath.NewIntFromBigInt(remaining))}

Expand Down
2 changes: 1 addition & 1 deletion x/vm/keeper/state_transition.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ func (k *Keeper) ApplyTransaction(ctx sdk.Context, tx *ethtypes.Transaction) (_
if msg.GasLimit > res.GasUsed {
remainingGas = msg.GasLimit - res.GasUsed
}
if err = k.RefundGas(ctx, *msg, remainingGas, types.GetEVMCoinDenom()); err != nil {
if err = k.RefundGas(ctx, *msg, remainingGas, res.GasUsed, types.GetEVMCoinDenom()); err != nil {
return nil, errorsmod.Wrapf(err, "failed to refund leftover gas to sender %s", msg.From)
}

Expand Down
12 changes: 12 additions & 0 deletions x/vm/keeper/statedb.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,18 @@ func (k *Keeper) GetAccount(ctx sdk.Context, addr common.Address) *statedb.Accou
)
}

// IsBaseAccountOrEmpty reports whether addr has no Cosmos account yet, or
// carries exactly the plain BaseAccount type. CreateAccount uses it to reject
// EVM contract deployment onto vesting, module, or other non-base accounts.
func (k *Keeper) IsBaseAccountOrEmpty(ctx sdk.Context, addr common.Address) bool {
acct := k.accountKeeper.GetAccount(ctx, addr.Bytes())
if acct == nil {
return true
}
_, ok := acct.(*authtypes.BaseAccount)
return ok
}

// GetState loads contract state from database.
func (k *Keeper) GetState(ctx sdk.Context, addr common.Address, key common.Hash) common.Hash {
ctx, span := ctx.StartSpan(tracer, "GetState", trace.WithAttributes(
Expand Down
4 changes: 4 additions & 0 deletions x/vm/statedb/commit_atomicity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ var _ statedb.Keeper = &atomicTestKeeper{}

func (k *atomicTestKeeper) store(ctx sdk.Context) storetypes.KVStore { return ctx.KVStore(k.key) }

func (k *atomicTestKeeper) IsBaseAccountOrEmpty(_ sdk.Context, _ common.Address) bool {
return true
}

func (k *atomicTestKeeper) GetAccount(ctx sdk.Context, addr common.Address) *statedb.Account {
bz := k.store(ctx).Get(addr.Bytes())
if bz == nil {
Expand Down
4 changes: 4 additions & 0 deletions x/vm/statedb/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ type ExtStateDB interface {
type Keeper interface {
// Read methods
GetAccount(ctx sdk.Context, addr common.Address) *Account
// IsBaseAccountOrEmpty reports whether addr has no Cosmos account yet, or
// carries exactly the plain BaseAccount type. EVM contract code may only
// attach to such an address.
IsBaseAccountOrEmpty(ctx sdk.Context, addr common.Address) bool
GetState(ctx sdk.Context, addr common.Address, key common.Hash) common.Hash
GetCode(ctx sdk.Context, codeHash common.Hash) []byte
GetCodeHash(ctx sdk.Context, addr common.Address) common.Hash
Expand Down
4 changes: 4 additions & 0 deletions x/vm/statedb/mock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ type MockKeeper struct {
codes map[common.Hash][]byte
}

func (k MockKeeper) IsBaseAccountOrEmpty(_ sdk.Context, _ common.Address) bool {
return true
}

func (k MockKeeper) GetCodeHash(_ sdk.Context, addr common.Address) common.Hash {
return common.HexToHash(addr.Hex())
}
Expand Down
3 changes: 3 additions & 0 deletions x/vm/statedb/statedb.go
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,9 @@ func (s *StateDB) createObject(addr common.Address) *stateObject {
// pre-funded-then-deployed flow is preserved without any balance carry-over
// here. See go-ethereum PR #29520.
func (s *StateDB) CreateAccount(addr common.Address) {
if !s.keeper.IsBaseAccountOrEmpty(s.ctx, addr) {
panic(fmt.Sprintf("cannot deploy EVM contract on top of non-base account %s", addr.Hex()))
}
s.createObject(addr)
}

Expand Down
40 changes: 40 additions & 0 deletions x/vm/statedb/statedb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,46 @@ func (suite *StateDBTestSuite) TestSubBalanceUnderflowPanics() {
})
}

func (suite *StateDBTestSuite) TestCreateAccountBlocksNonBaseAccountTypes() {
testCases := []struct {
name string
malleate func(*mocks.EVMKeeper, common.Address)
expectPanic bool
}{
{"no account", func(*mocks.EVMKeeper, common.Address) {}, false},
{"empty BaseAccount", func(k *mocks.EVMKeeper, addr common.Address) {
err := k.SetAccount(sdk.Context{}, addr, statedb.Account{Balance: uint256.NewInt(0), CodeHash: mocks.EmptyCodeHash})
suite.Require().NoError(err)
}, false},
{"funded BaseAccount", func(k *mocks.EVMKeeper, addr common.Address) {
err := k.SetAccount(sdk.Context{}, addr, statedb.Account{Balance: uint256.NewInt(100), CodeHash: mocks.EmptyCodeHash})
suite.Require().NoError(err)
}, false},
{"non-BaseAccount type present", func(k *mocks.EVMKeeper, addr common.Address) {
err := k.SetAccount(sdk.Context{}, addr, statedb.Account{Balance: uint256.NewInt(2), CodeHash: mocks.EmptyCodeHash})
suite.Require().NoError(err)
k.SetBlockedAccountType(addr, true)
}, true},
}

for _, tc := range testCases {
suite.Run(tc.name, func() {
keeper := mocks.NewEVMKeeper()
addr := common.BigToAddress(big.NewInt(200))
tc.malleate(keeper, addr)

db := statedb.New(newTestCtx(), keeper, emptyTxConfig)
createAccount := func() { db.CreateAccount(addr) }

if tc.expectPanic {
suite.Require().Panics(createAccount)
} else {
suite.Require().NotPanics(createAccount)
}
})
}
}

func (suite *StateDBTestSuite) TestState() {
key1 := common.BigToHash(big.NewInt(1))
value1 := common.BigToHash(big.NewInt(1))
Expand Down
Loading
Loading