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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

## UNRELEASED

### SECURITY

- [\#37](https://github.com/zeta-chain/evm/pull/37) Port the upstream `cosmos/evm`
statedb balance hardening to this fork: snapshot the locked balance on the statedb
account, guard `SubBalance` against underflow, guard `AddBalance` against overflow,
make `StateDB.Commit()` atomic so a late failure discards the whole commit, reject
balance reconciliation for addresses the chain blocks from receiving funds, and use
the cached context in the `x/ibc/callbacks` packet-timeout path. The blocked-address
check deliberately replaces upstream's unconditional module-account rejection, which
is equivalent under the `evmd` configuration but allows a chain to let a module
account hold and move funds.

- [\#6](https://github.com/zeta-chain/evm/pull/6 Add legacy ethermint protocol buffer files to support querying legacy transactions

### DEPENDENCIES
Expand Down
45 changes: 4 additions & 41 deletions precompiles/common/balance_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,9 @@ package common
import (
"fmt"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/holiman/uint256"

"github.com/cosmos/evm/utils"
"github.com/cosmos/evm/x/vm/statedb"
evmtypes "github.com/cosmos/evm/x/vm/types"

sdk "github.com/cosmos/cosmos-sdk/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
Expand Down Expand Up @@ -43,25 +39,25 @@ func (bh *BalanceHandler) AfterBalanceChange(ctx sdk.Context, stateDB *statedb.S
for _, event := range events[bh.prevEventsLen:] {
switch event.Type {
case banktypes.EventTypeCoinSpent:
spenderHexAddr, err := parseHexAddress(event, banktypes.AttributeKeySpender)
spenderHexAddr, err := ParseHexAddress(event, banktypes.AttributeKeySpender)
if err != nil {
return fmt.Errorf("failed to parse spender address from event %q: %w", banktypes.EventTypeCoinSpent, err)
}

amount, err := parseAmount(event)
amount, err := ParseAmount(event)
if err != nil {
return fmt.Errorf("failed to parse amount from event %q: %w", banktypes.EventTypeCoinSpent, err)
}

stateDB.SubBalance(spenderHexAddr, amount, tracing.BalanceChangeUnspecified)

case banktypes.EventTypeCoinReceived:
receiverHexAddr, err := parseHexAddress(event, banktypes.AttributeKeyReceiver)
receiverHexAddr, err := ParseHexAddress(event, banktypes.AttributeKeyReceiver)
if err != nil {
return fmt.Errorf("failed to parse receiver address from event %q: %w", banktypes.EventTypeCoinReceived, err)
}

amount, err := parseAmount(event)
amount, err := ParseAmount(event)
if err != nil {
return fmt.Errorf("failed to parse amount from event %q: %w", banktypes.EventTypeCoinReceived, err)
}
Expand All @@ -72,36 +68,3 @@ func (bh *BalanceHandler) AfterBalanceChange(ctx sdk.Context, stateDB *statedb.S

return nil
}

func parseHexAddress(event sdk.Event, key string) (common.Address, error) {
attr, ok := event.GetAttribute(key)
if !ok {
return common.Address{}, fmt.Errorf("event %q missing attribute %q", event.Type, key)
}

accAddr, err := sdk.AccAddressFromBech32(attr.Value)
if err != nil {
return common.Address{}, fmt.Errorf("invalid address %q: %w", attr.Value, err)
}

return common.BytesToAddress(accAddr), nil
}

func parseAmount(event sdk.Event) (*uint256.Int, error) {
amountAttr, ok := event.GetAttribute(sdk.AttributeKeyAmount)
if !ok {
return nil, fmt.Errorf("event %q missing attribute %q", banktypes.EventTypeCoinSpent, sdk.AttributeKeyAmount)
}

amountCoins, err := sdk.ParseCoinsNormalized(amountAttr.Value)
if err != nil {
return nil, fmt.Errorf("failed to parse coins from %q: %w", amountAttr.Value, err)
}

amountBigInt := amountCoins.AmountOf(evmtypes.GetEVMCoinDenom()).BigInt()
amount, err := utils.Uint256FromBigInt(evmtypes.ConvertAmountTo18DecimalsBigInt(amountBigInt))
if err != nil {
return nil, fmt.Errorf("failed to convert coin amount to Uint256: %w", err)
}
return amount, nil
}
59 changes: 52 additions & 7 deletions precompiles/common/balance_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func TestParseHexAddress(t *testing.T) {

event := tc.maleate()

addr, err := parseHexAddress(event, tc.key)
addr, err := ParseHexAddress(event, tc.key)
if tc.expError {
require.Error(t, err)
return
Expand All @@ -103,27 +103,70 @@ func TestParseHexAddress(t *testing.T) {
func TestParseAmount(t *testing.T) {
testCases := []struct {
name string
chainID testconstants.ChainID
maleate func() sdk.Event
expAmt *uint256.Int
expError bool
}{
{
name: "valid amount",
name: "valid amount",
chainID: testconstants.ExampleChainID,
maleate: func() sdk.Event {
coinStr := sdk.NewCoins(sdk.NewInt64Coin(evmtypes.GetEVMCoinDenom(), 5)).String()
return sdk.NewEvent("bank", sdk.NewAttribute(sdk.AttributeKeyAmount, coinStr))
},
expAmt: uint256.NewInt(5),
},
{
name: "missing amount",
name: "unrelated denom is ignored",
chainID: testconstants.ExampleChainID,
maleate: func() sdk.Event {
coinStr := sdk.NewCoins(sdk.NewInt64Coin("foobar", 7)).String()
return sdk.NewEvent("bank", sdk.NewAttribute(sdk.AttributeKeyAmount, coinStr))
},
expAmt: uint256.NewInt(0),
},
{
name: "base denom is scaled to 18 decimals",
chainID: testconstants.SixDecimalsChainID,
maleate: func() sdk.Event {
coinStr := sdk.NewCoins(sdk.NewInt64Coin(evmtypes.GetEVMCoinDenom(), 100)).String()
return sdk.NewEvent("bank", sdk.NewAttribute(sdk.AttributeKeyAmount, coinStr))
},
expAmt: uint256.NewInt(100_000_000_000_000),
},
{
name: "extended denom is taken as is",
chainID: testconstants.SixDecimalsChainID,
maleate: func() sdk.Event {
coinStr := sdk.NewCoins(sdk.NewInt64Coin(evmtypes.GetEVMCoinExtendedDenom(), 500)).String()
return sdk.NewEvent("bank", sdk.NewAttribute(sdk.AttributeKeyAmount, coinStr))
},
expAmt: uint256.NewInt(500),
},
{
name: "base and extended denoms are summed",
chainID: testconstants.SixDecimalsChainID,
maleate: func() sdk.Event {
coinStr := sdk.NewCoins(
sdk.NewInt64Coin(evmtypes.GetEVMCoinDenom(), 100),
sdk.NewInt64Coin(evmtypes.GetEVMCoinExtendedDenom(), 500),
).String()
return sdk.NewEvent("bank", sdk.NewAttribute(sdk.AttributeKeyAmount, coinStr))
},
expAmt: uint256.NewInt(100_000_000_000_500),
},
{
name: "missing amount",
chainID: testconstants.ExampleChainID,
maleate: func() sdk.Event {
return sdk.NewEvent("bank")
},
expError: true,
},
{
name: "invalid coins",
name: "invalid coins",
chainID: testconstants.ExampleChainID,
maleate: func() sdk.Event {
return sdk.NewEvent("bank", sdk.NewAttribute(sdk.AttributeKeyAmount, "invalid"))
},
Expand All @@ -133,16 +176,18 @@ func TestParseAmount(t *testing.T) {

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
setupBalanceHandlerTest(t)
configurator := evmtypes.NewEVMConfigurator()
configurator.ResetTestConfig()
require.NoError(t, configurator.WithEVMCoinInfo(testconstants.ExampleChainCoinInfo[tc.chainID]).Configure())

amt, err := parseAmount(tc.maleate())
amt, err := ParseAmount(tc.maleate())
if tc.expError {
require.Error(t, err)
return
}

require.NoError(t, err)
require.True(t, amt.Eq(tc.expAmt))
require.Equal(t, tc.expAmt.String(), amt.String())
})
}
}
Expand Down
11 changes: 9 additions & 2 deletions precompiles/common/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package common

import (
"fmt"
"math/big"

"github.com/ethereum/go-ethereum/common"
"github.com/holiman/uint256"
Expand Down Expand Up @@ -38,8 +39,14 @@ func ParseAmount(event sdk.Event) (*uint256.Int, error) {
return nil, fmt.Errorf("failed to parse coins from %q: %w", amountAttr.Value, err)
}

amountBigInt := amountCoins.AmountOf(evmtypes.GetEVMCoinDenom()).BigInt()
amount, err := utils.Uint256FromBigInt(evmtypes.ConvertAmountTo18DecimalsBigInt(amountBigInt))
baseAmount := amountCoins.AmountOf(evmtypes.GetEVMCoinDenom()).BigInt()
amountBigInt := evmtypes.ConvertAmountTo18DecimalsBigInt(baseAmount)
if evmtypes.GetEVMCoinExtendedDenom() != evmtypes.GetEVMCoinDenom() {
extendedAmount := amountCoins.AmountOf(evmtypes.GetEVMCoinExtendedDenom()).BigInt()
amountBigInt = new(big.Int).Add(amountBigInt, extendedAmount)
}

amount, err := utils.Uint256FromBigInt(amountBigInt)
if err != nil {
return nil, fmt.Errorf("failed to convert coin amount to Uint256: %w", err)
}
Expand Down
139 changes: 139 additions & 0 deletions tests/integration/precompiles/staking/test_integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/cosmos/evm/precompiles/testutil"
"github.com/cosmos/evm/precompiles/testutil/contracts"
cosmosevmutil "github.com/cosmos/evm/testutil/constants"
basefactory "github.com/cosmos/evm/testutil/integration/base/factory"
"github.com/cosmos/evm/testutil/integration/evm/network"
"github.com/cosmos/evm/testutil/integration/evm/utils"
testutiltx "github.com/cosmos/evm/testutil/tx"
Expand All @@ -35,6 +36,8 @@ import (
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/types/query"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
vestingtypes "github.com/cosmos/cosmos-sdk/x/auth/vesting/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
)

Expand Down Expand Up @@ -428,6 +431,90 @@ func TestPrecompileIntegrationTestSuite(t *testing.T, create network.CreateEvmAp
})
})

Context("from a vesting account", func() {
var (
vestAddr sdk.AccAddress
vestPriv *ethsecp256k1.PrivKey
amtLocked math.Int
amtSpendable math.Int
preBal math.Int
preSupply math.Int
)

BeforeEach(func() {
// setup vesting account to delegate from
vestAddr, vestPriv = testutiltx.NewAccAddressAndKey()
amtLocked = math.NewInt(2e18)
amtSpendable = math.NewInt(2e18)

funder := s.keyring.GetKey(0)
startTime := s.network.GetContext().BlockTime().Unix()
createMsg := &vestingtypes.MsgCreateVestingAccount{
FromAddress: funder.AccAddr.String(),
ToAddress: vestAddr.String(),
Amount: sdk.NewCoins(sdk.NewCoin(s.bondDenom, amtLocked)),
EndTime: startTime + 365*24*3600,
Delayed: false,
}
sendMsg := banktypes.NewMsgSend(
funder.AccAddr, vestAddr,
sdk.NewCoins(sdk.NewCoin(s.bondDenom, amtSpendable)),
)
_, err := s.factory.CommitCosmosTx(funder.Priv, basefactory.CosmosTxArgs{
Msgs: []sdk.Msg{createMsg, sendMsg},
})
Expect(err).To(BeNil(), "error while submitting vesting setup tx")

ctx := s.network.GetContext()
_, ok := s.network.App.GetAccountKeeper().GetAccount(ctx, vestAddr).(*vestingtypes.ContinuousVestingAccount)
Expect(ok).To(BeTrue(), "expected vesting account to persist after tx commit")
spendable := s.network.App.GetBankKeeper().SpendableCoin(ctx, vestAddr, s.bondDenom).Amount
Expect(spendable).To(Equal(amtSpendable), "unexpected spendable balance after vesting setup")

preBalRes, err := s.grpcHandler.GetBalanceFromBank(vestAddr, s.bondDenom)
Expect(err).To(BeNil(), "error while getting pre balance")
preBal = preBalRes.Balance.Amount
Expect(preBal).To(Equal(amtLocked.Add(amtSpendable)), "expected vester pre bank balance to equal OV + extra")

preSupplyRes, err := s.grpcHandler.GetTotalSupply()
Expect(err).To(BeNil(), "error while getting pre supply")
preSupply = preSupplyRes.Supply.AmountOf(s.bondDenom)
})

It("should preserve bank balance and total supply when delegating within spendable", func() {
// delegating less than spendable balance and less than
// locked balance
delAmt := big.NewInt(1e18)
gasPrice := big.NewInt(1e9)

callArgs.Args = []interface{}{
common.BytesToAddress(vestAddr), valAddr.String(), delAmt,
}
delTxArgs := txArgs
delTxArgs.GasPrice = gasPrice

logCheckArgs := passCheck.WithExpEvents(staking.EventTypeDelegate)
res, _, err := s.factory.CallContractAndCheckLogs(
vestPriv, delTxArgs, callArgs, logCheckArgs,
)
Expect(err).To(BeNil(), "error while calling the smart contract: %v", err)
Expect(s.network.NextBlock()).To(BeNil())

postBalRes, err := s.grpcHandler.GetBalanceFromBank(vestAddr, s.bondDenom)
Expect(err).To(BeNil(), "error while getting post balance")
postSupplyRes, err := s.grpcHandler.GetTotalSupply()
Expect(err).To(BeNil(), "error while getting post supply")
postBal := postBalRes.Balance.Amount
postSupply := postSupplyRes.Supply.AmountOf(s.bondDenom)

gasCost := new(big.Int).Mul(gasPrice, big.NewInt(res.GasUsed))
expBalDrop := new(big.Int).Add(delAmt, gasCost)
actualBalDrop := preBal.Sub(postBal).BigInt()
Expect(actualBalDrop).To(Equal(expBalDrop), "vesting bank balance dropped by more than delegation amount + gas")
Expect(postSupply).To(Equal(preSupply), "unexpected total supply after delegating from vesting account")
})
})

Context("on behalf of another account", func() {
It("should not delegate if delegator address is not the msg.sender", func() {
delegator := s.keyring.GetKey(0)
Expand Down Expand Up @@ -2041,6 +2128,58 @@ func TestPrecompileIntegrationTestSuite(t *testing.T, create network.CreateEvmAp
bondedTokensPoolFinalBalance := balRes.Balance
Expect(bondedTokensPoolFinalBalance.Amount).To(Equal(bondedTokensPoolInitialBalance.Amount))
})

DescribeTable("should not delegate and update balances accordingly across orderings - internal transfer to tokens pool",
func(tc struct {
before bool
after bool
msgAmt *big.Int
}) {
args.MethodName = "testDelegateWithTransfer"
args.Args = []interface{}{
common.BytesToAddress(bondedTokensPoolAccAddr),
s.keyring.GetAddr(0), valAddr.String(), tc.before, tc.after,
}

txArgs.To = &contractTwoAddr
if tc.msgAmt != nil {
txArgs.Amount = tc.msgAmt
}

reverReasonCheck := execRevertedCheck.WithErrContains(
errorsmod.Wrapf(
sdkerrors.ErrUnauthorized, "%s is not allowed to receive funds", bondedTokensPoolAccAddr.String(),
).Error(),
)

_, _, err := s.factory.CallContractAndCheckLogs(
s.keyring.GetPrivKey(0),
txArgs,
args,
reverReasonCheck,
)
Expect(err).To(BeNil(), "error while calling the smart contract: %v", err)
Expect(s.network.NextBlock()).To(BeNil())

balRes, err := s.grpcHandler.GetBalanceFromBank(contractTwoAddr.Bytes(), s.bondDenom)
Expect(err).To(BeNil())
Expect(balRes.Balance.Amount).To(Equal(contractInitialBalance.Amount))

balRes, err = s.grpcHandler.GetBalanceFromBank(bondedTokensPoolAccAddr, s.bondDenom)
Expect(err).To(BeNil())
Expect(balRes.Balance.Amount).To(Equal(bondedTokensPoolInitialBalance.Amount))
},
Entry("internal transfer after precompile call", struct {
before bool
after bool
msgAmt *big.Int
}{before: false, after: true, msgAmt: nil}),
Entry("internal transfer after precompile call with matching amounts", struct {
before bool
after bool
msgAmt *big.Int
}{before: false, after: true, msgAmt: big.NewInt(15)}),
)
})

It("should not delegate when validator does not exist", func() {
Expand Down
Loading
Loading