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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# CHANGELOG

## UNRELEASED

### IMPROVEMENTS
- [\#1238](https://github.com/cosmos/evm/pull/1238) Validate ICS-20 acknowledgement encoding in the erc20 IBC v2 middleware.
- [\#1243](https://github.com/cosmos/evm/pull/1243) Deploy contracts from an EOA rather than a module account in the test helpers. It is also now required: contract creation bumps the sender's nonce, `SetAccount` persists nonce and balance together, and the EVM commit path may not write a module account's balance.

### BUG FIXES
- [\#1222](https://github.com/cosmos/evm/pull/1222) Propagate ERC20 conversion ack in IBC v2 `OnRecvPacket`.
- [\#1253](https://github.com/cosmos/evm/pull/1253) Guard StateDB `SubBalance` against underflow and parse precompile balance-change events using both base and extended denoms.
- Make StateDB `Commit()` apply keeper writes through a cache context so a late failure cannot leave a partial write.

## v0.6.0

Follow the [migration document](docs/migrations/v0.5.x_to_v0.6.0.md) for upgrade instructions.
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ src="repo_header.svg"
alt="Cosmos EVM - A plug-and-play solution that adds EVM compatibility and customizability to your chain"
/>

## What is Cosmos EVM?

Cosmos EVM is a plug-and-play solution that adds EVM compatibility and customizability to your Cosmos SDK chain. Cosmos EVM is used by Ondo, Mezo, Mantra, XRP sidechain, Telegram Application Chain (TAC), Stable, and others. Cosmos EVM equips Cosmos chains with complete Ethereum capabilities: Solidity smart contracts, Ethereum JSON-RPC, native support for the EVM wallet/token/user experience, and access to the entire Ethereum developer ecosystem. Its precompiles and extensions allow developers to leverage modules like [IBC](https://github.com/cosmos/ibc-go) with EVM and get native ERC-20 support for tokens on Cosmos.

Cosmos EVM is customizable for your business use case, chain architecture, and performance needs.

**Please note**: This repo is undergoing changes while the code is being audited and tested. For the time being we will
be making v0.x releases. Some breaking changes might occur. Cosmos Labs will only mark the Cosmos EVM repository as stable with a v1
release after the audit, key stability features and benchmarking are completed.
Expand Down
35 changes: 22 additions & 13 deletions evmd/tests/ibc/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,22 @@ func SetupNativeErc20(t *testing.T, chain *evmibctesting.TestChain, senderAcc ev
evmCtx := chain.GetContext()
evmApp := chain.App.(evm.EvmApp)

ak := evmApp.GetAccountKeeper()
deployerAccAddr := sdk.AccAddress(erc20TestDeployer.Bytes())
if ak.GetAccount(evmCtx, deployerAccAddr) == nil {
ak.SetAccount(evmCtx, ak.NewAccountWithAddress(evmCtx, deployerAccAddr))
// Deploy new ERC20 contract with default metadata.
// The deployer is a dedicated EOA.
deployer := common.BytesToAddress([]byte("erc20-test-deployer"))
deployerAccAddr := sdk.AccAddress(deployer.Bytes())
if evmApp.GetAccountKeeper().GetAccount(evmCtx, deployerAccAddr) == nil {
evmApp.GetAccountKeeper().SetAccount(evmCtx, evmApp.GetAccountKeeper().NewAccountWithAddress(evmCtx, deployerAccAddr))
}

stateDB := statedb.New(chain.GetContext(), chain.App.(evm.EvmApp).GetEVMKeeper(), statedb.NewEmptyTxConfig())
stateDB := statedb.New(evmCtx, evmApp.GetEVMKeeper(), statedb.NewEmptyTxConfig())
contractAddr, err := DeployERC20Contract(evmCtx, stateDB, evmApp.GetAccountKeeper(), evmApp.GetEVMKeeper(), banktypes.Metadata{
DenomUnits: []*banktypes.DenomUnit{
{Denom: "example", Exponent: 18},
},
Name: "Example",
Symbol: "Ex",
})
}, deployer)
if err != nil {
t.Fatalf("ERC20 deployment failed: %v", err)
}
Expand All @@ -93,7 +95,7 @@ func SetupNativeErc20(t *testing.T, chain *evmibctesting.TestChain, senderAcc ev
evmCtx,
stateDB,
contractAbi,
erc20TestDeployer,
deployer,
contractAddr,
true,
false,
Expand Down Expand Up @@ -121,7 +123,8 @@ func SetupNativeErc20(t *testing.T, chain *evmibctesting.TestChain, senderAcc ev
}
}

// SetupNativeErc20 deploys, registers, and mints a native ERC20 token on an EVM-based chain.
// DeployContract deploys an arbitrary contract on an EVM-based chain.
// Like DeployERC20Contract, the sender is an EOA rather than a module account.
func DeployContract(t *testing.T, chain *evmibctesting.TestChain, deploymentData testutiltypes.ContractDeploymentData) (common.Address, error) {
t.Helper()

Expand Down Expand Up @@ -150,14 +153,20 @@ func DeployContract(t *testing.T, chain *evmibctesting.TestChain, deploymentData
return crypto.CreateAddress(from, account.Nonce), nil
}

var erc20TestDeployer = common.HexToAddress("0x000000000000000000000000000000000000beef")

// DeployERC20Contract creates and deploys an ERC20 contract on the EVM with
// deployer as owner.
//
// deployer must be an EOA. Do not pass a module account.
// It is also required in practice. Contract creation bumps the sender's nonce,
// SetAccount persists nonce and balance together, and the EVM commit path is
// not allowed to write a module account's balance.
func DeployERC20Contract(
ctx sdk.Context,
stateDB *statedb.StateDB,
accountKeeper erc20types.AccountKeeper,
evmKeeper erc20types.EVMKeeper,
coinMetadata banktypes.Metadata,
deployer common.Address,
) (common.Address, error) {
decimals := uint8(0)
if len(coinMetadata.DenomUnits) > 0 {
Expand All @@ -178,13 +187,13 @@ func DeployERC20Contract(
copy(data[:len(contracts.ERC20MinterBurnerDecimalsContract.Bin)], contracts.ERC20MinterBurnerDecimalsContract.Bin)
copy(data[len(contracts.ERC20MinterBurnerDecimalsContract.Bin):], ctorArgs)

nonce, err := accountKeeper.GetSequence(ctx, erc20TestDeployer.Bytes())
nonce, err := accountKeeper.GetSequence(ctx, deployer.Bytes())
if err != nil {
return common.Address{}, err
}

contractAddr := crypto.CreateAddress(erc20TestDeployer, nonce)
_, err = evmKeeper.CallEVMWithData(ctx, stateDB, erc20TestDeployer, nil, data, true, false, nil)
contractAddr := crypto.CreateAddress(deployer, nonce)
_, err = evmKeeper.CallEVMWithData(ctx, stateDB, deployer, nil, data, true, false, nil)
if err != nil {
return common.Address{}, errorsmod.Wrapf(err, "failed to deploy contract for %s", coinMetadata.Name)
}
Expand Down
1 change: 1 addition & 0 deletions mempool/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,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
18 changes: 18 additions & 0 deletions mempool/mocks/VMKeeper.go

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

55 changes: 50 additions & 5 deletions precompiles/common/balance_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,27 +99,70 @@ func TestParseAddress(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 @@ -129,7 +172,9 @@ 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 := cmn.ParseAmount(tc.maleate())
if tc.expError {
Expand All @@ -138,7 +183,7 @@ func TestParseAmount(t *testing.T) {
}

require.NoError(t, err)
require.True(t, amt.Eq(tc.expAmt))
require.Equal(t, tc.expAmt.String(), amt.String())
})
}
}
Expand Down
10 changes: 8 additions & 2 deletions precompiles/common/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,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
35 changes: 25 additions & 10 deletions tests/integration/x/vm/test_call_evm.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"github.com/cosmos/evm/x/erc20/types"
"github.com/cosmos/evm/x/vm/statedb"
evmtypes "github.com/cosmos/evm/x/vm/types"

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

func (s *KeeperTestSuite) TestCallEVM() {
Expand Down Expand Up @@ -73,10 +75,25 @@ func (s *KeeperTestSuite) TestCallEVM() {
func (s *KeeperTestSuite) TestCallEVMWithData() {
erc20 := contracts.ERC20MinterBurnerDecimalsContract.ABI
wcosmosEVMContract := common.HexToAddress(testconstants.WEVMOSContractMainnet)

// Deployments run from a dedicated EOA rather than a module account.
// It also fails in practice: contract creation bumps the sender's nonce,
// SetAccount writes nonce and balance together, and the EVM commit path may
// not write a module account's balance.
deployer := common.BytesToAddress([]byte("vm-test-deployer"))
// The sender's sequence is read before the message runs, so the account has to exist.
ensureDeployer := func() {
ctx := s.Network.GetContext()
ak := s.Network.App.GetAccountKeeper()
accAddr := sdk.AccAddress(deployer.Bytes())
if ak.GetAccount(ctx, accAddr) == nil {
ak.SetAccount(ctx, ak.NewAccountWithAddress(ctx, accAddr))
}
}

testCases := []struct {
name string
from common.Address
fromFn func() common.Address
malleate func() []byte
deploy bool
useNilDB bool
Expand Down Expand Up @@ -146,10 +163,9 @@ func (s *KeeperTestSuite) TestCallEVMWithData() {
},
{
name: "deploy",
fromFn: func() common.Address {
return s.Keyring.GetAddr(0)
},
from: deployer,
malleate: func() []byte {
ensureDeployer()
ctorArgs, _ := contracts.ERC20MinterBurnerDecimalsContract.ABI.Pack("", "test", "test", uint8(18))
data := append(contracts.ERC20MinterBurnerDecimalsContract.Bin, ctorArgs...) //nolint:gocritic
return data
Expand All @@ -161,8 +177,9 @@ func (s *KeeperTestSuite) TestCallEVMWithData() {
},
{
name: "fail deploy",
from: types.ModuleAddress,
from: deployer,
malleate: func() []byte {
ensureDeployer()
params := s.Network.App.GetEVMKeeper().GetParams(s.Network.GetContext())
params.AccessControl.Create = evmtypes.AccessControlType{
AccessType: evmtypes.AccessTypeRestricted,
Expand All @@ -175,12 +192,13 @@ func (s *KeeperTestSuite) TestCallEVMWithData() {
deploy: true,
useNilDB: false,
expPass: false,
expError: "",
expError: "does not have permission to deploy contracts",
},
{
name: "fail deploy with nil statedb",
from: types.ModuleAddress,
from: deployer,
malleate: func() []byte {
ensureDeployer()
ctorArgs, _ := contracts.ERC20MinterBurnerDecimalsContract.ABI.Pack("", "test", "test", uint8(18))
data := append(contracts.ERC20MinterBurnerDecimalsContract.Bin, ctorArgs...) //nolint:gocritic
return data
Expand All @@ -206,9 +224,6 @@ func (s *KeeperTestSuite) TestCallEVMWithData() {
}

from := tc.from
if tc.fromFn != nil {
from = tc.fromFn()
}

if tc.deploy {
res, err = s.Network.App.GetEVMKeeper().CallEVMWithData(s.Network.GetContext(), stateDB, from, nil, data, true, false, nil)
Expand Down
Loading
Loading