diff --git a/Dockerfile-localnet b/Dockerfile-localnet index 6be3ba3e03..cfe5e19a48 100644 --- a/Dockerfile-localnet +++ b/Dockerfile-localnet @@ -59,7 +59,7 @@ EXPOSE 22 FROM base-runtime AS latest-runtime COPY --from=cosmovisor-build /go/bin/cosmovisor /usr/local/bin -COPY --from=latest-build /go/bin/zetacored /go/bin/zetaclientd /go/bin/zetaclientd-supervisor /go/bin/zetae2e /usr/local/bin +COPY --from=latest-build /go/bin/zetacored /go/bin/zetaclientd /go/bin/zetaclientd-supervisor /go/bin/zetae2e /usr/local/bin/ # Optional old version build (from source). This old build is used as the genesis version in the upgrade tests. # Use --target latest-runtime to skip. @@ -75,7 +75,7 @@ RUN cd node && make install FROM base-runtime AS old-runtime-source COPY --from=cosmovisor-build /go/bin/cosmovisor /usr/local/bin -COPY --from=old-build-source /go/bin/zetacored /go/bin/zetaclientd /usr/local/bin +COPY --from=old-build-source /go/bin/zetacored /go/bin/zetaclientd /usr/local/bin/ COPY --from=latest-build /go/bin/zetaclientd-supervisor /usr/local/bin # Optional old version build (from binary). diff --git a/app/app.go b/app/app.go index 7c4186af5c..cc271fc35f 100644 --- a/app/app.go +++ b/app/app.go @@ -570,6 +570,7 @@ func New( precompiles.StatefulContracts( &app.FungibleKeeper, app.StakingKeeper, + app.BankKeeper, appCodec, storetypes.TransientGasConfig(), ), diff --git a/changelog.md b/changelog.md index 9d68531972..e5e305d748 100644 --- a/changelog.md +++ b/changelog.md @@ -9,6 +9,7 @@ * [2784](https://github.com/zeta-chain/node/pull/2784) - staking precompiled contract * [2795](https://github.com/zeta-chain/node/pull/2795) - support restricted address in Solana * [2861](https://github.com/zeta-chain/node/pull/2861) - emit events from staking precompile +* [2860](https://github.com/zeta-chain/node/pull/2860) - bank precompiled contract * [2870](https://github.com/zeta-chain/node/pull/2870) - support for multiple Bitcoin chains in the zetaclient * [2883](https://github.com/zeta-chain/node/pull/2883) - add chain static information for btc signet testnet diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index d84a4ba9a4..33d82d1ae8 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -322,6 +322,8 @@ func localE2ETest(cmd *cobra.Command, _ []string) { e2etests.TestPrecompilesPrototypeThroughContractName, e2etests.TestPrecompilesStakingName, e2etests.TestPrecompilesStakingThroughContractName, + e2etests.TestPrecompilesBankName, + e2etests.TestPrecompilesBankFailName, } } diff --git a/cmd/zetae2e/local/precompiles.go b/cmd/zetae2e/local/precompiles.go index 400db0d973..46668c9f44 100644 --- a/cmd/zetae2e/local/precompiles.go +++ b/cmd/zetae2e/local/precompiles.go @@ -35,6 +35,10 @@ func statefulPrecompilesTestRoutine( precompileRunner.Logger.Print("🏃 starting stateful precompiled contracts tests") startTime := time.Now() + // Send ERC20 that will be depositted into ERC20ZRC20 tokens. + txERC20Send := deployerRunner.SendERC20OnEvm(account.EVMAddress(), 10000) + precompileRunner.WaitForTxReceiptOnEvm(txERC20Send) + testsToRun, err := precompileRunner.GetE2ETestsToRunByName( e2etests.AllE2ETests, testNames..., diff --git a/codecov.yml b/codecov.yml index fedb830848..fee85c9c04 100644 --- a/codecov.yml +++ b/codecov.yml @@ -80,5 +80,4 @@ ignore: - "precompiles/**/*.abi" - "precompiles/**/*.json" - "precompiles/**/*.sol" - - "precompiles/prototype/IPrototype.go" - - "precompiles/staking/IStaking.go" + - "precompiles/**/*.gen.go" diff --git a/contrib/localnet/orchestrator/start-zetae2e.sh b/contrib/localnet/orchestrator/start-zetae2e.sh index 9e72cc15d9..6d98df3699 100644 --- a/contrib/localnet/orchestrator/start-zetae2e.sh +++ b/contrib/localnet/orchestrator/start-zetae2e.sh @@ -117,7 +117,7 @@ fund_eth_from_config '.additional_accounts.user_admin.evm_address' 10000 "admin fund_eth_from_config '.additional_accounts.user_migration.evm_address' 10000 "migration tester" # unlock precompile tests accounts -fund_eth_from_config '.additional_accounts.user_precompile.evm_address' 10000 "precompile tester" +fund_eth_from_config '.additional_accounts.user_precompile.evm_address' 10000 "precompiles tester" # unlock v2 ethers tests accounts fund_eth_from_config '.additional_accounts.user_v2_ether.evm_address' 10000 "V2 ethers tester" @@ -131,16 +131,6 @@ fund_eth_from_config '.additional_accounts.user_v2_ether_revert.evm_address' 100 # unlock v2 erc20 revert tests accounts fund_eth_from_config '.additional_accounts.user_v2_erc20_revert.evm_address' 10000 "V2 ERC20 revert tester" -# unlock precompile tests accounts -address=$(yq -r '.additional_accounts.user_precompile.evm_address' config.yml) -echo "funding precompile tester address ${address} with 10000 Ether" -geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 > /dev/null - -# unlock precompile tests accounts -address=$(yq -r '.additional_accounts.user_precompile.evm_address' config.yml) -echo "funding precompile tester address ${address} with 10000 Ether" -geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 > /dev/null - # unlock local solana relayer accounts if host solana > /dev/null; then solana_url=$(config_str '.rpcs.solana') diff --git a/e2e/e2etests/e2etests.go b/e2e/e2etests/e2etests.go index dd9409e4cf..510f307f47 100644 --- a/e2e/e2etests/e2etests.go +++ b/e2e/e2etests/e2etests.go @@ -160,6 +160,9 @@ const ( TestPrecompilesPrototypeThroughContractName = "precompile_contracts_prototype_through_contract" TestPrecompilesStakingName = "precompile_contracts_staking" TestPrecompilesStakingThroughContractName = "precompile_contracts_staking_through_contract" + TestPrecompilesBankName = "precompile_contracts_bank" + TestPrecompilesBankFailName = "precompile_contracts_bank_fail" + TestPrecompilesBankThroughContractName = "precompile_contracts_bank_through_contract" ) // AllE2ETests is an ordered list of all e2e tests @@ -886,4 +889,16 @@ var AllE2ETests = []runner.E2ETest{ []runner.ArgDefinition{}, TestPrecompilesStakingThroughContract, ), + runner.NewE2ETest( + TestPrecompilesBankName, + "test stateful precompiled contracts bank with ZRC20 tokens", + []runner.ArgDefinition{}, + TestPrecompilesBank, + ), + runner.NewE2ETest( + TestPrecompilesBankFailName, + "test stateful precompiled contracts bank with non ZRC20 tokens", + []runner.ArgDefinition{}, + TestPrecompilesBankNonZRC20, + ), } diff --git a/e2e/e2etests/test_precompiles_bank.go b/e2e/e2etests/test_precompiles_bank.go new file mode 100644 index 0000000000..1e78212355 --- /dev/null +++ b/e2e/e2etests/test_precompiles_bank.go @@ -0,0 +1,192 @@ +package e2etests + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/stretchr/testify/require" + + "github.com/zeta-chain/node/e2e/runner" + "github.com/zeta-chain/node/e2e/utils" + "github.com/zeta-chain/node/precompiles/bank" +) + +func TestPrecompilesBank(r *runner.E2ERunner, args []string) { + require.Len(r, args, 0, "No arguments expected") + + // Increase the gasLimit. It's required because of the gas consumed by precompiled functions. + previousGasLimit := r.ZEVMAuth.GasLimit + r.ZEVMAuth.GasLimit = 10_000_000 + defer func() { + r.ZEVMAuth.GasLimit = previousGasLimit + + // Reset the allowance to 0; this is needed when running upgrade tests where + // this test runs twice. + tx, err := r.ERC20ZRC20.Approve(r.ZEVMAuth, bank.ContractAddress, big.NewInt(0)) + require.NoError(r, err) + receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequireTxSuccessful(r, receipt, "Resetting allowance failed") + }() + + totalAmount := big.NewInt(1e3) + depositAmount := big.NewInt(500) + higherBalanceAmount := big.NewInt(1001) + higherAllowanceAmount := big.NewInt(501) + spender := r.EVMAddress() + + // Get ERC20ZRC20. + txHash := r.DepositERC20WithAmountAndMessage(r.EVMAddress(), totalAmount, []byte{}) + utils.WaitCctxMinedByInboundHash(r.Ctx, txHash.Hex(), r.CctxClient, r.Logger, r.CctxTimeout) + + // Create a bank contract caller. + bankContract, err := bank.NewIBank(bank.ContractAddress, r.ZEVMClient) + require.NoError(r, err, "Failed to create bank contract caller") + + // Cosmos coin balance should be 0 at this point. + cosmosBalance, err := bankContract.BalanceOf(&bind.CallOpts{Context: r.Ctx}, r.ERC20ZRC20Addr, spender) + require.NoError(r, err, "Call bank.BalanceOf()") + require.Equal(r, uint64(0), cosmosBalance.Uint64(), "spender cosmos coin balance should be 0") + + // Approve allowance of 500 ERC20ZRC20 tokens for the bank contract. Should pass. + tx, err := r.ERC20ZRC20.Approve(r.ZEVMAuth, bank.ContractAddress, depositAmount) + require.NoError(r, err) + receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequireTxSuccessful(r, receipt, "Approve ETHZRC20 bank allowance tx failed") + + // Deposit 501 ERC20ZRC20 tokens to the bank contract. + // It's higher than allowance but lower than balance, should fail. + tx, err = bankContract.Deposit(r.ZEVMAuth, r.ERC20ZRC20Addr, higherAllowanceAmount) + require.NoError(r, err, "Call bank.Deposit() with amout higher than allowance") + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequiredTxFailed(r, receipt, "Depositting an amount higher than allowed should fail") + + // Approve allowance of 1000 ERC20ZRC20 tokens. + tx, err = r.ERC20ZRC20.Approve(r.ZEVMAuth, bank.ContractAddress, big.NewInt(1e3)) + require.NoError(r, err) + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequireTxSuccessful(r, receipt, "Approve ETHZRC20 bank allowance tx failed") + + // Deposit 1001 ERC20ZRC20 tokens to the bank contract. + // It's higher than spender balance but within approved allowance, should fail. + tx, err = bankContract.Deposit(r.ZEVMAuth, r.ERC20ZRC20Addr, higherBalanceAmount) + require.NoError(r, err, "Call bank.Deposit() with amout higher than balance") + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequiredTxFailed(r, receipt, "Depositting an amount higher than balance should fail") + + // Deposit 500 ERC20ZRC20 tokens to the bank contract. Should pass. + tx, err = bankContract.Deposit(r.ZEVMAuth, r.ERC20ZRC20Addr, depositAmount) + require.NoError(r, err, "Call bank.Deposit() with correct amount") + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequireTxSuccessful(r, receipt, "Depositting a correct amount should pass") + + // Check the deposit event. + eventDeposit, err := bankContract.ParseDeposit(*receipt.Logs[0]) + require.NoError(r, err, "Parse Deposit event") + require.Equal(r, r.EVMAddress(), eventDeposit.Zrc20Depositor, "Deposit event token should be r.EVMAddress()") + require.Equal(r, r.ERC20ZRC20Addr, eventDeposit.Zrc20Token, "Deposit event token should be ERC20ZRC20Addr") + require.Equal(r, depositAmount, eventDeposit.Amount, "Deposit event amount should be 500") + + // Spender: cosmos coin balance should be 500 at this point. + cosmosBalance, err = bankContract.BalanceOf(&bind.CallOpts{Context: r.Ctx}, r.ERC20ZRC20Addr, spender) + require.NoError(r, err, "Call bank.BalanceOf()") + require.Equal(r, uint64(500), cosmosBalance.Uint64(), "spender cosmos coin balance should be 500") + + // Bank: ERC20ZRC20 balance should be 500 tokens locked. + bankZRC20Balance, err := r.ERC20ZRC20.BalanceOf(&bind.CallOpts{Context: r.Ctx}, bank.ContractAddress) + require.NoError(r, err, "Call ERC20ZRC20.BalanceOf") + require.Equal(r, uint64(500), bankZRC20Balance.Uint64(), "bank ERC20ZRC20 balance should be 500") + + // Try to withdraw 501 ERC20ZRC20 tokens. Should fail. + tx, err = bankContract.Withdraw(r.ZEVMAuth, r.ERC20ZRC20Addr, big.NewInt(501)) + require.NoError(r, err, "Error calling bank.withdraw()") + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequiredTxFailed(r, receipt, "Withdrawing more than cosmos coin balance amount should fail") + + // Bank: ERC20ZRC20 balance should be 500 tokens locked after a failed withdraw. + // No tokens should be unlocked with a failed withdraw. + bankZRC20Balance, err = r.ERC20ZRC20.BalanceOf(&bind.CallOpts{Context: r.Ctx}, bank.ContractAddress) + require.NoError(r, err, "Call ERC20ZRC20.BalanceOf") + require.Equal(r, uint64(500), bankZRC20Balance.Uint64(), "bank ERC20ZRC20 balance should be 500") + + // Try to withdraw 500 ERC20ZRC20 tokens. Should pass. + tx, err = bankContract.Withdraw(r.ZEVMAuth, r.ERC20ZRC20Addr, depositAmount) + require.NoError(r, err, "Error calling bank.withdraw()") + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequireTxSuccessful(r, receipt, "Withdraw correct amount should pass") + + // Check the withdraw event. + eventWithdraw, err := bankContract.ParseWithdraw(*receipt.Logs[0]) + require.NoError(r, err, "Parse Deposit event") + require.Equal(r, r.EVMAddress(), eventWithdraw.Zrc20Withdrawer, "Deposit event token should be r.EVMAddress()") + require.Equal(r, r.ERC20ZRC20Addr, eventWithdraw.Zrc20Token, "Deposit event token should be ERC20ZRC20Addr") + require.Equal(r, depositAmount, eventWithdraw.Amount, "Deposit event amount should be 500") + + // Spender: cosmos coin balance should be 0 at this point. + cosmosBalance, err = bankContract.BalanceOf(&bind.CallOpts{Context: r.Ctx}, r.ERC20ZRC20Addr, spender) + require.NoError(r, err, "Call bank.BalanceOf()") + require.Equal(r, uint64(0), cosmosBalance.Uint64(), "spender cosmos coin balance should be 0") + + // Spender: ERC20ZRC20 balance should be 1000 at this point. + zrc20Balance, err := r.ERC20ZRC20.BalanceOf(&bind.CallOpts{Context: r.Ctx}, spender) + require.NoError(r, err, "Call bank.BalanceOf()") + require.Equal(r, uint64(1000), zrc20Balance.Uint64(), "spender ERC20ZRC20 balance should be 1000") + + // Bank: ERC20ZRC20 balance should be 0 tokens locked. + bankZRC20Balance, err = r.ERC20ZRC20.BalanceOf(&bind.CallOpts{Context: r.Ctx}, bank.ContractAddress) + require.NoError(r, err, "Call ERC20ZRC20.BalanceOf") + require.Equal(r, uint64(0), bankZRC20Balance.Uint64(), "bank ERC20ZRC20 balance should be 0") +} + +func TestPrecompilesBankNonZRC20(r *runner.E2ERunner, args []string) { + require.Len(r, args, 0, "No arguments expected") + + // Increase the gasLimit. It's required because of the gas consumed by precompiled functions. + previousGasLimit := r.ZEVMAuth.GasLimit + r.ZEVMAuth.GasLimit = 10_000_000 + defer func() { + r.ZEVMAuth.GasLimit = previousGasLimit + }() + + spender, bankAddr := r.EVMAddress(), bank.ContractAddress + + // Create a bank contract caller. + bankContract, err := bank.NewIBank(bank.ContractAddress, r.ZEVMClient) + require.NoError(r, err, "Failed to create bank contract caller") + + // Deposit and approve 50 WZETA for the test. + approveAmount := big.NewInt(0).Mul(big.NewInt(1e18), big.NewInt(50)) + r.DepositAndApproveWZeta(approveAmount) + + // Non ZRC20 balanceOf check should fail. + _, err = bankContract.BalanceOf(&bind.CallOpts{Context: r.Ctx}, r.WZetaAddr, spender) + require.Error(r, err, "bank.balanceOf() should error out when checking for non ZRC20 balance") + require.Contains( + r, + err.Error(), + "invalid token 0x5F0b1a82749cb4E2278EC87F8BF6B618dC71a8bf: token is not a whitelisted ZRC20", + "Error should be 'token is not a whitelisted ZRC20'", + ) + + // Allow the bank contract to spend 25 WZeta tokens. + tx, err := r.WZeta.Approve(r.ZEVMAuth, bankAddr, big.NewInt(25)) + require.NoError(r, err, "Error approving allowance for bank contract") + receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + require.EqualValues(r, uint64(1), receipt.Status, "approve allowance tx failed") + + // Check the allowance of the bank in WZeta tokens. Should be 25. + allowance, err := r.WZeta.Allowance(&bind.CallOpts{Context: r.Ctx}, spender, bankAddr) + require.NoError(r, err, "Error retrieving bank allowance") + require.EqualValues(r, uint64(25), allowance.Uint64(), "Error allowance for bank contract") + + // Call Deposit with 25 Non ZRC20 tokens. Should fail. + tx, err = bankContract.Deposit(r.ZEVMAuth, r.WZetaAddr, big.NewInt(25)) + require.NoError(r, err, "Error calling bank.deposit()") + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + require.Equal(r, uint64(0), receipt.Status, "Non ZRC20 deposit should fail") + + // Call Withdraw with 25 on ZRC20 tokens. Should fail. + tx, err = bankContract.Withdraw(r.ZEVMAuth, r.WZetaAddr, big.NewInt(25)) + require.NoError(r, err, "Error calling bank.withdraw()") + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + require.Equal(r, uint64(0), receipt.Status, "Non ZRC20 withdraw should fail") +} diff --git a/e2e/e2etests/test_rate_limiter.go b/e2e/e2etests/test_rate_limiter.go index c2e10551aa..e74691e1bd 100644 --- a/e2e/e2etests/test_rate_limiter.go +++ b/e2e/e2etests/test_rate_limiter.go @@ -122,7 +122,7 @@ func createAndWaitWithdraws(r *runner.E2ERunner, withdrawType withdrawType, with return err } - duration := time.Now().Sub(startTime).Seconds() + duration := time.Since(startTime).Seconds() block, err := r.ZEVMClient.BlockNumber(r.Ctx) if err != nil { return fmt.Errorf("error getting block number: %w", err) @@ -155,7 +155,7 @@ func waitForWithdrawMined( } // record the time for completion - duration := time.Now().Sub(startTime).Seconds() + duration := time.Since(startTime).Seconds() block, err := r.ZEVMClient.BlockNumber(ctx) if err != nil { return err diff --git a/e2e/e2etests/test_stress_btc_deposit.go b/e2e/e2etests/test_stress_btc_deposit.go index 25b89088ac..53caea09df 100644 --- a/e2e/e2etests/test_stress_btc_deposit.go +++ b/e2e/e2etests/test_stress_btc_deposit.go @@ -53,7 +53,7 @@ func monitorBTCDeposit(r *runner.E2ERunner, hash *chainhash.Hash, index int, sta cctx.Index, ) } - timeToComplete := time.Now().Sub(startTime) + timeToComplete := time.Since(startTime) r.Logger.Print("index %d: deposit cctx success in %s", index, timeToComplete.String()) return nil diff --git a/e2e/e2etests/test_stress_btc_withdraw.go b/e2e/e2etests/test_stress_btc_withdraw.go index 6e212c97e3..79cf1ae773 100644 --- a/e2e/e2etests/test_stress_btc_withdraw.go +++ b/e2e/e2etests/test_stress_btc_withdraw.go @@ -66,7 +66,7 @@ func monitorBTCWithdraw(r *runner.E2ERunner, tx *ethtypes.Transaction, index int cctx.Index, ) } - timeToComplete := time.Now().Sub(startTime) + timeToComplete := time.Since(startTime) r.Logger.Print("index %d: withdraw cctx success in %s", index, timeToComplete.String()) return nil diff --git a/e2e/e2etests/test_stress_eth_deposit.go b/e2e/e2etests/test_stress_eth_deposit.go index 19676c9cba..29abb76ba0 100644 --- a/e2e/e2etests/test_stress_eth_deposit.go +++ b/e2e/e2etests/test_stress_eth_deposit.go @@ -52,7 +52,7 @@ func monitorEtherDeposit(r *runner.E2ERunner, hash ethcommon.Hash, index int, st cctx.Index, ) } - timeToComplete := time.Now().Sub(startTime) + timeToComplete := time.Since(startTime) r.Logger.Print("index %d: deposit cctx success in %s", index, timeToComplete.String()) return nil diff --git a/e2e/e2etests/test_stress_eth_withdraw.go b/e2e/e2etests/test_stress_eth_withdraw.go index 98fa8e8783..337a4d416d 100644 --- a/e2e/e2etests/test_stress_eth_withdraw.go +++ b/e2e/e2etests/test_stress_eth_withdraw.go @@ -70,7 +70,7 @@ func monitorEtherWithdraw(r *runner.E2ERunner, tx *ethtypes.Transaction, index i cctx.Index, ) } - timeToComplete := time.Now().Sub(startTime) + timeToComplete := time.Since(startTime) r.Logger.Print("index %d: withdraw cctx success in %s", index, timeToComplete.String()) return nil diff --git a/e2e/utils/require.go b/e2e/utils/require.go index b139a2cad5..e610219e15 100644 --- a/e2e/utils/require.go +++ b/e2e/utils/require.go @@ -43,5 +43,9 @@ func errSuffix(msgAndArgs ...any) string { template := "; " + msgAndArgs[0].(string) + if len(msgAndArgs) == 1 { + return template + } + return fmt.Sprintf(template, msgAndArgs[1:]) } diff --git a/precompiles/bank/IBank.abi b/precompiles/bank/IBank.abi new file mode 100644 index 0000000000..9806953a39 --- /dev/null +++ b/precompiles/bank/IBank.abi @@ -0,0 +1,148 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "zrc20_depositor", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "zrc20_token", + "type": "address" + }, + { + "indexed": true, + "internalType": "string", + "name": "cosmos_token", + "type": "string" + }, + { + "indexed": false, + "internalType": "string", + "name": "cosmos_address", + "type": "string" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Deposit", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "zrc20_withdrawer", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "zrc20_token", + "type": "address" + }, + { + "indexed": true, + "internalType": "string", + "name": "cosmos_token", + "type": "string" + }, + { + "indexed": false, + "internalType": "string", + "name": "cosmos_address", + "type": "string" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Withdraw", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "zrc20", + "type": "address" + }, + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "zrc20", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "deposit", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "zrc20", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "withdraw", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/precompiles/bank/IBank.gen.go b/precompiles/bank/IBank.gen.go new file mode 100644 index 0000000000..5064cc8661 --- /dev/null +++ b/precompiles/bank/IBank.gen.go @@ -0,0 +1,582 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package bank + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// IBankMetaData contains all meta data concerning the IBank contract. +var IBankMetaData = &bind.MetaData{ + ABI: "[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"zrc20_depositor\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"zrc20_token\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"string\",\"name\":\"cosmos_token\",\"type\":\"string\"},{\"indexed\":false,\"internalType\":\"string\",\"name\":\"cosmos_address\",\"type\":\"string\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Deposit\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"zrc20_withdrawer\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"zrc20_token\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"string\",\"name\":\"cosmos_token\",\"type\":\"string\"},{\"indexed\":false,\"internalType\":\"string\",\"name\":\"cosmos_address\",\"type\":\"string\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Withdraw\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"zrc20\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"user\",\"type\":\"address\"}],\"name\":\"balanceOf\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"balance\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"zrc20\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"deposit\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"zrc20\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"withdraw\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", +} + +// IBankABI is the input ABI used to generate the binding from. +// Deprecated: Use IBankMetaData.ABI instead. +var IBankABI = IBankMetaData.ABI + +// IBank is an auto generated Go binding around an Ethereum contract. +type IBank struct { + IBankCaller // Read-only binding to the contract + IBankTransactor // Write-only binding to the contract + IBankFilterer // Log filterer for contract events +} + +// IBankCaller is an auto generated read-only Go binding around an Ethereum contract. +type IBankCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// IBankTransactor is an auto generated write-only Go binding around an Ethereum contract. +type IBankTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// IBankFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type IBankFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// IBankSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type IBankSession struct { + Contract *IBank // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// IBankCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type IBankCallerSession struct { + Contract *IBankCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// IBankTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type IBankTransactorSession struct { + Contract *IBankTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// IBankRaw is an auto generated low-level Go binding around an Ethereum contract. +type IBankRaw struct { + Contract *IBank // Generic contract binding to access the raw methods on +} + +// IBankCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type IBankCallerRaw struct { + Contract *IBankCaller // Generic read-only contract binding to access the raw methods on +} + +// IBankTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type IBankTransactorRaw struct { + Contract *IBankTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewIBank creates a new instance of IBank, bound to a specific deployed contract. +func NewIBank(address common.Address, backend bind.ContractBackend) (*IBank, error) { + contract, err := bindIBank(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &IBank{IBankCaller: IBankCaller{contract: contract}, IBankTransactor: IBankTransactor{contract: contract}, IBankFilterer: IBankFilterer{contract: contract}}, nil +} + +// NewIBankCaller creates a new read-only instance of IBank, bound to a specific deployed contract. +func NewIBankCaller(address common.Address, caller bind.ContractCaller) (*IBankCaller, error) { + contract, err := bindIBank(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &IBankCaller{contract: contract}, nil +} + +// NewIBankTransactor creates a new write-only instance of IBank, bound to a specific deployed contract. +func NewIBankTransactor(address common.Address, transactor bind.ContractTransactor) (*IBankTransactor, error) { + contract, err := bindIBank(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &IBankTransactor{contract: contract}, nil +} + +// NewIBankFilterer creates a new log filterer instance of IBank, bound to a specific deployed contract. +func NewIBankFilterer(address common.Address, filterer bind.ContractFilterer) (*IBankFilterer, error) { + contract, err := bindIBank(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &IBankFilterer{contract: contract}, nil +} + +// bindIBank binds a generic wrapper to an already deployed contract. +func bindIBank(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := IBankMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_IBank *IBankRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _IBank.Contract.IBankCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_IBank *IBankRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _IBank.Contract.IBankTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_IBank *IBankRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _IBank.Contract.IBankTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_IBank *IBankCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _IBank.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_IBank *IBankTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _IBank.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_IBank *IBankTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _IBank.Contract.contract.Transact(opts, method, params...) +} + +// BalanceOf is a free data retrieval call binding the contract method 0xf7888aec. +// +// Solidity: function balanceOf(address zrc20, address user) view returns(uint256 balance) +func (_IBank *IBankCaller) BalanceOf(opts *bind.CallOpts, zrc20 common.Address, user common.Address) (*big.Int, error) { + var out []interface{} + err := _IBank.contract.Call(opts, &out, "balanceOf", zrc20, user) + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// BalanceOf is a free data retrieval call binding the contract method 0xf7888aec. +// +// Solidity: function balanceOf(address zrc20, address user) view returns(uint256 balance) +func (_IBank *IBankSession) BalanceOf(zrc20 common.Address, user common.Address) (*big.Int, error) { + return _IBank.Contract.BalanceOf(&_IBank.CallOpts, zrc20, user) +} + +// BalanceOf is a free data retrieval call binding the contract method 0xf7888aec. +// +// Solidity: function balanceOf(address zrc20, address user) view returns(uint256 balance) +func (_IBank *IBankCallerSession) BalanceOf(zrc20 common.Address, user common.Address) (*big.Int, error) { + return _IBank.Contract.BalanceOf(&_IBank.CallOpts, zrc20, user) +} + +// Deposit is a paid mutator transaction binding the contract method 0x47e7ef24. +// +// Solidity: function deposit(address zrc20, uint256 amount) returns(bool success) +func (_IBank *IBankTransactor) Deposit(opts *bind.TransactOpts, zrc20 common.Address, amount *big.Int) (*types.Transaction, error) { + return _IBank.contract.Transact(opts, "deposit", zrc20, amount) +} + +// Deposit is a paid mutator transaction binding the contract method 0x47e7ef24. +// +// Solidity: function deposit(address zrc20, uint256 amount) returns(bool success) +func (_IBank *IBankSession) Deposit(zrc20 common.Address, amount *big.Int) (*types.Transaction, error) { + return _IBank.Contract.Deposit(&_IBank.TransactOpts, zrc20, amount) +} + +// Deposit is a paid mutator transaction binding the contract method 0x47e7ef24. +// +// Solidity: function deposit(address zrc20, uint256 amount) returns(bool success) +func (_IBank *IBankTransactorSession) Deposit(zrc20 common.Address, amount *big.Int) (*types.Transaction, error) { + return _IBank.Contract.Deposit(&_IBank.TransactOpts, zrc20, amount) +} + +// Withdraw is a paid mutator transaction binding the contract method 0xf3fef3a3. +// +// Solidity: function withdraw(address zrc20, uint256 amount) returns(bool success) +func (_IBank *IBankTransactor) Withdraw(opts *bind.TransactOpts, zrc20 common.Address, amount *big.Int) (*types.Transaction, error) { + return _IBank.contract.Transact(opts, "withdraw", zrc20, amount) +} + +// Withdraw is a paid mutator transaction binding the contract method 0xf3fef3a3. +// +// Solidity: function withdraw(address zrc20, uint256 amount) returns(bool success) +func (_IBank *IBankSession) Withdraw(zrc20 common.Address, amount *big.Int) (*types.Transaction, error) { + return _IBank.Contract.Withdraw(&_IBank.TransactOpts, zrc20, amount) +} + +// Withdraw is a paid mutator transaction binding the contract method 0xf3fef3a3. +// +// Solidity: function withdraw(address zrc20, uint256 amount) returns(bool success) +func (_IBank *IBankTransactorSession) Withdraw(zrc20 common.Address, amount *big.Int) (*types.Transaction, error) { + return _IBank.Contract.Withdraw(&_IBank.TransactOpts, zrc20, amount) +} + +// IBankDepositIterator is returned from FilterDeposit and is used to iterate over the raw logs and unpacked data for Deposit events raised by the IBank contract. +type IBankDepositIterator struct { + Event *IBankDeposit // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *IBankDepositIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(IBankDeposit) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(IBankDeposit) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *IBankDepositIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *IBankDepositIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// IBankDeposit represents a Deposit event raised by the IBank contract. +type IBankDeposit struct { + Zrc20Depositor common.Address + Zrc20Token common.Address + CosmosToken common.Hash + CosmosAddress string + Amount *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterDeposit is a free log retrieval operation binding the contract event 0xbd7d4de0b30a306221956a420cad57737ae9c1ee63072c96a4f1ab81e6eea264. +// +// Solidity: event Deposit(address indexed zrc20_depositor, address indexed zrc20_token, string indexed cosmos_token, string cosmos_address, uint256 amount) +func (_IBank *IBankFilterer) FilterDeposit(opts *bind.FilterOpts, zrc20_depositor []common.Address, zrc20_token []common.Address, cosmos_token []string) (*IBankDepositIterator, error) { + + var zrc20_depositorRule []interface{} + for _, zrc20_depositorItem := range zrc20_depositor { + zrc20_depositorRule = append(zrc20_depositorRule, zrc20_depositorItem) + } + var zrc20_tokenRule []interface{} + for _, zrc20_tokenItem := range zrc20_token { + zrc20_tokenRule = append(zrc20_tokenRule, zrc20_tokenItem) + } + var cosmos_tokenRule []interface{} + for _, cosmos_tokenItem := range cosmos_token { + cosmos_tokenRule = append(cosmos_tokenRule, cosmos_tokenItem) + } + + logs, sub, err := _IBank.contract.FilterLogs(opts, "Deposit", zrc20_depositorRule, zrc20_tokenRule, cosmos_tokenRule) + if err != nil { + return nil, err + } + return &IBankDepositIterator{contract: _IBank.contract, event: "Deposit", logs: logs, sub: sub}, nil +} + +// WatchDeposit is a free log subscription operation binding the contract event 0xbd7d4de0b30a306221956a420cad57737ae9c1ee63072c96a4f1ab81e6eea264. +// +// Solidity: event Deposit(address indexed zrc20_depositor, address indexed zrc20_token, string indexed cosmos_token, string cosmos_address, uint256 amount) +func (_IBank *IBankFilterer) WatchDeposit(opts *bind.WatchOpts, sink chan<- *IBankDeposit, zrc20_depositor []common.Address, zrc20_token []common.Address, cosmos_token []string) (event.Subscription, error) { + + var zrc20_depositorRule []interface{} + for _, zrc20_depositorItem := range zrc20_depositor { + zrc20_depositorRule = append(zrc20_depositorRule, zrc20_depositorItem) + } + var zrc20_tokenRule []interface{} + for _, zrc20_tokenItem := range zrc20_token { + zrc20_tokenRule = append(zrc20_tokenRule, zrc20_tokenItem) + } + var cosmos_tokenRule []interface{} + for _, cosmos_tokenItem := range cosmos_token { + cosmos_tokenRule = append(cosmos_tokenRule, cosmos_tokenItem) + } + + logs, sub, err := _IBank.contract.WatchLogs(opts, "Deposit", zrc20_depositorRule, zrc20_tokenRule, cosmos_tokenRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(IBankDeposit) + if err := _IBank.contract.UnpackLog(event, "Deposit", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseDeposit is a log parse operation binding the contract event 0xbd7d4de0b30a306221956a420cad57737ae9c1ee63072c96a4f1ab81e6eea264. +// +// Solidity: event Deposit(address indexed zrc20_depositor, address indexed zrc20_token, string indexed cosmos_token, string cosmos_address, uint256 amount) +func (_IBank *IBankFilterer) ParseDeposit(log types.Log) (*IBankDeposit, error) { + event := new(IBankDeposit) + if err := _IBank.contract.UnpackLog(event, "Deposit", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// IBankWithdrawIterator is returned from FilterWithdraw and is used to iterate over the raw logs and unpacked data for Withdraw events raised by the IBank contract. +type IBankWithdrawIterator struct { + Event *IBankWithdraw // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *IBankWithdrawIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(IBankWithdraw) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(IBankWithdraw) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *IBankWithdrawIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *IBankWithdrawIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// IBankWithdraw represents a Withdraw event raised by the IBank contract. +type IBankWithdraw struct { + Zrc20Withdrawer common.Address + Zrc20Token common.Address + CosmosToken common.Hash + CosmosAddress string + Amount *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterWithdraw is a free log retrieval operation binding the contract event 0x1ad70707c91d850319aeab00514a0166569359f0b8dc5285bdd6e6b9c464b18e. +// +// Solidity: event Withdraw(address indexed zrc20_withdrawer, address indexed zrc20_token, string indexed cosmos_token, string cosmos_address, uint256 amount) +func (_IBank *IBankFilterer) FilterWithdraw(opts *bind.FilterOpts, zrc20_withdrawer []common.Address, zrc20_token []common.Address, cosmos_token []string) (*IBankWithdrawIterator, error) { + + var zrc20_withdrawerRule []interface{} + for _, zrc20_withdrawerItem := range zrc20_withdrawer { + zrc20_withdrawerRule = append(zrc20_withdrawerRule, zrc20_withdrawerItem) + } + var zrc20_tokenRule []interface{} + for _, zrc20_tokenItem := range zrc20_token { + zrc20_tokenRule = append(zrc20_tokenRule, zrc20_tokenItem) + } + var cosmos_tokenRule []interface{} + for _, cosmos_tokenItem := range cosmos_token { + cosmos_tokenRule = append(cosmos_tokenRule, cosmos_tokenItem) + } + + logs, sub, err := _IBank.contract.FilterLogs(opts, "Withdraw", zrc20_withdrawerRule, zrc20_tokenRule, cosmos_tokenRule) + if err != nil { + return nil, err + } + return &IBankWithdrawIterator{contract: _IBank.contract, event: "Withdraw", logs: logs, sub: sub}, nil +} + +// WatchWithdraw is a free log subscription operation binding the contract event 0x1ad70707c91d850319aeab00514a0166569359f0b8dc5285bdd6e6b9c464b18e. +// +// Solidity: event Withdraw(address indexed zrc20_withdrawer, address indexed zrc20_token, string indexed cosmos_token, string cosmos_address, uint256 amount) +func (_IBank *IBankFilterer) WatchWithdraw(opts *bind.WatchOpts, sink chan<- *IBankWithdraw, zrc20_withdrawer []common.Address, zrc20_token []common.Address, cosmos_token []string) (event.Subscription, error) { + + var zrc20_withdrawerRule []interface{} + for _, zrc20_withdrawerItem := range zrc20_withdrawer { + zrc20_withdrawerRule = append(zrc20_withdrawerRule, zrc20_withdrawerItem) + } + var zrc20_tokenRule []interface{} + for _, zrc20_tokenItem := range zrc20_token { + zrc20_tokenRule = append(zrc20_tokenRule, zrc20_tokenItem) + } + var cosmos_tokenRule []interface{} + for _, cosmos_tokenItem := range cosmos_token { + cosmos_tokenRule = append(cosmos_tokenRule, cosmos_tokenItem) + } + + logs, sub, err := _IBank.contract.WatchLogs(opts, "Withdraw", zrc20_withdrawerRule, zrc20_tokenRule, cosmos_tokenRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(IBankWithdraw) + if err := _IBank.contract.UnpackLog(event, "Withdraw", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseWithdraw is a log parse operation binding the contract event 0x1ad70707c91d850319aeab00514a0166569359f0b8dc5285bdd6e6b9c464b18e. +// +// Solidity: event Withdraw(address indexed zrc20_withdrawer, address indexed zrc20_token, string indexed cosmos_token, string cosmos_address, uint256 amount) +func (_IBank *IBankFilterer) ParseWithdraw(log types.Log) (*IBankWithdraw, error) { + event := new(IBankWithdraw) + if err := _IBank.contract.UnpackLog(event, "Withdraw", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} diff --git a/precompiles/bank/IBank.json b/precompiles/bank/IBank.json new file mode 100644 index 0000000000..1ef1654602 --- /dev/null +++ b/precompiles/bank/IBank.json @@ -0,0 +1,150 @@ +{ + "abi": [ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "zrc20_depositor", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "zrc20_token", + "type": "address" + }, + { + "indexed": true, + "internalType": "string", + "name": "cosmos_token", + "type": "string" + }, + { + "indexed": false, + "internalType": "string", + "name": "cosmos_address", + "type": "string" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Deposit", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "zrc20_withdrawer", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "zrc20_token", + "type": "address" + }, + { + "indexed": true, + "internalType": "string", + "name": "cosmos_token", + "type": "string" + }, + { + "indexed": false, + "internalType": "string", + "name": "cosmos_address", + "type": "string" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Withdraw", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "zrc20", + "type": "address" + }, + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "zrc20", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "deposit", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "zrc20", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "withdraw", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } + ] +} diff --git a/precompiles/bank/IBank.sol b/precompiles/bank/IBank.sol new file mode 100644 index 0000000000..4b5d7f2eba --- /dev/null +++ b/precompiles/bank/IBank.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +/// @title IBank Interface for Cross-chain Token Deposits and Withdrawals +/// @notice This interface defines the functions for depositing ZRC20 tokens and withdrawing Cosmos tokens, +/// as well as querying the balance of Cosmos tokens corresponding to a given ZRC20 token. +/// @dev This contract interacts with a precompiled contract at a fixed address. + +/// @dev The IBank contract's precompiled address. +address constant IBANK_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000067; // Address 103 + +/// @dev The IBank contract instance using the precompiled address. +IBank constant IBANK_CONTRACT = IBank(IBANK_PRECOMPILE_ADDRESS); + +/// @dev Interface for the IBank contract. +interface IBank { + /// @notice Deposit event is emitted when deposit function is called. + /// @param zrc20_depositor Depositor EVM address. + /// @param zrc20_token ZRC20 address deposited. + /// @param cosmos_token Cosmos token denomination the tokens were converted into. + /// @param cosmos_address Cosmos address the tokens were deposited to. + /// @param amount Amount deposited. + event Deposit( + address indexed zrc20_depositor, + address indexed zrc20_token, + string indexed cosmos_token, + string cosmos_address, + uint256 amount + ); + + /// @notice Withdraw event is emitted when withdraw function is called. + /// @param zrc20_withdrawer Withdrawer EVM address. + /// @param zrc20_token ZRC20 address withdrawn. + /// @param cosmos_token Cosmos token denomination the tokens were converted from. + /// @param cosmos_address Cosmos address the tokens were withdrawn from. + /// @param amount Amount withdrawn. + event Withdraw( + address indexed zrc20_withdrawer, + address indexed zrc20_token, + string indexed cosmos_token, + string cosmos_address, + uint256 amount + ); + + /// @notice Deposit a ZRC20 token and mint the corresponding Cosmos token to the user's account. + /// @param zrc20 The ZRC20 token address to be deposited. + /// @param amount The amount of ZRC20 tokens to deposit. + /// @return success Boolean indicating whether the deposit was successful. + function deposit( + address zrc20, + uint256 amount + ) external returns (bool success); + + /// @notice Withdraw Cosmos tokens and convert them back to the corresponding ZRC20 token for the user. + /// @param zrc20 The ZRC20 token address for the corresponding Cosmos token. + /// @param amount The amount of Cosmos tokens to withdraw. + /// @return success Boolean indicating whether the withdrawal was successful. + function withdraw( + address zrc20, + uint256 amount + ) external returns (bool success); + + /// @notice Retrieve the Cosmos token balance corresponding to a specific ZRC20 token for a given user. + /// @param zrc20 The ZRC20 cosmos token denomination to check the balance for. + /// @param user The address of the user to retrieve the balance for. + /// @return balance The balance of the Cosmos token for the specified ZRC20 token and user. + function balanceOf( + address zrc20, + address user + ) external view returns (uint256 balance); +} diff --git a/precompiles/bank/bank.go b/precompiles/bank/bank.go new file mode 100644 index 0000000000..18898969a2 --- /dev/null +++ b/precompiles/bank/bank.go @@ -0,0 +1,208 @@ +package bank + +import ( + "github.com/cosmos/cosmos-sdk/codec" + storetypes "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + bank "github.com/cosmos/cosmos-sdk/x/bank/keeper" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/zeta-chain/protocol-contracts/v2/pkg/zrc20.sol" + + ptypes "github.com/zeta-chain/node/precompiles/types" + fungiblekeeper "github.com/zeta-chain/node/x/fungible/keeper" +) + +var ( + ABI abi.ABI + ContractAddress = common.HexToAddress("0x0000000000000000000000000000000000000067") + GasRequiredByMethod = map[[4]byte]uint64{} + ViewMethod = map[[4]byte]bool{} +) + +func init() { + initABI() +} + +func initABI() { + if err := ABI.UnmarshalJSON([]byte(IBankMetaData.ABI)); err != nil { + panic(err) + } + + GasRequiredByMethod = map[[4]byte]uint64{} + for methodName := range ABI.Methods { + var methodID [4]byte + copy(methodID[:], ABI.Methods[methodName].ID[:4]) + switch methodName { + case DepositMethodName: + GasRequiredByMethod[methodID] = DepositMethodGas + case WithdrawMethodName: + GasRequiredByMethod[methodID] = WithdrawMethodGas + case BalanceOfMethodName: + GasRequiredByMethod[methodID] = BalanceOfGas + default: + GasRequiredByMethod[methodID] = DefaultGas + } + } +} + +type Contract struct { + ptypes.BaseContract + + bankKeeper bank.Keeper + fungibleKeeper fungiblekeeper.Keeper + zrc20ABI *abi.ABI + cdc codec.Codec + kvGasConfig storetypes.GasConfig +} + +func NewIBankContract( + ctx sdk.Context, + bankKeeper bank.Keeper, + fungibleKeeper fungiblekeeper.Keeper, + cdc codec.Codec, + kvGasConfig storetypes.GasConfig, +) *Contract { + accAddress := sdk.AccAddress(ContractAddress.Bytes()) + if fungibleKeeper.GetAuthKeeper().GetAccount(ctx, accAddress) == nil { + fungibleKeeper.GetAuthKeeper().SetAccount(ctx, authtypes.NewBaseAccount(accAddress, nil, 0, 0)) + } + + // Instantiate the ZRC20 ABI only one time. + // This avoids instantiating it every time deposit or withdraw are called. + zrc20ABI, err := zrc20.ZRC20MetaData.GetAbi() + if err != nil { + return nil + } + + return &Contract{ + BaseContract: ptypes.NewBaseContract(ContractAddress), + bankKeeper: bankKeeper, + fungibleKeeper: fungibleKeeper, + zrc20ABI: zrc20ABI, + cdc: cdc, + kvGasConfig: kvGasConfig, + } +} + +// Address() is required to implement the PrecompiledContract interface. +func (c *Contract) Address() common.Address { + return ContractAddress +} + +// Abi() is required to implement the PrecompiledContract interface. +func (c *Contract) Abi() abi.ABI { + return ABI +} + +// RequiredGas is required to implement the PrecompiledContract interface. +// The gas has to be calculated deterministically based on the input. +func (c *Contract) RequiredGas(input []byte) uint64 { + // get methodID (first 4 bytes) + var methodID [4]byte + copy(methodID[:], input[:4]) + // base cost to prevent large input size + baseCost := uint64(len(input)) * c.kvGasConfig.WriteCostPerByte + if ViewMethod[methodID] { + baseCost = uint64(len(input)) * c.kvGasConfig.ReadCostPerByte + } + + if requiredGas, ok := GasRequiredByMethod[methodID]; ok { + return requiredGas + baseCost + } + + // Can not happen, but return 0 if the method is not found. + return 0 +} + +// Run is the entrypoint of the precompiled contract, it switches over the input method, +// and execute them accordingly. +func (c *Contract) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) ([]byte, error) { + method, err := ABI.MethodById(contract.Input[:4]) + if err != nil { + return nil, err + } + + args, err := method.Inputs.Unpack(contract.Input[4:]) + if err != nil { + return nil, err + } + + stateDB := evm.StateDB.(ptypes.ExtStateDB) + + switch method.Name { + // Deposit and Withdraw methods are both not allowed in read-only mode. + case DepositMethodName, WithdrawMethodName: + if readOnly { + return nil, ptypes.ErrUnexpected{ + Got: "method not allowed in read-only mode " + method.Name, + } + } + + var res []byte + execErr := stateDB.ExecuteNativeAction(contract.Address(), nil, func(ctx sdk.Context) error { + if method.Name == DepositMethodName { + res, err = c.deposit(ctx, evm, contract, method, args) + } else if method.Name == WithdrawMethodName { + res, err = c.withdraw(ctx, evm, contract, method, args) + } + return err + }) + if execErr != nil { + return nil, err + } + return res, nil + + case BalanceOfMethodName: + var res []byte + execErr := stateDB.ExecuteNativeAction(contract.Address(), nil, func(ctx sdk.Context) error { + res, err = c.balanceOf(ctx, method, args) + return err + }) + if execErr != nil { + return nil, err + } + return res, nil + + default: + return nil, ptypes.ErrInvalidMethod{ + Method: method.Name, + } + } +} + +// getEVMCallerAddress returns the caller address. +// Usually the caller is the contract.CallerAddress, which is the address of the contract that called the precompiled contract. +// If contract.CallerAddress != evm.Origin is true, it means the call was made through a contract, +// on which case there is a need to set the caller to the evm.Origin. +func getEVMCallerAddress(evm *vm.EVM, contract *vm.Contract) (common.Address, error) { + caller := contract.CallerAddress + if contract.CallerAddress != evm.Origin { + caller = evm.Origin + } + + return caller, nil +} + +// getCosmosAddress returns the counterpart cosmos address of the given ethereum address. +// It checks if the address is empty or blocked by the bank keeper. +func getCosmosAddress(bankKeeper bank.Keeper, addr common.Address) (sdk.AccAddress, error) { + toAddr := sdk.AccAddress(addr.Bytes()) + if toAddr.Empty() { + return nil, &ptypes.ErrInvalidAddr{ + Got: toAddr.String(), + Reason: "empty address", + } + } + + if bankKeeper.BlockedAddr(toAddr) { + return nil, &ptypes.ErrInvalidAddr{ + Got: toAddr.String(), + Reason: "destination address blocked by bank keeper", + } + } + + return toAddr, nil +} diff --git a/precompiles/bank/bank_test.go b/precompiles/bank/bank_test.go new file mode 100644 index 0000000000..518c330a7f --- /dev/null +++ b/precompiles/bank/bank_test.go @@ -0,0 +1,156 @@ +package bank + +import ( + "encoding/json" + "math/big" + "testing" + + storetypes "github.com/cosmos/cosmos-sdk/store/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/stretchr/testify/require" + ethermint "github.com/zeta-chain/ethermint/types" + "github.com/zeta-chain/node/testutil/keeper" + "github.com/zeta-chain/node/testutil/sample" +) + +func Test_IBankContract(t *testing.T) { + var encoding ethermint.EncodingConfig + appCodec := encoding.Codec + fungibleKeeper, ctx, keepers, _ := keeper.FungibleKeeper(t) + gasConfig := storetypes.TransientGasConfig() + + t.Run("should create contract and check address and ABI", func(t *testing.T) { + contract := NewIBankContract(ctx, keepers.BankKeeper, *fungibleKeeper, appCodec, gasConfig) + require.NotNil(t, contract, "NewIBankContract() should not return a nil contract") + + address := contract.Address() + require.Equal(t, ContractAddress, address, "contract address should match the precompiled address") + + abi := contract.Abi() + require.NotNil(t, abi, "contract ABI should not be nil") + }) + + t.Run("should check methods are present in ABI", func(t *testing.T) { + contract := NewIBankContract(ctx, keepers.BankKeeper, *fungibleKeeper, appCodec, gasConfig) + abi := contract.Abi() + + require.NotNil(t, abi.Methods[DepositMethodName], "deposit method should be present in the ABI") + require.NotNil(t, abi.Methods[WithdrawMethodName], "withdraw method should be present in the ABI") + require.NotNil(t, abi.Methods[BalanceOfMethodName], "balanceOf method should be present in the ABI") + }) + + t.Run("should check gas requirements for methods", func(t *testing.T) { + contract := NewIBankContract(ctx, keepers.BankKeeper, *fungibleKeeper, appCodec, gasConfig) + abi := contract.Abi() + var method [4]byte + + t.Run("deposit", func(t *testing.T) { + gasDeposit := contract.RequiredGas(abi.Methods[DepositMethodName].ID) + copy(method[:], abi.Methods[DepositMethodName].ID[:4]) + baseCost := uint64(len(method)) * gasConfig.WriteCostPerByte + require.Equal( + t, + GasRequiredByMethod[method]+baseCost, + gasDeposit, + "deposit method should require %d gas, got %d", + GasRequiredByMethod[method]+baseCost, + gasDeposit, + ) + }) + + t.Run("withdraw", func(t *testing.T) { + gasWithdraw := contract.RequiredGas(abi.Methods[WithdrawMethodName].ID) + copy(method[:], abi.Methods[WithdrawMethodName].ID[:4]) + baseCost := uint64(len(method)) * gasConfig.WriteCostPerByte + require.Equal( + t, + GasRequiredByMethod[method]+baseCost, + gasWithdraw, + "withdraw method should require %d gas, got %d", + GasRequiredByMethod[method]+baseCost, + gasWithdraw, + ) + }) + + t.Run("balanceOf", func(t *testing.T) { + gasBalanceOf := contract.RequiredGas(abi.Methods[BalanceOfMethodName].ID) + copy(method[:], abi.Methods[BalanceOfMethodName].ID[:4]) + baseCost := uint64(len(method)) * gasConfig.WriteCostPerByte + require.Equal( + t, + GasRequiredByMethod[method]+baseCost, + gasBalanceOf, + "balanceOf method should require %d gas, got %d", + GasRequiredByMethod[method]+baseCost, + gasBalanceOf, + ) + }) + + t.Run("invalid method", func(t *testing.T) { + invalidMethodBytes := []byte("invalidMethod") + gasInvalidMethod := contract.RequiredGas(invalidMethodBytes) + require.Equal( + t, + uint64(0), + gasInvalidMethod, + "invalid method should require %d gas, got %d", + uint64(0), + gasInvalidMethod, + ) + }) + }) +} + +func Test_InvalidMethod(t *testing.T) { + var encoding ethermint.EncodingConfig + appCodec := encoding.Codec + fungibleKeeper, ctx, keepers, _ := keeper.FungibleKeeper(t) + gasConfig := storetypes.TransientGasConfig() + + contract := NewIBankContract(ctx, keepers.BankKeeper, *fungibleKeeper, appCodec, gasConfig) + require.NotNil(t, contract, "NewIBankContract() should not return a nil contract") + + abi := contract.Abi() + require.NotNil(t, abi, "contract ABI should not be nil") + + _, doNotExist := abi.Methods["invalidMethod"] + require.False(t, doNotExist, "invalidMethod should not be present in the ABI") +} + +func Test_InvalidABI(t *testing.T) { + IBankMetaData.ABI = "invalid json" + defer func() { + if r := recover(); r != nil { + require.IsType(t, &json.SyntaxError{}, r, "expected error type: json.SyntaxError, got: %T", r) + } + }() + + initABI() +} + +func Test_getEVMCallerAddress(t *testing.T) { + mockEVM := vm.EVM{ + TxContext: vm.TxContext{ + Origin: common.Address{}, + }, + } + + mockVMContract := vm.NewContract( + contractRef{address: common.Address{}}, + contractRef{address: ContractAddress}, + big.NewInt(0), + 0, + ) + + // When contract.CallerAddress == evm.Origin, caller is set to contract.CallerAddress. + caller, err := getEVMCallerAddress(&mockEVM, mockVMContract) + require.NoError(t, err) + require.Equal(t, common.Address{}, caller, "address shouldn be the same") + + // When contract.CallerAddress != evm.Origin, caller should be set to evm.Origin. + mockEVM.Origin = sample.EthAddress() + caller, err = getEVMCallerAddress(&mockEVM, mockVMContract) + require.NoError(t, err) + require.Equal(t, mockEVM.Origin, caller, "address should be evm.Origin") +} diff --git a/precompiles/bank/bindings.go b/precompiles/bank/bindings.go new file mode 100644 index 0000000000..98f35ceeee --- /dev/null +++ b/precompiles/bank/bindings.go @@ -0,0 +1,7 @@ +//go:generate sh -c "solc IBank.sol --combined-json abi | jq '.contracts.\"IBank.sol:IBank\"' > IBank.json" +//go:generate sh -c "cat IBank.json | jq .abi > IBank.abi" +//go:generate sh -c "abigen --abi IBank.abi --pkg bank --type IBank --out IBank.gen.go" + +package bank + +var _ Contract diff --git a/precompiles/bank/coin.go b/precompiles/bank/coin.go new file mode 100644 index 0000000000..2fab8aedcc --- /dev/null +++ b/precompiles/bank/coin.go @@ -0,0 +1,43 @@ +package bank + +import ( + "math/big" + + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" + + ptypes "github.com/zeta-chain/node/precompiles/types" +) + +// ZRC20ToCosmosDenom returns the cosmos coin address for a given ZRC20 address. +// This is converted to "zevm/{ZRC20Address}". +func ZRC20ToCosmosDenom(ZRC20Address common.Address) string { + return ZEVMDenom + ZRC20Address.String() +} + +func createCoinSet(tokenDenom string, amount *big.Int) (sdk.Coins, error) { + coin := sdk.NewCoin(tokenDenom, math.NewIntFromBigInt(amount)) + if !coin.IsValid() { + return nil, &ptypes.ErrInvalidCoin{ + Got: coin.GetDenom(), + Negative: coin.IsNegative(), + Nil: coin.IsNil(), + } + } + + // A sdk.Coins (type []sdk.Coin) has to be created because it's the type expected by MintCoins + // and SendCoinsFromModuleToAccount. + // But sdk.Coins will only contain one coin, always. + coinSet := sdk.NewCoins(coin) + if !coinSet.IsValid() || coinSet.Empty() || coinSet.IsAnyNil() || coinSet == nil { + return nil, &ptypes.ErrInvalidCoin{ + Got: coinSet.String(), + Negative: coinSet.IsAnyNegative(), + Nil: coinSet.IsAnyNil(), + Empty: coinSet.Empty(), + } + } + + return coinSet, nil +} diff --git a/precompiles/bank/coin_test.go b/precompiles/bank/coin_test.go new file mode 100644 index 0000000000..0a9669e0a6 --- /dev/null +++ b/precompiles/bank/coin_test.go @@ -0,0 +1,29 @@ +package bank + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +func Test_ZRC20ToCosmosDenom(t *testing.T) { + address := big.NewInt(12345) // 0x3039 + expected := "zrc20/0x0000000000000000000000000000000000003039" + denom := ZRC20ToCosmosDenom(common.BigToAddress(address)) + require.Equal(t, expected, denom, "denom should be %s, got %s", expected, denom) +} + +func Test_createCoinSet(t *testing.T) { + tokenDenom := "zrc20/0x0000000000000000000000000000000000003039" + amount := big.NewInt(100) + + coinSet, err := createCoinSet(tokenDenom, amount) + require.NoError(t, err, "createCoinSet should not return an error") + require.NotNil(t, coinSet, "coinSet should not be nil") + + coin := coinSet[0] + require.Equal(t, tokenDenom, coin.Denom, "coin denom should be %s, got %s", tokenDenom, coin.Denom) + require.Equal(t, amount, coin.Amount.BigInt(), "coin amount should be %s, got %s", amount, coin.Amount.BigInt()) +} diff --git a/precompiles/bank/const.go b/precompiles/bank/const.go new file mode 100644 index 0000000000..3c9755d2e3 --- /dev/null +++ b/precompiles/bank/const.go @@ -0,0 +1,22 @@ +package bank + +const ( + // ZEVM cosmos coins prefix. + ZEVMDenom = "zrc20/" + + // Write methods. + DepositMethodName = "deposit" + DepositMethodGas = 200_000 + DepositEventName = "Deposit" + + WithdrawMethodName = "withdraw" + WithdrawMethodGas = 200_000 + WithdrawEventName = "Withdraw" + + // Read methods. + BalanceOfMethodName = "balanceOf" + BalanceOfGas = 10_000 + + // Default gas for unknown methods. + DefaultGas = 0 +) diff --git a/precompiles/bank/logs.go b/precompiles/bank/logs.go new file mode 100644 index 0000000000..36b877dfa5 --- /dev/null +++ b/precompiles/bank/logs.go @@ -0,0 +1,50 @@ +package bank + +import ( + "math/big" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + + "github.com/zeta-chain/node/precompiles/logs" +) + +type eventData struct { + zrc20Addr common.Address + zrc20Token common.Address + cosmosAddr string + cosmosCoin string + amount *big.Int +} + +func (c *Contract) addEventLog( + ctx sdk.Context, + stateDB vm.StateDB, + eventName string, + eventData eventData, +) error { + event := c.Abi().Events[eventName] + + topics, err := logs.MakeTopics( + event, + []interface{}{common.BytesToAddress(eventData.zrc20Addr.Bytes())}, + []interface{}{common.BytesToAddress(eventData.zrc20Token.Bytes())}, + []interface{}{eventData.cosmosCoin}, + ) + if err != nil { + return err + } + + data, err := logs.PackArguments([]logs.Argument{ + {Type: "string", Value: eventData.cosmosAddr}, + {Type: "uint256", Value: eventData.amount}, + }) + if err != nil { + return err + } + + logs.AddLog(ctx, c.Address(), stateDB, topics, data) + + return nil +} diff --git a/precompiles/bank/method_balance_of.go b/precompiles/bank/method_balance_of.go new file mode 100644 index 0000000000..e4bc644a2b --- /dev/null +++ b/precompiles/bank/method_balance_of.go @@ -0,0 +1,80 @@ +package bank + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + + ptypes "github.com/zeta-chain/node/precompiles/types" +) + +// balanceOf returns the balance of cosmos coins minted by the bank's deposit function, +// for a given cosmos account calculated with toAddr := sdk.AccAddress(addr.Bytes()). +// The denomination of the cosmos coin will be "zrc20/0x12345" where 0x12345 is the ZRC20 address. +// Call this function using solidity with the following signature: +// From IBank.sol: function balanceOf(address zrc20, address user) external view returns (uint256 balance); +func (c *Contract) balanceOf( + ctx sdk.Context, + method *abi.Method, + args []interface{}, +) (result []byte, err error) { + if len(args) != 2 { + return nil, &(ptypes.ErrInvalidNumberOfArgs{ + Got: len(args), + Expect: 2, + }) + } + + // function balanceOf(address zrc20, address user) external view returns (uint256 balance); + zrc20Addr, addr, err := unpackBalanceOfArgs(args) + if err != nil { + return nil, err + } + + // Get the counterpart cosmos address. + toAddr, err := getCosmosAddress(c.bankKeeper, addr) + if err != nil { + return nil, err + } + + // Safety check: token has to be a valid whitelisted ZRC20 and not be paused. + // Do not check for t.Paused, as the balance is read only the EOA won't be able to operate. + _, found := c.fungibleKeeper.GetForeignCoins(ctx, zrc20Addr.String()) + if !found { + return nil, &ptypes.ErrInvalidToken{ + Got: zrc20Addr.String(), + Reason: "token is not a whitelisted ZRC20", + } + } + + // Bank Keeper GetBalance returns the specified Cosmos coin balance for a given address. + // Check explicitly the balance is a non-negative non-nil value. + coin := c.bankKeeper.GetBalance(ctx, toAddr, ZRC20ToCosmosDenom(zrc20Addr)) + if !coin.IsValid() { + return nil, &ptypes.ErrInvalidCoin{ + Got: coin.GetDenom(), + Negative: coin.IsNegative(), + Nil: coin.IsNil(), + } + } + + return method.Outputs.Pack(coin.Amount.BigInt()) +} + +func unpackBalanceOfArgs(args []interface{}) (zrc20Addr common.Address, addr common.Address, err error) { + zrc20Addr, ok := args[0].(common.Address) + if !ok { + return common.Address{}, common.Address{}, &ptypes.ErrInvalidAddr{ + Got: zrc20Addr.String(), + } + } + + addr, ok = args[1].(common.Address) + if !ok { + return common.Address{}, common.Address{}, &ptypes.ErrInvalidAddr{ + Got: addr.String(), + } + } + + return zrc20Addr, addr, nil +} diff --git a/precompiles/bank/method_deposit.go b/precompiles/bank/method_deposit.go new file mode 100644 index 0000000000..3d139124e3 --- /dev/null +++ b/precompiles/bank/method_deposit.go @@ -0,0 +1,197 @@ +package bank + +import ( + "math/big" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + + ptypes "github.com/zeta-chain/node/precompiles/types" + "github.com/zeta-chain/node/x/fungible/types" +) + +// deposit is used to deposit ZRC20 into the bank contract, and receive the same amount of cosmos coins in exchange. +// The denomination of the cosmos coin will be "zrc20/ZRC20Address", as an example depossiting an arbitrary ZRC20 token with +// address 0x12345 will mint cosmos coins with the denomination "zrc20/0x12345". +// The caller cosmos address will be calculated from the EVM caller address. by executing toAddr := sdk.AccAddress(addr.Bytes()). +// This function can be think of a permissionless way of minting cosmos coins. +// This is how deposit works: +// - The caller has to allow the bank contract to spend a certain amount ZRC20 token coins on its behalf. This is mandatory. +// - Then, the caller calls deposit(ZRC20 address, amount), to deposit the amount and receive cosmos coins. +// - The bank will check there's enough balance, the caller is not a blocked address, and the token is a not paused ZRC20. +// - Then the cosmos coins "zrc20/0x12345" will be minted and sent to the caller's cosmos address. +// Call this function using solidity with the following signature: +// - From IBank.sol: function deposit(address zrc20, uint256 amount) external returns (bool success); +func (c *Contract) deposit( + ctx sdk.Context, + evm *vm.EVM, + contract *vm.Contract, + method *abi.Method, + args []interface{}, +) (result []byte, err error) { + // This function is developed using the Check - Effects - Interactions pattern: + // 1. Check everything is correct. + if len(args) != 2 { + return nil, &(ptypes.ErrInvalidNumberOfArgs{ + Got: len(args), + Expect: 2, + }) + } + + // Unpack parameters for function deposit. + // function deposit(address zrc20, uint256 amount) external returns (bool success); + zrc20Addr, amount, err := unpackDepositArgs(args) + if err != nil { + return nil, err + } + + // Get the correct caller address. + caller, err := getEVMCallerAddress(evm, contract) + if err != nil { + return nil, err + } + + // Get the cosmos address of the caller. + toAddr, err := getCosmosAddress(c.bankKeeper, caller) + if err != nil { + return nil, err + } + + // Safety check: token has to be a valid whitelisted ZRC20 and not be paused. + t, found := c.fungibleKeeper.GetForeignCoins(ctx, zrc20Addr.String()) + if !found || t.Paused { + return nil, &ptypes.ErrInvalidToken{ + Got: zrc20Addr.String(), + Reason: "token is not a whitelisted ZRC20 or it's paused", + } + } + + // Check for enough balance. + // function balanceOf(address account) public view virtual override returns (uint256) + resBalanceOf, err := c.CallContract( + ctx, + &c.fungibleKeeper, + c.zrc20ABI, + zrc20Addr, + "balanceOf", + []interface{}{caller}, + ) + if err != nil { + return nil, &ptypes.ErrUnexpected{ + When: "balanceOf", + Got: err.Error(), + } + } + + balance, ok := resBalanceOf[0].(*big.Int) + if !ok || balance.Cmp(amount) < 0 || balance.Cmp(big.NewInt(0)) <= 0 { + return nil, &ptypes.ErrInvalidAmount{ + Got: balance.String(), + } + } + + // Check for enough bank's allowance. + // function allowance(address owner, address spender) public view virtual override returns (uint256) + resAllowance, err := c.CallContract( + ctx, + &c.fungibleKeeper, + c.zrc20ABI, + zrc20Addr, + "allowance", + []interface{}{caller, ContractAddress}, + ) + if err != nil { + return nil, &ptypes.ErrUnexpected{ + When: "allowance", + Got: err.Error(), + } + } + + allowance, ok := resAllowance[0].(*big.Int) + if !ok || allowance.Cmp(amount) < 0 || allowance.Cmp(big.NewInt(0)) <= 0 { + return nil, &ptypes.ErrInvalidAmount{ + Got: allowance.String(), + } + } + + // The process of creating a new cosmos coin is: + // - Generate the new coin denom using ZRC20 address, + // this way we map ZRC20 addresses to cosmos denoms "zevm/0x12345". + // - Mint coins. + // - Send coins to the caller. + coinSet, err := createCoinSet(ZRC20ToCosmosDenom(zrc20Addr), amount) + if err != nil { + return nil, err + } + + // 2. Effect: subtract balance. + // function transferFrom(address sender, address recipient, uint256 amount) public virtual override returns (bool) + resTransferFrom, err := c.CallContract( + ctx, + &c.fungibleKeeper, + c.zrc20ABI, + zrc20Addr, + "transferFrom", + []interface{}{caller, ContractAddress, amount}, + ) + if err != nil { + return nil, &ptypes.ErrUnexpected{ + When: "transferFrom", + Got: err.Error(), + } + } + + transferred, ok := resTransferFrom[0].(bool) + if !ok || !transferred { + return nil, &ptypes.ErrUnexpected{ + When: "transferFrom", + Got: "transaction not successful", + } + } + + // 3. Interactions: create cosmos coin and send. + err = c.bankKeeper.MintCoins(ctx, types.ModuleName, coinSet) + if err != nil { + return nil, &ptypes.ErrUnexpected{ + When: "MintCoins", + Got: err.Error(), + } + } + + err = c.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, toAddr, coinSet) + if err != nil { + return nil, &ptypes.ErrUnexpected{ + When: "SendCoinsFromModuleToAccount", + Got: err.Error(), + } + } + + if err := c.addEventLog(ctx, evm.StateDB, DepositEventName, eventData{caller, zrc20Addr, toAddr.String(), coinSet.Denoms()[0], amount}); err != nil { + return nil, &ptypes.ErrUnexpected{ + When: "AddDepositLog", + Got: err.Error(), + } + } + + return method.Outputs.Pack(true) +} + +func unpackDepositArgs(args []interface{}) (zrc20Addr common.Address, amount *big.Int, err error) { + zrc20Addr, ok := args[0].(common.Address) + if !ok { + return common.Address{}, nil, &ptypes.ErrInvalidAddr{ + Got: zrc20Addr.String(), + } + } + + amount, ok = args[1].(*big.Int) + if !ok || amount.Sign() < 0 || amount == nil || amount == new(big.Int) { + return common.Address{}, nil, &ptypes.ErrInvalidAmount{ + Got: amount.String(), + } + } + + return zrc20Addr, amount, nil +} diff --git a/precompiles/bank/method_test.go b/precompiles/bank/method_test.go new file mode 100644 index 0000000000..bf94918430 --- /dev/null +++ b/precompiles/bank/method_test.go @@ -0,0 +1,284 @@ +package bank + +import ( + "math/big" + "testing" + + storetypes "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" + "github.com/stretchr/testify/require" + ethermint "github.com/zeta-chain/ethermint/types" + evmkeeper "github.com/zeta-chain/ethermint/x/evm/keeper" + "github.com/zeta-chain/ethermint/x/evm/statedb" + "github.com/zeta-chain/node/pkg/chains" + erc1967proxy "github.com/zeta-chain/node/pkg/contracts/erc1967proxy" + ptypes "github.com/zeta-chain/node/precompiles/types" + "github.com/zeta-chain/node/testutil/keeper" + "github.com/zeta-chain/node/testutil/sample" + fungiblekeeper "github.com/zeta-chain/node/x/fungible/keeper" + fungibletypes "github.com/zeta-chain/node/x/fungible/types" + gatewayzevm "github.com/zeta-chain/protocol-contracts/v2/pkg/gatewayzevm.sol" +) + +func Test_Deposit(t *testing.T) { + t.Run("should fail when caller has 0 token balance", func(t *testing.T) { + ts := setupChain(t) + + methodID := ts.abi.Methods[DepositMethodName] + + // Set CallerAddress and evm.Origin to the caller address. + // Caller does not have any balance, and bank does not have any allowance. + ts.mockVMContract.CallerAddress = fungibletypes.ModuleAddressEVM + ts.mockEVM.Origin = fungibletypes.ModuleAddressEVM + + // Set the input arguments for the deposit method. + ts.mockVMContract.Input = packInputArgs( + t, + methodID, + []interface{}{ts.zrc20Address, big.NewInt(0)}..., + ) + + _, err := ts.contract.Run(ts.mockEVM, ts.mockVMContract, false) + require.ErrorAs( + t, + ptypes.ErrInvalidAmount{ + Got: "0", + }, + err, + ) + }) + + t.Run("should fail when bank has 0 token allowance", func(t *testing.T) { + ts := setupChain(t) + ts.fungibleKeeper.DepositZRC20(ts.ctx, ts.zrc20Address, fungibletypes.ModuleAddressEVM, big.NewInt(1000)) + + methodID := ts.abi.Methods[DepositMethodName] + + // Set CallerAddress and evm.Origin to the caller address. + // Caller does not have any balance, and bank does not have any allowance. + ts.mockVMContract.CallerAddress = fungibletypes.ModuleAddressEVM + ts.mockEVM.Origin = fungibletypes.ModuleAddressEVM + + // Set the input arguments for the deposit method. + ts.mockVMContract.Input = packInputArgs( + t, + methodID, + []interface{}{ts.zrc20Address, big.NewInt(0)}..., + ) + + _, err := ts.contract.Run(ts.mockEVM, ts.mockVMContract, false) + require.ErrorAs( + t, + ptypes.ErrInvalidAmount{ + Got: "0", + }, + err, + ) + }) +} + +/* + Functions to set up the test environment. +*/ + +type testSuite struct { + ctx sdk.Context + fungibleKeeper *fungiblekeeper.Keeper + sdkKeepers keeper.SDKKeepers + contract *Contract + abi abi.ABI + mockEVM *vm.EVM + mockVMContract *vm.Contract + zrc20Address common.Address +} + +func setupChain(t *testing.T) testSuite { + // Initialize basic parameters to mock the chain. + fungibleKeeper, ctx, sdkKeepers, _ := keeper.FungibleKeeper(t) + chainID := getValidChainID(t) + + // Make sure the account store is initialized. + // This is completely needed for accounts to be created in the state. + fungibleKeeper.GetAuthKeeper().GetModuleAccount(ctx, fungibletypes.ModuleName) + + // Deploy system contracts in order to deploy a ZRC20 token. + deploySystemContracts(t, ctx, fungibleKeeper, *sdkKeepers.EvmKeeper) + zrc20Address := setupGasCoin(t, ctx, fungibleKeeper, sdkKeepers.EvmKeeper, chainID, "ZRC20", "ZRC20") + + // Keepers and chain configuration. + var encoding ethermint.EncodingConfig + appCodec := encoding.Codec + gasConfig := storetypes.TransientGasConfig() + + // Create the bank contract. + contract := NewIBankContract(ctx, sdkKeepers.BankKeeper, *fungibleKeeper, appCodec, gasConfig) + require.NotNil(t, contract, "NewIBankContract() should not return a nil contract") + + abi := contract.Abi() + require.NotNil(t, abi, "contract ABI should not be nil") + + address := contract.Address() + require.NotNil(t, address, "contract address should not be nil") + + mockEVM := vm.NewEVM( + vm.BlockContext{}, + vm.TxContext{}, + statedb.New(ctx, sdkKeepers.EvmKeeper, statedb.TxConfig{}), + ¶ms.ChainConfig{}, + vm.Config{}, + ) + + mockVMContract := vm.NewContract( + contractRef{address: common.Address{}}, + contractRef{address: ContractAddress}, + big.NewInt(0), + 0, + ) + + return testSuite{ + ctx, + fungibleKeeper, + sdkKeepers, + contract, + abi, + mockEVM, + mockVMContract, + zrc20Address, + } +} + +// setupGasCoin is a helper function to setup the gas coin for testing +func setupGasCoin( + t *testing.T, + ctx sdk.Context, + k *fungiblekeeper.Keeper, + evmk *evmkeeper.Keeper, + chainID int64, + assetName string, + symbol string, +) (zrc20 common.Address) { + addr, err := k.SetupChainGasCoinAndPool( + ctx, + chainID, + assetName, + symbol, + 8, + nil, + ) + require.NoError(t, err) + assertContractDeployment(t, *evmk, ctx, addr) + return addr +} + +// get a valid chain id independently of the build flag +func getValidChainID(t *testing.T) int64 { + list := chains.DefaultChainsList() + require.NotEmpty(t, list) + require.NotNil(t, list[0]) + return list[0].ChainId +} + +// require that a contract has been deployed by checking stored code is non-empty. +func assertContractDeployment(t *testing.T, k evmkeeper.Keeper, ctx sdk.Context, contractAddress common.Address) { + acc := k.GetAccount(ctx, contractAddress) + require.NotNil(t, acc) + code := k.GetCode(ctx, common.BytesToHash(acc.CodeHash)) + require.NotEmpty(t, code) +} + +// deploySystemContracts deploys the system contracts and returns their addresses. +func deploySystemContracts( + t *testing.T, + ctx sdk.Context, + k *fungiblekeeper.Keeper, + evmk evmkeeper.Keeper, +) (wzeta, uniswapV2Factory, uniswapV2Router, connector, systemContract common.Address) { + var err error + + wzeta, err = k.DeployWZETA(ctx) + require.NoError(t, err) + require.NotEmpty(t, wzeta) + assertContractDeployment(t, evmk, ctx, wzeta) + + uniswapV2Factory, err = k.DeployUniswapV2Factory(ctx) + require.NoError(t, err) + require.NotEmpty(t, uniswapV2Factory) + assertContractDeployment(t, evmk, ctx, uniswapV2Factory) + + uniswapV2Router, err = k.DeployUniswapV2Router02(ctx, uniswapV2Factory, wzeta) + require.NoError(t, err) + require.NotEmpty(t, uniswapV2Router) + assertContractDeployment(t, evmk, ctx, uniswapV2Router) + + connector, err = k.DeployConnectorZEVM(ctx, wzeta) + require.NoError(t, err) + require.NotEmpty(t, connector) + assertContractDeployment(t, evmk, ctx, connector) + + systemContract, err = k.DeploySystemContract(ctx, wzeta, uniswapV2Factory, uniswapV2Router) + require.NoError(t, err) + require.NotEmpty(t, systemContract) + assertContractDeployment(t, evmk, ctx, systemContract) + + // deploy the gateway contract + contract := deployGatewayContract(t, ctx, k, &evmk, wzeta, sample.EthAddress()) + require.NotEmpty(t, contract) + + return +} + +// deploy upgradable gateway contract and return its address +func deployGatewayContract( + t *testing.T, + ctx sdk.Context, + k *fungiblekeeper.Keeper, + evmk *evmkeeper.Keeper, + wzeta, admin common.Address, +) common.Address { + // Deploy the gateway contract + implAddr, err := k.DeployContract(ctx, gatewayzevm.GatewayZEVMMetaData) + require.NoError(t, err) + require.NotEmpty(t, implAddr) + assertContractDeployment(t, *evmk, ctx, implAddr) + + // Deploy the proxy contract + gatewayABI, err := gatewayzevm.GatewayZEVMMetaData.GetAbi() + require.NoError(t, err) + + // Encode the initializer data + initializerData, err := gatewayABI.Pack("initialize", wzeta, admin) + require.NoError(t, err) + + gatewayContract, err := k.DeployContract(ctx, erc1967proxy.ERC1967ProxyMetaData, implAddr, initializerData) + require.NoError(t, err) + require.NotEmpty(t, gatewayContract) + assertContractDeployment(t, *evmk, ctx, gatewayContract) + + // store the gateway in the system contract object + sys, found := k.GetSystemContract(ctx) + if !found { + sys = fungibletypes.SystemContract{} + } + sys.Gateway = gatewayContract.Hex() + k.SetSystemContract(ctx, sys) + + return gatewayContract +} + +func packInputArgs(t *testing.T, methodID abi.Method, args ...interface{}) []byte { + input, err := methodID.Inputs.Pack(args...) + require.NoError(t, err) + return append(methodID.ID, input...) +} + +type contractRef struct { + address common.Address +} + +func (c contractRef) Address() common.Address { + return c.address +} diff --git a/precompiles/bank/method_withdraw.go b/precompiles/bank/method_withdraw.go new file mode 100644 index 0000000000..81b7bcef4b --- /dev/null +++ b/precompiles/bank/method_withdraw.go @@ -0,0 +1,179 @@ +package bank + +import ( + "math/big" + + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + + ptypes "github.com/zeta-chain/node/precompiles/types" + "github.com/zeta-chain/node/x/fungible/types" +) + +// withdraw is used to withdraw cosmos coins minted using the bank's deposit function. +// The caller has to have enough cosmos coin on its cosmos account balance to withdraw the requested amount. +// After all check pass the bank will burn the cosmos coins and transfer the ZRC20 amount to the withdrawer. +// The cosmos coins have the denomination of "zrc20/0x12345" where 0x12345 is the ZRC20 address. +// Call this function using solidity with the following signature: +// From IBank.sol: function withdraw(address zrc20, uint256 amount) external returns (bool success); +// The address to be passed to the function is the ZRC20 address, like in 0x12345. +func (c *Contract) withdraw( + ctx sdk.Context, + evm *vm.EVM, + contract *vm.Contract, + method *abi.Method, + args []interface{}, +) (result []byte, err error) { + // 1. Check everything is correct. + if len(args) != 2 { + return nil, &(ptypes.ErrInvalidNumberOfArgs{ + Got: len(args), + Expect: 2, + }) + } + + // Unpack parameters for function withdraw. + // function withdraw(address zrc20, uint256 amount) external returns (bool success); + zrc20Addr, amount, err := unpackWithdrawArgs(args) + if err != nil { + return nil, err + } + + // Get the correct caller address. + caller, err := getEVMCallerAddress(evm, contract) + if err != nil { + return nil, err + } + + // Get the cosmos address of the caller. + // This address should have enough cosmos coin balance as the requested amount. + fromAddr, err := getCosmosAddress(c.bankKeeper, caller) + if err != nil { + return nil, err + } + + // Safety check: token has to be a valid whitelisted ZRC20 and not be paused. + t, found := c.fungibleKeeper.GetForeignCoins(ctx, zrc20Addr.String()) + if !found || t.Paused { + return nil, &ptypes.ErrInvalidToken{ + Got: zrc20Addr.String(), + Reason: "token is not a whitelisted ZRC20 or it's paused", + } + } + + // Caller has to have enough cosmos coin balance to withdraw the requested amount. + coin := c.bankKeeper.GetBalance(ctx, fromAddr, ZRC20ToCosmosDenom(zrc20Addr)) + if coin.Amount.IsNil() { + return nil, &ptypes.ErrInsufficientBalance{ + Requested: amount.String(), + Got: "nil", + } + } + + if coin.Amount.LT(math.NewIntFromBigInt(amount)) { + return nil, &ptypes.ErrInsufficientBalance{ + Requested: amount.String(), + Got: coin.Amount.String(), + } + } + + coinSet, err := createCoinSet(ZRC20ToCosmosDenom(zrc20Addr), amount) + if err != nil { + return nil, err + } + + // Check for bank's ZRC20 balance. + // function balanceOf(address account) public view virtual override returns (uint256) + resBalanceOf, err := c.CallContract( + ctx, + &c.fungibleKeeper, + c.zrc20ABI, + zrc20Addr, + "balanceOf", + []interface{}{ContractAddress}, + ) + if err != nil { + return nil, &ptypes.ErrUnexpected{ + When: "balanceOf", + Got: err.Error(), + } + } + + balance, ok := resBalanceOf[0].(*big.Int) + if !ok || balance.Cmp(amount) == -1 { + return nil, &ptypes.ErrInvalidAmount{ + Got: "not enough bank balance", + } + } + + // 2. Effect: transfer balance. + + // function transfer(address recipient, uint256 amount) public virtual override returns (bool) + resTransferFrom, err := c.CallContract( + ctx, + &c.fungibleKeeper, + c.zrc20ABI, + zrc20Addr, + "transfer", + []interface{}{caller /* sender */, amount}, + ) + if err != nil { + return nil, &ptypes.ErrUnexpected{ + When: "transferFrom", + Got: err.Error(), + } + } + + transferred, ok := resTransferFrom[0].(bool) + if !ok || !transferred { + return nil, &ptypes.ErrUnexpected{ + When: "transferFrom", + Got: "transaction not successful", + } + } + + // 3. Interactions: send to module and burn. + if err := c.bankKeeper.SendCoinsFromAccountToModule(ctx, fromAddr, types.ModuleName, coinSet); err != nil { + return nil, &ptypes.ErrUnexpected{ + When: "SendCoinsFromAccountToModule", + Got: err.Error(), + } + } + + if err := c.bankKeeper.BurnCoins(ctx, types.ModuleName, coinSet); err != nil { + return nil, &ptypes.ErrUnexpected{ + When: "BurnCoins", + Got: err.Error(), + } + } + + if err := c.addEventLog(ctx, evm.StateDB, WithdrawEventName, eventData{caller, zrc20Addr, fromAddr.String(), coinSet.Denoms()[0], amount}); err != nil { + return nil, &ptypes.ErrUnexpected{ + When: "AddWithdrawLog", + Got: err.Error(), + } + } + + return method.Outputs.Pack(true) +} + +func unpackWithdrawArgs(args []interface{}) (zrc20Addr common.Address, amount *big.Int, err error) { + zrc20Addr, ok := args[0].(common.Address) + if !ok { + return common.Address{}, nil, &ptypes.ErrInvalidAddr{ + Got: zrc20Addr.String(), + } + } + + amount, ok = args[1].(*big.Int) + if !ok || amount.Sign() < 0 || amount == nil || amount == new(big.Int) { + return common.Address{}, nil, &ptypes.ErrInvalidAmount{ + Got: amount.String(), + } + } + + return zrc20Addr, amount, nil +} diff --git a/precompiles/logs/logs.go b/precompiles/logs/logs.go index f168e33da4..07960a442e 100644 --- a/precompiles/logs/logs.go +++ b/precompiles/logs/logs.go @@ -1,8 +1,6 @@ package logs import ( - "math/big" - sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" @@ -10,6 +8,11 @@ import ( "github.com/ethereum/go-ethereum/core/vm" ) +type Argument struct { + Type string + Value interface{} +} + // AddLog adds log to stateDB func AddLog(ctx sdk.Context, precompileAddr common.Address, stateDB vm.StateDB, topics []common.Hash, data []byte) { stateDB.AddLog(&types.Log{ @@ -38,18 +41,29 @@ func MakeTopics(event abi.Event, query ...[]interface{}) ([]common.Hash, error) return topics, nil } -// PackBigInt is a helper function to pack a uint256 amount -func PackBigInt(amount *big.Int) ([]byte, error) { - uint256Type, err := abi.NewType("uint256", "", nil) - if err != nil { - return nil, err +// PackArguments packs an arbitrary number of logs.Arguments as non-indexed data. +// When packing data, make sure the Argument are passed in the same order as the event definition. +func PackArguments(args []Argument) ([]byte, error) { + types := abi.Arguments{} + toPack := []interface{}{} + + for _, arg := range args { + abiType, err := abi.NewType(arg.Type, "", nil) + if err != nil { + return nil, err + } + + types = append(types, abi.Argument{ + Type: abiType, + }) + + toPack = append(toPack, arg.Value) } - arguments := abi.Arguments{ - { - Type: uint256Type, - }, + data, err := types.Pack(toPack...) + if err != nil { + return nil, err } - return arguments.Pack(amount) + return data, nil } diff --git a/precompiles/precompiles.go b/precompiles/precompiles.go index 1adf65005f..cdd5e2ff74 100644 --- a/precompiles/precompiles.go +++ b/precompiles/precompiles.go @@ -4,12 +4,14 @@ import ( "github.com/cosmos/cosmos-sdk/codec" storetypes "github.com/cosmos/cosmos-sdk/store/types" sdktypes "github.com/cosmos/cosmos-sdk/types" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" ethparams "github.com/ethereum/go-ethereum/params" evmkeeper "github.com/zeta-chain/ethermint/x/evm/keeper" + "github.com/zeta-chain/node/precompiles/bank" "github.com/zeta-chain/node/precompiles/prototype" "github.com/zeta-chain/node/precompiles/staking" fungiblekeeper "github.com/zeta-chain/node/x/fungible/keeper" @@ -21,12 +23,14 @@ import ( var EnabledStatefulContracts = map[common.Address]bool{ prototype.ContractAddress: true, staking.ContractAddress: true, + bank.ContractAddress: true, } // StatefulContracts returns all the registered precompiled contracts. func StatefulContracts( fungibleKeeper *fungiblekeeper.Keeper, stakingKeeper *stakingkeeper.Keeper, + bankKeeper bankkeeper.Keeper, cdc codec.Codec, gasConfig storetypes.GasConfig, ) (precompiledContracts []evmkeeper.CustomContractFn) { @@ -53,5 +57,14 @@ func StatefulContracts( precompiledContracts = append(precompiledContracts, stakingContract) } + if EnabledStatefulContracts[bank.ContractAddress] { + bankContract := func(ctx sdktypes.Context, _ ethparams.Rules) vm.PrecompiledContract { + return bank.NewIBankContract(ctx, bankKeeper, *fungibleKeeper, cdc, gasConfig) + } + + // Append the staking contract to the precompiledContracts slice. + precompiledContracts = append(precompiledContracts, bankContract) + } + return precompiledContracts } diff --git a/precompiles/precompiles_test.go b/precompiles/precompiles_test.go index 998240e644..a0b55572ea 100644 --- a/precompiles/precompiles_test.go +++ b/precompiles/precompiles_test.go @@ -25,7 +25,7 @@ func Test_StatefulContracts(t *testing.T) { } // StatefulContracts() should return all the enabled contracts. - contracts := StatefulContracts(k, &sdkk.StakingKeeper, appCodec, gasConfig) + contracts := StatefulContracts(k, &sdkk.StakingKeeper, sdkk.BankKeeper, appCodec, gasConfig) require.NotNil(t, contracts, "StatefulContracts() should not return a nil slice") require.Len(t, contracts, expectedContracts, "StatefulContracts() should return all the enabled contracts") diff --git a/precompiles/prototype/IPrototype.go b/precompiles/prototype/IPrototype.gen.go similarity index 100% rename from precompiles/prototype/IPrototype.go rename to precompiles/prototype/IPrototype.gen.go diff --git a/precompiles/prototype/bindings.go b/precompiles/prototype/bindings.go index e4a31a5e56..eeb59d988e 100644 --- a/precompiles/prototype/bindings.go +++ b/precompiles/prototype/bindings.go @@ -1,6 +1,6 @@ //go:generate sh -c "solc IPrototype.sol --combined-json abi | jq '.contracts.\"IPrototype.sol:IPrototype\"' > IPrototype.json" //go:generate sh -c "cat IPrototype.json | jq .abi > IPrototype.abi" -//go:generate sh -c "abigen --abi IPrototype.abi --pkg prototype --type IPrototype --out IPrototype.go" +//go:generate sh -c "abigen --abi IPrototype.abi --pkg prototype --type IPrototype --out IPrototype.gen.go" package prototype diff --git a/precompiles/staking/IStaking.go b/precompiles/staking/IStaking.gen.go similarity index 100% rename from precompiles/staking/IStaking.go rename to precompiles/staking/IStaking.gen.go diff --git a/precompiles/staking/bindings.go b/precompiles/staking/bindings.go index 5e182735be..324289b9f0 100644 --- a/precompiles/staking/bindings.go +++ b/precompiles/staking/bindings.go @@ -1,6 +1,6 @@ //go:generate sh -c "solc IStaking.sol --combined-json abi | jq '.contracts.\"IStaking.sol:IStaking\"' > IStaking.json" //go:generate sh -c "cat IStaking.json | jq .abi > IStaking.abi" -//go:generate sh -c "abigen --abi IStaking.abi --pkg staking --type IStaking --out IStaking.go" +//go:generate sh -c "abigen --abi IStaking.abi --pkg staking --type IStaking --out IStaking.gen.go" package staking diff --git a/precompiles/staking/logs.go b/precompiles/staking/logs.go index 165dd91aa3..ea8e51274c 100644 --- a/precompiles/staking/logs.go +++ b/precompiles/staking/logs.go @@ -37,7 +37,9 @@ func (c *Contract) AddStakeLog( } // amount is part of event data - data, err := logs.PackBigInt(amount) + data, err := logs.PackArguments([]logs.Argument{ + {Type: "uint256", Value: amount}, + }) if err != nil { return err } @@ -67,7 +69,9 @@ func (c *Contract) AddUnstakeLog( } // amount is part of event data - data, err := logs.PackBigInt(amount) + data, err := logs.PackArguments([]logs.Argument{ + {Type: "uint256", Value: amount}, + }) if err != nil { return err } @@ -108,7 +112,9 @@ func (c *Contract) AddMoveStakeLog( } // amount is part of event data - data, err := logs.PackBigInt(amount) + data, err := logs.PackArguments([]logs.Argument{ + {Type: "uint256", Value: amount}, + }) if err != nil { return err } diff --git a/precompiles/types/errors.go b/precompiles/types/errors.go index 0cc6928541..03e397fa14 100644 --- a/precompiles/types/errors.go +++ b/precompiles/types/errors.go @@ -3,19 +3,22 @@ package types import "fmt" /* -Address related errors + Address related errors */ + type ErrInvalidAddr struct { - Got string + Got string + Reason string } func (e ErrInvalidAddr) Error() string { - return fmt.Sprintf("invalid address %s", e.Got) + return fmt.Sprintf("invalid address %s, reason: %s", e.Got, e.Reason) } /* -Argument related errors + Argument related errors */ + type ErrInvalidNumberOfArgs struct { Got, Expect int } @@ -33,8 +36,56 @@ func (e ErrInvalidArgument) Error() string { } /* -Method related errors + Token related errors +*/ + +type ErrInvalidToken struct { + Got string + Reason string +} + +func (e ErrInvalidToken) Error() string { + return fmt.Sprintf("invalid token %s: %s", e.Got, e.Reason) +} + +type ErrInvalidCoin struct { + Got string + Negative bool + Nil bool + Empty bool +} + +func (e ErrInvalidCoin) Error() string { + return fmt.Sprintf( + "invalid coin: denom: %s, is negative: %v, is nil: %v, is empty: %v", + e.Got, + e.Negative, + e.Nil, + e.Empty, + ) +} + +type ErrInvalidAmount struct { + Got string +} + +func (e ErrInvalidAmount) Error() string { + return fmt.Sprintf("invalid token amount: %s", e.Got) +} + +type ErrInsufficientBalance struct { + Requested string + Got string +} + +func (e ErrInsufficientBalance) Error() string { + return fmt.Sprintf("insufficient balance: requested %s, current %s", e.Requested, e.Got) +} + +/* + Method related errors */ + type ErrInvalidMethod struct { Method string } @@ -42,3 +93,12 @@ type ErrInvalidMethod struct { func (e ErrInvalidMethod) Error() string { return fmt.Sprintf("invalid method: %s", e.Method) } + +type ErrUnexpected struct { + When string + Got string +} + +func (e ErrUnexpected) Error() string { + return fmt.Sprintf("unexpected error in %s: %s", e.When, e.Got) +} diff --git a/precompiles/types/errors_test.go b/precompiles/types/errors_test.go index 5693a450eb..c80647a08d 100644 --- a/precompiles/types/errors_test.go +++ b/precompiles/types/errors_test.go @@ -1,16 +1,22 @@ package types -import "testing" +import ( + "testing" + + "github.com/stretchr/testify/require" +) func Test_ErrInvalidAddr(t *testing.T) { e := ErrInvalidAddr{ - Got: "foo", + Got: "foo", + Reason: "bar", } got := e.Error() - expect := "invalid address foo" + expect := "invalid address foo, reason: bar" if got != expect { t.Errorf("Expected %v, got %v", expect, got) } + require.ErrorIs(t, ErrInvalidAddr{"foo", "bar"}, e) } func Test_ErrInvalidNumberOfArgs(t *testing.T) { @@ -23,6 +29,7 @@ func Test_ErrInvalidNumberOfArgs(t *testing.T) { if got != expect { t.Errorf("Expected %v, got %v", expect, got) } + require.ErrorIs(t, ErrInvalidNumberOfArgs{1, 2}, e) } func Test_ErrInvalidArgument(t *testing.T) { @@ -34,6 +41,7 @@ func Test_ErrInvalidArgument(t *testing.T) { if got != expect { t.Errorf("Expected %v, got %v", expect, got) } + require.ErrorIs(t, ErrInvalidArgument{"foo"}, e) } func Test_ErrInvalidMethod(t *testing.T) { @@ -45,4 +53,71 @@ func Test_ErrInvalidMethod(t *testing.T) { if got != expect { t.Errorf("Expected %v, got %v", expect, got) } + require.ErrorIs(t, ErrInvalidMethod{"foo"}, e) +} + +func Test_ErrInvalidCoin(t *testing.T) { + e := ErrInvalidCoin{ + Got: "foo", + Negative: true, + Nil: false, + Empty: false, + } + got := e.Error() + expect := "invalid coin: denom: foo, is negative: true, is nil: false, is empty: false" + if got != expect { + t.Errorf("Expected %v, got %v", expect, got) + } + require.ErrorIs(t, ErrInvalidCoin{"foo", true, false, false}, e) +} + +func Test_ErrInvalidAmount(t *testing.T) { + e := ErrInvalidAmount{ + Got: "foo", + } + got := e.Error() + expect := "invalid token amount: foo" + if got != expect { + t.Errorf("Expected %v, got %v", expect, got) + } + require.ErrorIs(t, ErrInvalidAmount{"foo"}, e) +} + +func Test_ErrUnexpected(t *testing.T) { + e := ErrUnexpected{ + When: "foo", + Got: "bar", + } + got := e.Error() + expect := "unexpected error in foo: bar" + if got != expect { + t.Errorf("Expected %v, got %v", expect, got) + } + require.ErrorIs(t, ErrUnexpected{"foo", "bar"}, e) +} + +func Test_ErrInsufficientBalance(t *testing.T) { + e := ErrInsufficientBalance{ + Requested: "foo", + Got: "bar", + } + got := e.Error() + expect := "insufficient balance: requested foo, current bar" + if got != expect { + t.Errorf("Expected %v, got %v", expect, got) + } + require.ErrorIs(t, ErrInsufficientBalance{"foo", "bar"}, e) +} + +func Test_ErrInvalidToken(t *testing.T) { + e := ErrInvalidToken{ + Got: "foo", + Reason: "bar", + } + got := e.Error() + expect := "invalid token foo: bar" + if got != expect { + t.Errorf("Expected %v, got %v", expect, got) + } + require.ErrorIs(t, ErrInvalidToken{"foo", "bar"}, e) } diff --git a/precompiles/types/types.go b/precompiles/types/types.go index 7bd4a57bfa..fd153d43b2 100644 --- a/precompiles/types/types.go +++ b/precompiles/types/types.go @@ -4,9 +4,12 @@ import ( "math/big" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" "github.com/zeta-chain/ethermint/x/evm/statedb" + + fungiblekeeper "github.com/zeta-chain/node/x/fungible/keeper" ) // Interface compliance. @@ -30,8 +33,19 @@ type Registrable interface { RegistryKey() common.Address } +type ContractCaller interface { + CallContract( + ctx sdk.Context, + fungibleKeeper *fungiblekeeper.Keeper, + abi *abi.ABI, + dst common.Address, + method string, + args []interface{}) ([]interface{}, error) +} + type BaseContract interface { Registrable + ContractCaller } // A baseContract implements Registrable and BaseContract interfaces. @@ -52,3 +66,53 @@ func (c *baseContract) RegistryKey() common.Address { func BytesToBigInt(data []byte) *big.Int { return big.NewInt(0).SetBytes(data[:]) } + +// CallContract calls a contract method on behalf of a precompiled contract. +// - noEtherumTxEvent is set to true because we don't want to emit EthereumTxEvent, +// as any MsgEthereumTx with more than one ethereum_tx will fail and the receipt +// won't be able to be retrieved. +// - from is set always to the precompiled contract address. +func (c *baseContract) CallContract( + ctx sdk.Context, + fungibleKeeper *fungiblekeeper.Keeper, + abi *abi.ABI, + dst common.Address, + method string, + args []interface{}, +) ([]interface{}, error) { + res, err := fungibleKeeper.CallEVM( + ctx, // ctx + *abi, // abi + c.RegistryKey(), // from + dst, // to + big.NewInt(0), // value + nil, // gasLimit + true, // commit + true, // noEthereumTxEvent + method, // method + args..., // args + ) + if err != nil { + return nil, &ErrUnexpected{ + When: "CallEVM " + method, + Got: err.Error(), + } + } + + if res.VmError != "" { + return nil, &ErrUnexpected{ + When: "VmError " + method, + Got: res.VmError, + } + } + + ret, err := abi.Methods[method].Outputs.Unpack(res.Ret) + if err != nil { + return nil, &ErrUnexpected{ + When: "Unpack " + method, + Got: err.Error(), + } + } + + return ret, nil +}