Skip to content
Open
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
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
4 changes: 2 additions & 2 deletions tests/integration/precompiles/staking/test_staking.go
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ func (s *PrecompileTestSuite) TestRun() {
s.Require().NoError(err, "failed to pack input")
return input
},
1, // use gas > 0 to avoid doing gas estimation
25000, // use enough gas to avoid out of gas error
true,
false,
"write protection",
Expand All @@ -391,7 +391,7 @@ func (s *PrecompileTestSuite) TestRun() {
func(_ keyring.Key) []byte {
return []byte("invalid")
},
1, // use gas > 0 to avoid doing gas estimation
25000, // use enough gas to avoid out of gas error
false,
false,
"no method with id",
Expand Down
Loading
Loading