package e2e_test import ( "context" "fmt" "math/big" "time" sdkerrors "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" txtypes "github.com/cosmos/cosmos-sdk/types/tx" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/0glabs/0g-chain/chaincfg" "github.com/0glabs/0g-chain/tests/e2e/testutil" "github.com/0glabs/0g-chain/tests/util" evmutiltypes "github.com/0glabs/0g-chain/x/evmutil/types" ) const initialCosmosCoinConversionDenomFunds = int64(1e4) func setupConvertToCoinTest( suite *IntegrationTestSuite, accountName string, ) (denom string, initialFunds sdk.Coins, user *testutil.SigningAccount) { // we expect a denom to be registered to the allowed denoms param // and for the funded account to have a balance for that denom params, err := suite.ZgChain.Grpc.Query.Evmutil.Params( context.Background(), &evmutiltypes.QueryParamsRequest{}, ) suite.NoError(err) suite.GreaterOrEqual( len(params.Params.AllowedCosmosDenoms), 1, "0g-chain expected to have at least one AllowedCosmosDenom for ERC20 conversion", ) tokenInfo := params.Params.AllowedCosmosDenoms[0] denom = tokenInfo.CosmosDenom initialFunds = sdk.NewCoins( sdk.NewInt64Coin(suite.ZgChain.StakingDenom, 1e5), // gas money sdk.NewInt64Coin(denom, initialCosmosCoinConversionDenomFunds), // conversion-enabled cosmos coin ) user = suite.ZgChain.NewFundedAccount(accountName, initialFunds) return denom, initialFunds, user } // amount must be less than initial funds (initialCosmosCoinConversionDenomFunds) func (suite *IntegrationTestSuite) setupAccountWithCosmosCoinERC20Balance( accountName string, amount int64, ) (user *testutil.SigningAccount, contractAddress *evmutiltypes.InternalEVMAddress, denom string, sdkBalance sdk.Coins) { if amount > initialCosmosCoinConversionDenomFunds { panic(fmt.Sprintf("test erc20 amount must be less than %d", initialCosmosCoinConversionDenomFunds)) } denom, sdkBalance, user = setupConvertToCoinTest(suite, accountName) convertAmount := sdk.NewInt64Coin(denom, amount) // setup user to have erc20 balance msg := evmutiltypes.NewMsgConvertCosmosCoinToERC20( user.SdkAddress.String(), user.EvmAddress.Hex(), convertAmount, ) tx := util.ZgChainMsgRequest{ Msgs: []sdk.Msg{&msg}, GasLimit: 4e5, FeeAmount: sdk.NewCoins(chaincfg.MakeCoinForGasDenom(400)), Data: "converting sdk coin to erc20", } res := user.SignAndBroadcastZgChainTx(tx) suite.NoError(res.Err) // adjust sdk balance sdkBalance = sdkBalance.Sub(convertAmount) // query for the deployed contract deployedContracts, err := suite.ZgChain.Grpc.Query.Evmutil.DeployedCosmosCoinContracts( context.Background(), &evmutiltypes.QueryDeployedCosmosCoinContractsRequest{CosmosDenoms: []string{denom}}, ) suite.NoError(err) suite.Len(deployedContracts.DeployedCosmosCoinContracts, 1) contractAddress = deployedContracts.DeployedCosmosCoinContracts[0].Address return user, contractAddress, denom, sdkBalance } func (suite *IntegrationTestSuite) TestConvertCosmosCoinsToFromERC20() { denom, initialFunds, user := setupConvertToCoinTest(suite, "cosmo-coin-converter") convertAmount := int64(5e3) initialModuleBalance := suite.ZgChain.GetModuleBalances(evmutiltypes.ModuleName).AmountOf(denom) /////////////////////////////// // CONVERT COSMOS COIN -> ERC20 /////////////////////////////// convertToErc20Msg := evmutiltypes.NewMsgConvertCosmosCoinToERC20( user.SdkAddress.String(), user.EvmAddress.Hex(), sdk.NewInt64Coin(denom, convertAmount), ) tx := util.ZgChainMsgRequest{ Msgs: []sdk.Msg{&convertToErc20Msg}, GasLimit: 2e6, FeeAmount: sdk.NewCoins(chaincfg.MakeCoinForGasDenom(2000)), Data: "converting sdk coin to erc20", } res := user.SignAndBroadcastZgChainTx(tx) suite.NoError(res.Err) // query for the deployed contract deployedContracts, err := suite.ZgChain.Grpc.Query.Evmutil.DeployedCosmosCoinContracts( context.Background(), &evmutiltypes.QueryDeployedCosmosCoinContractsRequest{CosmosDenoms: []string{denom}}, ) suite.NoError(err) suite.Len(deployedContracts.DeployedCosmosCoinContracts, 1) contractAddress := deployedContracts.DeployedCosmosCoinContracts[0].Address // check erc20 balance erc20Balance := suite.ZgChain.GetErc20Balance(contractAddress.Address, user.EvmAddress) suite.BigIntsEqual(big.NewInt(convertAmount), erc20Balance, "unexpected erc20 balance post-convert") // check cosmos coin is deducted from account expectedFunds := initialFunds.AmountOf(denom).SubRaw(convertAmount) balance := suite.ZgChain.QuerySdkForBalances(user.SdkAddress).AmountOf(denom) suite.Equal(expectedFunds, balance) // check that module account has sdk coins expectedModuleBalance := initialModuleBalance.AddRaw(convertAmount) actualModuleBalance := suite.ZgChain.GetModuleBalances(evmutiltypes.ModuleName).AmountOf(denom) suite.Equal(expectedModuleBalance, actualModuleBalance) /////////////////////////////// // CONVERT ERC20 -> COSMOS COIN /////////////////////////////// convertFromErc20Msg := evmutiltypes.NewMsgConvertCosmosCoinFromERC20( user.EvmAddress.Hex(), user.SdkAddress.String(), sdk.NewInt64Coin(denom, convertAmount), ) tx = util.ZgChainMsgRequest{ Msgs: []sdk.Msg{&convertFromErc20Msg}, GasLimit: 2e5, FeeAmount: sdk.NewCoins(chaincfg.MakeCoinForGasDenom(200)), Data: "converting erc20 to cosmos coin", } res = user.SignAndBroadcastZgChainTx(tx) suite.NoError(res.Err) // check erc20 balance erc20Balance = suite.ZgChain.GetErc20Balance(contractAddress.Address, user.EvmAddress) suite.BigIntsEqual(big.NewInt(0), erc20Balance, "expected all erc20 to be converted back") // check cosmos coin is added back to account expectedFunds = initialFunds.AmountOf(denom) balance = suite.ZgChain.QuerySdkForBalances(user.SdkAddress).AmountOf(denom) suite.Equal(expectedFunds, balance) // check that module account has sdk coins deducted actualModuleBalance = suite.ZgChain.GetModuleBalances(evmutiltypes.ModuleName).AmountOf(denom) suite.Equal(initialModuleBalance, actualModuleBalance) } // like the above, but confirming we can sign the messages with eip712 func (suite *IntegrationTestSuite) TestEIP712ConvertCosmosCoinsToFromERC20() { denom, initialFunds, user := setupConvertToCoinTest(suite, "cosmo-coin-converter-eip712") convertAmount := int64(5e3) initialModuleBalance := suite.ZgChain.GetModuleBalances(evmutiltypes.ModuleName).AmountOf(denom) /////////////////////////////// // CONVERT COSMOS COIN -> ERC20 /////////////////////////////// convertToErc20Msg := evmutiltypes.NewMsgConvertCosmosCoinToERC20( user.SdkAddress.String(), user.EvmAddress.Hex(), sdk.NewInt64Coin(denom, convertAmount), ) tx := suite.NewEip712TxBuilder( user, suite.ZgChain, 2e6, sdk.NewCoins(chaincfg.MakeCoinForGasDenom(1e4)), []sdk.Msg{&convertToErc20Msg}, "this is a memo", ).GetTx() txBytes, err := suite.ZgChain.EncodingConfig.TxConfig.TxEncoder()(tx) suite.NoError(err) // submit the eip712 message to the chain. res, err := suite.ZgChain.Grpc.Query.Tx.BroadcastTx(context.Background(), &txtypes.BroadcastTxRequest{ TxBytes: txBytes, Mode: txtypes.BroadcastMode_BROADCAST_MODE_SYNC, }) suite.NoError(err) suite.Equal(sdkerrors.SuccessABCICode, res.TxResponse.Code) _, err = util.WaitForSdkTxCommit(suite.ZgChain.Grpc.Query.Tx, res.TxResponse.TxHash, 12*time.Second) suite.Require().NoError(err) // query for the deployed contract deployedContracts, err := suite.ZgChain.Grpc.Query.Evmutil.DeployedCosmosCoinContracts( context.Background(), &evmutiltypes.QueryDeployedCosmosCoinContractsRequest{CosmosDenoms: []string{denom}}, ) suite.NoError(err) suite.Len(deployedContracts.DeployedCosmosCoinContracts, 1) contractAddress := deployedContracts.DeployedCosmosCoinContracts[0].Address // check erc20 balance erc20Balance := suite.ZgChain.GetErc20Balance(contractAddress.Address, user.EvmAddress) suite.BigIntsEqual(big.NewInt(convertAmount), erc20Balance, "unexpected erc20 balance post-convert") // check cosmos coin is deducted from account expectedFunds := initialFunds.AmountOf(denom).SubRaw(convertAmount) balance := suite.ZgChain.QuerySdkForBalances(user.SdkAddress).AmountOf(denom) suite.Equal(expectedFunds, balance) // check that module account has sdk coins expectedModuleBalance := initialModuleBalance.AddRaw(convertAmount) actualModuleBalance := suite.ZgChain.GetModuleBalances(evmutiltypes.ModuleName).AmountOf(denom) suite.Equal(expectedModuleBalance, actualModuleBalance) /////////////////////////////// // CONVERT ERC20 -> COSMOS COIN /////////////////////////////// convertFromErc20Msg := evmutiltypes.NewMsgConvertCosmosCoinFromERC20( user.EvmAddress.Hex(), user.SdkAddress.String(), sdk.NewInt64Coin(denom, convertAmount), ) tx = suite.NewEip712TxBuilder( user, suite.ZgChain, 2e5, sdk.NewCoins(chaincfg.MakeCoinForGasDenom(200)), []sdk.Msg{&convertFromErc20Msg}, "", ).GetTx() txBytes, err = suite.ZgChain.EncodingConfig.TxConfig.TxEncoder()(tx) suite.NoError(err) // submit the eip712 message to the chain res, err = suite.ZgChain.Grpc.Query.Tx.BroadcastTx(context.Background(), &txtypes.BroadcastTxRequest{ TxBytes: txBytes, Mode: txtypes.BroadcastMode_BROADCAST_MODE_SYNC, }) suite.NoError(err) suite.Equal(sdkerrors.SuccessABCICode, res.TxResponse.Code) _, err = util.WaitForSdkTxCommit(suite.ZgChain.Grpc.Query.Tx, res.TxResponse.TxHash, 6*time.Second) suite.NoError(err) // check erc20 balance erc20Balance = suite.ZgChain.GetErc20Balance(contractAddress.Address, user.EvmAddress) suite.BigIntsEqual(big.NewInt(0), erc20Balance, "expected all erc20 to be converted back") // check cosmos coin is added back to account expectedFunds = initialFunds.AmountOf(denom) balance = suite.ZgChain.QuerySdkForBalances(user.SdkAddress).AmountOf(denom) suite.Equal(expectedFunds, balance) // check that module account has sdk coins deducted actualModuleBalance = suite.ZgChain.GetModuleBalances(evmutiltypes.ModuleName).AmountOf(denom) suite.Equal(initialModuleBalance, actualModuleBalance) } func (suite *IntegrationTestSuite) TestConvertCosmosCoins_ForbiddenERC20Calls() { // give them erc20 balance so we know that's not preventing these method calls // this test sacrifices the 1 coin by never returning it to the sdk. user, contractAddress, _, _ := suite.setupAccountWithCosmosCoinERC20Balance("cosmo-coin-converter-unhappy", 1) suite.Run("users can't mint()", func() { data := util.BuildErc20MintCallData(user.EvmAddress, big.NewInt(1)) nonce, err := user.NextNonce() suite.NoError(err) mintTx := util.EvmTxRequest{ Tx: ethtypes.NewTx( ðtypes.LegacyTx{ Nonce: nonce, GasPrice: minEvmGasPrice, Gas: 1e5, To: &contractAddress.Address, Data: data, }, ), Data: "attempting to mint, should fail", } res := user.SignAndBroadcastEvmTx(mintTx) suite.ErrorAs(res.Err, &util.ErrEvmTxFailed) // TODO: when traceTransactions enabled, read actual error log suite.ErrorContains(res.Err, "transaction was committed but failed. likely an execution revert by contract code") }) suite.Run("users can't burn()", func() { data := util.BuildErc20BurnCallData(user.EvmAddress, big.NewInt(1)) nonce, err := user.NextNonce() suite.NoError(err) burnTx := util.EvmTxRequest{ Tx: ethtypes.NewTx( ðtypes.LegacyTx{ Nonce: nonce, GasPrice: minEvmGasPrice, Gas: 1e5, To: &contractAddress.Address, Data: data, }, ), Data: "attempting to burn, should fail", } res := user.SignAndBroadcastEvmTx(burnTx) suite.ErrorAs(res.Err, &util.ErrEvmTxFailed) // TODO: when traceTransactions enabled, read actual error log suite.ErrorContains(res.Err, "transaction was committed but failed. likely an execution revert by contract code") }) } // - check approval flow of erc20. alice approves bob to move their funds // - check complex conversion flow. bob converts funds they receive on evm back to sdk.Coin func (suite *IntegrationTestSuite) TestConvertCosmosCoins_ERC20Magic() { initialAliceAmount := int64(2e3) alice, contractAddress, denom, _ := suite.setupAccountWithCosmosCoinERC20Balance( "cosmo-coin-converter-complex-alice", initialAliceAmount, ) gasMoney := sdk.NewCoins(chaincfg.MakeCoinForGasDenom(1e5)) bob := suite.ZgChain.NewFundedAccount("cosmo-coin-converter-complex-bob", gasMoney) amount := big.NewInt(1e3) // test assumes this is half of alice's balance. // bob can't move alice's funds nonce, err := bob.NextNonce() suite.NoError(err) transferFromTxData := ðtypes.LegacyTx{ Nonce: nonce, GasPrice: minEvmGasPrice, Gas: 1e5, To: &contractAddress.Address, Value: &big.Int{}, Data: util.BuildErc20TransferFromCallData(alice.EvmAddress, bob.EvmAddress, amount), } transferTx := util.EvmTxRequest{ Tx: ethtypes.NewTx(transferFromTxData), Data: "bob can't move alice's funds, should fail", } res := bob.SignAndBroadcastEvmTx(transferTx) suite.ErrorAs(res.Err, &util.ErrEvmTxFailed) suite.ErrorContains(res.Err, "transaction was committed but failed. likely an execution revert by contract code") // approve bob to move alice's funds nonce, err = alice.NextNonce() suite.NoError(err) approveTx := util.EvmTxRequest{ Tx: ethtypes.NewTx(ðtypes.LegacyTx{ Nonce: nonce, GasPrice: minEvmGasPrice, Gas: 1e5, To: &contractAddress.Address, Value: &big.Int{}, Data: util.BuildErc20ApproveCallData(bob.EvmAddress, amount), }), Data: "alice approves bob to spend amount", } res = alice.SignAndBroadcastEvmTx(approveTx) suite.NoError(res.Err) // bob can't move more than alice allowed transferFromTxData.Data = util.BuildErc20TransferFromCallData( alice.EvmAddress, bob.EvmAddress, new(big.Int).Add(amount, big.NewInt(1)), ) transferFromTxData.Nonce, err = bob.NextNonce() suite.NoError(err) transferTooMuchTx := util.EvmTxRequest{ Tx: ethtypes.NewTx(transferFromTxData), Data: "transferring more than approved, should fail", } res = bob.SignAndBroadcastEvmTx(transferTooMuchTx) suite.ErrorAs(res.Err, &util.ErrEvmTxFailed) suite.ErrorContains(res.Err, "transaction was committed but failed. likely an execution revert by contract code") // bob can move allowed amount transferFromTxData.Data = util.BuildErc20TransferFromCallData(alice.EvmAddress, bob.EvmAddress, amount) transferFromTxData.Nonce, err = bob.NextNonce() suite.NoError(err) transferJustRightTx := util.EvmTxRequest{ Tx: ethtypes.NewTx(transferFromTxData), Data: "bob transfers alice's funds, allowed because he's approved", } res = bob.SignAndBroadcastEvmTx(transferJustRightTx) suite.Require().NoError(res.Err) // alice should have amount deducted erc20Balance := suite.ZgChain.GetErc20Balance(contractAddress.Address, alice.EvmAddress) suite.BigIntsEqual(big.NewInt(initialAliceAmount-amount.Int64()), erc20Balance, "alice has unexpected erc20 balance") // bob should have amount added erc20Balance = suite.ZgChain.GetErc20Balance(contractAddress.Address, bob.EvmAddress) suite.BigIntsEqual(amount, erc20Balance, "bob has unexpected erc20 balance") // convert bob's new funds back to an sdk.Coin convertMsg := evmutiltypes.NewMsgConvertCosmosCoinFromERC20( bob.EvmAddress.Hex(), bob.SdkAddress.String(), sdk.NewInt64Coin(denom, amount.Int64()), ) convertTx := util.ZgChainMsgRequest{ Msgs: []sdk.Msg{&convertMsg}, GasLimit: 2e5, FeeAmount: sdk.NewCoins(chaincfg.MakeCoinForGasDenom(200)), Data: "bob converts his new erc20 to an sdk.Coin", } convertRes := bob.SignAndBroadcastZgChainTx(convertTx) suite.NoError(convertRes.Err) // bob should have no more erc20 balance erc20Balance = suite.ZgChain.GetErc20Balance(contractAddress.Address, bob.EvmAddress) suite.BigIntsEqual(big.NewInt(0), erc20Balance, "expected no erc20 balance for bob") // bob should have sdk balance balance := suite.ZgChain.QuerySdkForBalances(bob.SdkAddress).AmountOf(denom) suite.Equal(sdk.NewIntFromBigInt(amount), balance) // alice should have the remaining balance erc20Balance = suite.ZgChain.GetErc20Balance(contractAddress.Address, alice.EvmAddress) suite.BigIntsEqual(amount, erc20Balance, "expected alice to have half initial funds remaining") // convert alice's remaining balance back to sdk coins convertMsg = evmutiltypes.NewMsgConvertCosmosCoinFromERC20( alice.EvmAddress.Hex(), alice.SdkAddress.String(), sdk.NewInt64Coin(denom, amount.Int64()), ) convertRes = alice.SignAndBroadcastZgChainTx(convertTx) suite.NoError(convertRes.Err) }