From 90a86d2ae8b01297cd1154c84b5f47312c7fd2b6 Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Mon, 7 Oct 2024 17:33:37 +0200 Subject: [PATCH 01/10] feat: add lockZRC20 capability to fungible keeper --- precompiles/bank/method_deposit.go | 75 ++++++-------- precompiles/bank/method_test.go | 12 +-- x/fungible/keeper/lock_zrc20_bank.go | 144 +++++++++++++++++++++++++++ 3 files changed, 176 insertions(+), 55 deletions(-) create mode 100644 x/fungible/keeper/lock_zrc20_bank.go diff --git a/precompiles/bank/method_deposit.go b/precompiles/bank/method_deposit.go index 2f57944723..9eba3c94e4 100644 --- a/precompiles/bank/method_deposit.go +++ b/precompiles/bank/method_deposit.go @@ -107,33 +107,33 @@ func (c *Contract) deposit( // 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 { - return nil, &ptypes.ErrUnexpected{ - Got: "ZRC20 allowance returned an unexpected type", - } - } - - if allowance.Cmp(amount) < 0 || allowance.Cmp(big.NewInt(0)) <= 0 { - return nil, &ptypes.ErrInvalidAmount{ - Got: allowance.String(), - } - } + // 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 { + // return nil, &ptypes.ErrUnexpected{ + // Got: "ZRC20 allowance returned an unexpected type", + // } + // } + + // if 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, @@ -146,30 +146,13 @@ func (c *Contract) deposit( } // 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 { + if err := c.fungibleKeeper.LockZRC20InBank(ctx, c.zrc20ABI, zrc20Addr, caller, amount); err != nil { return nil, &ptypes.ErrUnexpected{ - When: "transferFrom", + When: "LockZRC20InBank", 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. if err := c.bankKeeper.MintCoins(ctx, types.ModuleName, coinSet); err != nil { return nil, &ptypes.ErrUnexpected{ diff --git a/precompiles/bank/method_test.go b/precompiles/bank/method_test.go index 912c41b40c..9ce21732ae 100644 --- a/precompiles/bank/method_test.go +++ b/precompiles/bank/method_test.go @@ -137,8 +137,8 @@ func Test_Methods(t *testing.T) { require.Error(t, err) require.ErrorAs( t, - ptypes.ErrInvalidAmount{ - Got: "0", + ptypes.ErrInvalidCoin{ + Empty: true, }, err, ) @@ -173,13 +173,7 @@ func Test_Methods(t *testing.T) { success, err := ts.bankContract.Run(ts.mockEVM, ts.mockVMContract, false) require.Error(t, err) - require.ErrorAs( - t, - ptypes.ErrInvalidAmount{ - Got: "500", - }, - err, - ) + require.Contains(t, err.Error(), "unexpected error in LockZRC20InBank: invalid allowance, got: 500") res, err := ts.bankABI.Methods[DepositMethodName].Outputs.Unpack(success) require.NoError(t, err) diff --git a/x/fungible/keeper/lock_zrc20_bank.go b/x/fungible/keeper/lock_zrc20_bank.go new file mode 100644 index 0000000000..e2f745d544 --- /dev/null +++ b/x/fungible/keeper/lock_zrc20_bank.go @@ -0,0 +1,144 @@ +package keeper + +import ( + "fmt" + "math/big" + + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" +) + +const ( + transferFrom = "transferFrom" + allowance = "allowance" +) + +var ( + bankAddress common.Address = common.HexToAddress("0x0000000000000000000000000000000000000067") + zeroAddress common.Address = common.HexToAddress("0x0000000000000000000000000000000000000000") +) + +// LockZRC20InBank locks ZRC20 tokens in the bank contract. +// The caller must have approved the bank contract to spend the amount of ZRC20 tokens. +func (k Keeper) LockZRC20InBank( + ctx sdk.Context, + zrc20ABI *abi.ABI, + zrc20Address, from common.Address, + amount *big.Int, +) error { + accAddress := sdk.AccAddress(bankAddress.Bytes()) + if k.GetAuthKeeper().GetAccount(ctx, accAddress) == nil { + k.GetAuthKeeper().SetAccount(ctx, authtypes.NewBaseAccount(accAddress, nil, 0, 0)) + } + + if amount.Sign() <= 0 { + return fmt.Errorf("amount must be positive") + } + + if from == zeroAddress { + return fmt.Errorf("from address cannot be zero") + } + + if zrc20Address == zeroAddress { + return fmt.Errorf("zrc20 address cannot be zero") + } + + if err := k.checkBankAllowance(ctx, zrc20ABI, from, zrc20Address, amount); err != nil { + return err + } + + args := []interface{}{from, bankAddress, amount} + res, err := k.CallEVM( + ctx, + *zrc20ABI, + bankAddress, + zrc20Address, + big.NewInt(0), + nil, + true, + true, + transferFrom, + args..., + ) + if err != nil { + return err + } + + if res.VmError != "" { + return fmt.Errorf("%s", res.VmError) + } + + ret, err := zrc20ABI.Methods[transferFrom].Outputs.Unpack(res.Ret) + if err != nil { + return err + } + + transferred, ok := ret[0].(bool) + if !ok { + return fmt.Errorf("transferFrom returned an unexpected value") + } + + if !transferred { + return fmt.Errorf("transferFrom not successful") + } + + return nil +} + +func (k Keeper) checkBankAllowance( + ctx sdk.Context, + zrc20ABI *abi.ABI, + from, zrc20Address common.Address, + amount *big.Int, +) error { + if amount.Sign() <= 0 { + return fmt.Errorf("amount must be positive") + } + + if from == zeroAddress { + return fmt.Errorf("from address cannot be zero") + } + + if zrc20Address == zeroAddress { + return fmt.Errorf("zrc20 address cannot be zero") + } + + args := []interface{}{from, bankAddress} + res, err := k.CallEVM( + ctx, + *zrc20ABI, + bankAddress, + zrc20Address, + big.NewInt(0), + nil, + true, + true, + allowance, + args..., + ) + if err != nil { + return err + } + + if res.VmError != "" { + return fmt.Errorf("%s", res.VmError) + } + + ret, err := zrc20ABI.Methods[allowance].Outputs.Unpack(res.Ret) + if err != nil { + return err + } + + allowance, ok := ret[0].(*big.Int) + if !ok { + return fmt.Errorf("ZRC20 allowance returned an unexpected type") + } + + if allowance.Cmp(amount) < 0 || allowance.Cmp(big.NewInt(0)) <= 0 { + return fmt.Errorf("invalid allowance, got: %s", allowance.String()) + } + + return nil +} From eb7873cdf30cdea9902494823c8648c6d6be85fe Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Mon, 7 Oct 2024 19:03:50 +0200 Subject: [PATCH 02/10] extract functions to fungible --- precompiles/bank/method_deposit.go | 48 +----- precompiles/bank/method_withdraw.go | 72 ++------- x/fungible/keeper/lock_zrc20_bank.go | 144 ------------------ x/fungible/keeper/zrc20_check_allowance.go | 72 +++++++++ x/fungible/keeper/zrc20_check_balance.go | 67 ++++++++ .../keeper/zrc20_check_token_validity.go | 26 ++++ x/fungible/keeper/zrc20_lock_token.go | 82 ++++++++++ x/fungible/keeper/zrc20_unlock_token.go | 71 +++++++++ 8 files changed, 328 insertions(+), 254 deletions(-) delete mode 100644 x/fungible/keeper/lock_zrc20_bank.go create mode 100644 x/fungible/keeper/zrc20_check_allowance.go create mode 100644 x/fungible/keeper/zrc20_check_balance.go create mode 100644 x/fungible/keeper/zrc20_check_token_validity.go create mode 100644 x/fungible/keeper/zrc20_lock_token.go create mode 100644 x/fungible/keeper/zrc20_unlock_token.go diff --git a/precompiles/bank/method_deposit.go b/precompiles/bank/method_deposit.go index 9eba3c94e4..37fdd53da4 100644 --- a/precompiles/bank/method_deposit.go +++ b/precompiles/bank/method_deposit.go @@ -59,22 +59,6 @@ func (c *Contract) deposit( 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 { - return nil, &ptypes.ErrInvalidToken{ - Got: zrc20Addr.String(), - Reason: "token is not a whitelisted ZRC20", - } - } - - if t.Paused { - return nil, &ptypes.ErrInvalidToken{ - Got: zrc20Addr.String(), - Reason: "token is paused", - } - } - // Check for enough balance. // function balanceOf(address account) public view virtual override returns (uint256) resBalanceOf, err := c.CallContract( @@ -105,36 +89,6 @@ func (c *Contract) deposit( } } - // 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 { - // return nil, &ptypes.ErrUnexpected{ - // Got: "ZRC20 allowance returned an unexpected type", - // } - // } - - // if 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". @@ -146,7 +100,7 @@ func (c *Contract) deposit( } // 2. Effect: subtract balance. - if err := c.fungibleKeeper.LockZRC20InBank(ctx, c.zrc20ABI, zrc20Addr, caller, amount); err != nil { + if err := c.fungibleKeeper.LockZRC20(ctx, c.zrc20ABI, zrc20Addr, caller, amount); err != nil { return nil, &ptypes.ErrUnexpected{ When: "LockZRC20InBank", Got: err.Error(), diff --git a/precompiles/bank/method_withdraw.go b/precompiles/bank/method_withdraw.go index 99cb4f763c..66df0ca05e 100644 --- a/precompiles/bank/method_withdraw.go +++ b/precompiles/bank/method_withdraw.go @@ -56,18 +56,10 @@ func (c *Contract) withdraw( } // Safety check: token has to be a valid whitelisted ZRC20 and not be paused. - t, found := c.fungibleKeeper.GetForeignCoins(ctx, zrc20Addr.String()) - if !found { + if err := c.fungibleKeeper.IsValidZRC20(ctx, zrc20Addr); err != nil { return nil, &ptypes.ErrInvalidToken{ Got: zrc20Addr.String(), - Reason: "token is not a whitelisted ZRC20", - } - } - - if t.Paused { - return nil, &ptypes.ErrInvalidToken{ - Got: zrc20Addr.String(), - Reason: "token is paused", + Reason: err.Error(), } } @@ -92,36 +84,14 @@ func (c *Contract) withdraw( 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 { + // Check if fungible address has enough ZRC20 balance. + if err := c.fungibleKeeper.CheckFungibleZRC20Balance(ctx, c.zrc20ABI, zrc20Addr, amount); err != nil { return nil, &ptypes.ErrUnexpected{ When: "balanceOf", Got: err.Error(), } } - balance, ok := resBalanceOf[0].(*big.Int) - if !ok { - return nil, &ptypes.ErrUnexpected{ - Got: "ZRC20 balanceOf returned an unexpected type", - } - } - - if balance.Cmp(amount) == -1 { - return nil, &ptypes.ErrInvalidAmount{ - Got: balance.String(), - } - } - // 2. Effect: burn cosmos coin balance. if err := c.bankKeeper.SendCoinsFromAccountToModule(ctx, fromAddr, types.ModuleName, coinSet); err != nil { return nil, &ptypes.ErrUnexpected{ @@ -137,45 +107,21 @@ func (c *Contract) withdraw( } } - if err := c.addEventLog(ctx, evm.StateDB, WithdrawEventName, eventData{caller, zrc20Addr, fromAddr.String(), coinSet.Denoms()[0], amount}); err != nil { + // 3. Interactions: send ZRC20. + if err := c.fungibleKeeper.UnlockZRC20(ctx, c.zrc20ABI, zrc20Addr, caller, amount); err != nil { return nil, &ptypes.ErrUnexpected{ - When: "AddWithdrawLog", + When: "UnlockZRC20InBank", Got: err.Error(), } } - // 3. Interactions: send to module and burn. - - // function transfer(address recipient, uint256 amount) public virtual override returns (bool) - resTransfer, err := c.CallContract( - ctx, - &c.fungibleKeeper, - c.zrc20ABI, - zrc20Addr, - "transfer", - []interface{}{caller /* sender */, amount}, - ) - if err != nil { + if err := c.addEventLog(ctx, evm.StateDB, WithdrawEventName, eventData{caller, zrc20Addr, fromAddr.String(), coinSet.Denoms()[0], amount}); err != nil { return nil, &ptypes.ErrUnexpected{ - When: "transfer", + When: "AddWithdrawLog", Got: err.Error(), } } - transferred, ok := resTransfer[0].(bool) - if !ok { - return nil, &ptypes.ErrUnexpected{ - Got: "ZRC20 transfer returned an unexpected type", - } - } - - if !transferred { - return nil, &ptypes.ErrUnexpected{ - When: "transfer", - Got: "transaction not successful", - } - } - return method.Outputs.Pack(true) } diff --git a/x/fungible/keeper/lock_zrc20_bank.go b/x/fungible/keeper/lock_zrc20_bank.go deleted file mode 100644 index e2f745d544..0000000000 --- a/x/fungible/keeper/lock_zrc20_bank.go +++ /dev/null @@ -1,144 +0,0 @@ -package keeper - -import ( - "fmt" - "math/big" - - sdk "github.com/cosmos/cosmos-sdk/types" - authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" - "github.com/ethereum/go-ethereum/accounts/abi" - "github.com/ethereum/go-ethereum/common" -) - -const ( - transferFrom = "transferFrom" - allowance = "allowance" -) - -var ( - bankAddress common.Address = common.HexToAddress("0x0000000000000000000000000000000000000067") - zeroAddress common.Address = common.HexToAddress("0x0000000000000000000000000000000000000000") -) - -// LockZRC20InBank locks ZRC20 tokens in the bank contract. -// The caller must have approved the bank contract to spend the amount of ZRC20 tokens. -func (k Keeper) LockZRC20InBank( - ctx sdk.Context, - zrc20ABI *abi.ABI, - zrc20Address, from common.Address, - amount *big.Int, -) error { - accAddress := sdk.AccAddress(bankAddress.Bytes()) - if k.GetAuthKeeper().GetAccount(ctx, accAddress) == nil { - k.GetAuthKeeper().SetAccount(ctx, authtypes.NewBaseAccount(accAddress, nil, 0, 0)) - } - - if amount.Sign() <= 0 { - return fmt.Errorf("amount must be positive") - } - - if from == zeroAddress { - return fmt.Errorf("from address cannot be zero") - } - - if zrc20Address == zeroAddress { - return fmt.Errorf("zrc20 address cannot be zero") - } - - if err := k.checkBankAllowance(ctx, zrc20ABI, from, zrc20Address, amount); err != nil { - return err - } - - args := []interface{}{from, bankAddress, amount} - res, err := k.CallEVM( - ctx, - *zrc20ABI, - bankAddress, - zrc20Address, - big.NewInt(0), - nil, - true, - true, - transferFrom, - args..., - ) - if err != nil { - return err - } - - if res.VmError != "" { - return fmt.Errorf("%s", res.VmError) - } - - ret, err := zrc20ABI.Methods[transferFrom].Outputs.Unpack(res.Ret) - if err != nil { - return err - } - - transferred, ok := ret[0].(bool) - if !ok { - return fmt.Errorf("transferFrom returned an unexpected value") - } - - if !transferred { - return fmt.Errorf("transferFrom not successful") - } - - return nil -} - -func (k Keeper) checkBankAllowance( - ctx sdk.Context, - zrc20ABI *abi.ABI, - from, zrc20Address common.Address, - amount *big.Int, -) error { - if amount.Sign() <= 0 { - return fmt.Errorf("amount must be positive") - } - - if from == zeroAddress { - return fmt.Errorf("from address cannot be zero") - } - - if zrc20Address == zeroAddress { - return fmt.Errorf("zrc20 address cannot be zero") - } - - args := []interface{}{from, bankAddress} - res, err := k.CallEVM( - ctx, - *zrc20ABI, - bankAddress, - zrc20Address, - big.NewInt(0), - nil, - true, - true, - allowance, - args..., - ) - if err != nil { - return err - } - - if res.VmError != "" { - return fmt.Errorf("%s", res.VmError) - } - - ret, err := zrc20ABI.Methods[allowance].Outputs.Unpack(res.Ret) - if err != nil { - return err - } - - allowance, ok := ret[0].(*big.Int) - if !ok { - return fmt.Errorf("ZRC20 allowance returned an unexpected type") - } - - if allowance.Cmp(amount) < 0 || allowance.Cmp(big.NewInt(0)) <= 0 { - return fmt.Errorf("invalid allowance, got: %s", allowance.String()) - } - - return nil -} diff --git a/x/fungible/keeper/zrc20_check_allowance.go b/x/fungible/keeper/zrc20_check_allowance.go new file mode 100644 index 0000000000..19a9846817 --- /dev/null +++ b/x/fungible/keeper/zrc20_check_allowance.go @@ -0,0 +1,72 @@ +package keeper + +import ( + "fmt" + "math/big" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + + fungibletypes "github.com/zeta-chain/node/x/fungible/types" +) + +const allowance = "allowance" + +// CheckFungibleZRC20Allowance checks if the allowance of ZRC20 tokens, +// is equal or greater than the provided amount. +func (k Keeper) CheckFungibleZRC20Allowance( + ctx sdk.Context, + zrc20ABI *abi.ABI, + from, zrc20Address common.Address, + amount *big.Int, +) error { + if amount.Sign() <= 0 { + return fmt.Errorf("amount must be positive, got: %s", amount.String()) + } + + if from == zeroAddress { + return fmt.Errorf("from address cannot be zero") + } + + if zrc20Address == zeroAddress { + return fmt.Errorf("zrc20 address cannot be zero") + } + + args := []interface{}{from, fungibletypes.ModuleAddressEVM} + res, err := k.CallEVM( + ctx, + *zrc20ABI, + fungibletypes.ModuleAddressEVM, + zrc20Address, + big.NewInt(0), + nil, + true, + true, + allowance, + args..., + ) + if err != nil { + return err + } + + if res.VmError != "" { + return fmt.Errorf("%s", res.VmError) + } + + ret, err := zrc20ABI.Methods[allowance].Outputs.Unpack(res.Ret) + if err != nil { + return err + } + + allowance, ok := ret[0].(*big.Int) + if !ok { + return fmt.Errorf("ZRC20 allowance returned an unexpected type") + } + + if allowance.Cmp(amount) < 0 || allowance.Cmp(big.NewInt(0)) <= 0 { + return fmt.Errorf("invalid allowance, got: %s", allowance.String()) + } + + return nil +} diff --git a/x/fungible/keeper/zrc20_check_balance.go b/x/fungible/keeper/zrc20_check_balance.go new file mode 100644 index 0000000000..a9f0272c99 --- /dev/null +++ b/x/fungible/keeper/zrc20_check_balance.go @@ -0,0 +1,67 @@ +package keeper + +import ( + "fmt" + "math/big" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + + fungibletypes "github.com/zeta-chain/node/x/fungible/types" +) + +const balanceOf = "balanceOf" + +// CheckFungibleZRC20Balance checks if the balance of ZRC20 tokens, +// is equal or greater than the provided amount. +func (k Keeper) CheckFungibleZRC20Balance( + ctx sdk.Context, + zrc20ABI *abi.ABI, + zrc20Address common.Address, + amount *big.Int, +) error { + if amount.Sign() <= 0 { + return fmt.Errorf("amount must be positive, got: %s", amount.String()) + } + + if zrc20Address == zeroAddress { + return fmt.Errorf("zrc20 address cannot be zero") + } + + res, err := k.CallEVM( + ctx, + *zrc20ABI, + fungibletypes.ModuleAddressEVM, + zrc20Address, + big.NewInt(0), + nil, + true, + true, + balanceOf, + fungibletypes.ModuleAddressEVM, + ) + if err != nil { + return err + } + + if res.VmError != "" { + return fmt.Errorf("%s", res.VmError) + } + + ret, err := zrc20ABI.Methods[balanceOf].Outputs.Unpack(res.Ret) + if err != nil { + return err + } + + balance, ok := ret[0].(*big.Int) + if !ok { + return fmt.Errorf("ZRC20 balanceOf returned an unexpected type") + } + + if balance.Cmp(amount) == -1 { + return fmt.Errorf("invalid balance, got: %s", balance.String()) + } + + return nil +} diff --git a/x/fungible/keeper/zrc20_check_token_validity.go b/x/fungible/keeper/zrc20_check_token_validity.go new file mode 100644 index 0000000000..bd4bf21c5b --- /dev/null +++ b/x/fungible/keeper/zrc20_check_token_validity.go @@ -0,0 +1,26 @@ +package keeper + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" +) + +// IsValidZRC20 returns an error whenever a ZRC20 is not whitelisted or paused. +func (k Keeper) IsValidZRC20(ctx sdk.Context, zrc20Address common.Address) error { + if zrc20Address == zeroAddress { + return fmt.Errorf("zrc20 address cannot be zero") + } + + t, found := k.GetForeignCoins(ctx, zrc20Address.String()) + if !found { + return fmt.Errorf("ZRC20 is not whitelisted, address: %s", zrc20Address.String()) + } + + if t.Paused { + return fmt.Errorf("ZRC20 is paused, address: %s", zrc20Address.String()) + } + + return nil +} diff --git a/x/fungible/keeper/zrc20_lock_token.go b/x/fungible/keeper/zrc20_lock_token.go new file mode 100644 index 0000000000..8d79843ef6 --- /dev/null +++ b/x/fungible/keeper/zrc20_lock_token.go @@ -0,0 +1,82 @@ +package keeper + +import ( + "fmt" + "math/big" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + + fungibletypes "github.com/zeta-chain/node/x/fungible/types" +) + +const transferFrom = "transferFrom" + +var zeroAddress common.Address = common.HexToAddress("0x0000000000000000000000000000000000000000") + +// LockZRC20 locks ZRC20 tokens in the bank contract. +// The caller must have approved the bank contract to spend the amount of ZRC20 tokens. +func (k Keeper) LockZRC20( + ctx sdk.Context, + zrc20ABI *abi.ABI, + zrc20Address, from common.Address, + amount *big.Int, +) error { + if amount.Sign() <= 0 { + return fmt.Errorf("amount must be positive, got: %s", amount.String()) + } + + if from == zeroAddress { + return fmt.Errorf("from address cannot be zero") + } + + if zrc20Address == zeroAddress { + return fmt.Errorf("zrc20 address cannot be zero") + } + + if err := k.IsValidZRC20(ctx, zrc20Address); err != nil { + return err + } + + if err := k.CheckFungibleZRC20Allowance(ctx, zrc20ABI, from, zrc20Address, amount); err != nil { + return err + } + + args := []interface{}{from, fungibletypes.ModuleAddressEVM, amount} + res, err := k.CallEVM( + ctx, + *zrc20ABI, + fungibletypes.ModuleAddressEVM, + zrc20Address, + big.NewInt(0), + nil, + true, + true, + transferFrom, + args..., + ) + if err != nil { + return err + } + + if res.VmError != "" { + return fmt.Errorf("%s", res.VmError) + } + + ret, err := zrc20ABI.Methods[transferFrom].Outputs.Unpack(res.Ret) + if err != nil { + return err + } + + transferred, ok := ret[0].(bool) + if !ok { + return fmt.Errorf("transferFrom returned an unexpected value") + } + + if !transferred { + return fmt.Errorf("transferFrom not successful") + } + + return nil +} diff --git a/x/fungible/keeper/zrc20_unlock_token.go b/x/fungible/keeper/zrc20_unlock_token.go new file mode 100644 index 0000000000..d932e1bcd7 --- /dev/null +++ b/x/fungible/keeper/zrc20_unlock_token.go @@ -0,0 +1,71 @@ +package keeper + +import ( + "fmt" + "math/big" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + + fungibletypes "github.com/zeta-chain/node/x/fungible/types" +) + +const transfer = "transfer" + +// UnlockZRC20 unlocks ZRC20 tokens and sends them to the "to" address. +func (k Keeper) UnlockZRC20( + ctx sdk.Context, + zrc20ABI *abi.ABI, + zrc20Address, to common.Address, + amount *big.Int, +) error { + if amount.Sign() <= 0 { + return fmt.Errorf("amount must be positive, got: %s", amount.String()) + } + + if to == zeroAddress { + return fmt.Errorf("from address cannot be zero") + } + + if zrc20Address == zeroAddress { + return fmt.Errorf("zrc20 address cannot be zero") + } + + args := []interface{}{to, amount} + res, err := k.CallEVM( + ctx, + *zrc20ABI, + fungibletypes.ModuleAddressEVM, + zrc20Address, + big.NewInt(0), + nil, + true, + true, + transfer, + args..., + ) + if err != nil { + return err + } + + if res.VmError != "" { + return fmt.Errorf("%s", res.VmError) + } + + ret, err := zrc20ABI.Methods[transferFrom].Outputs.Unpack(res.Ret) + if err != nil { + return err + } + + transferred, ok := ret[0].(bool) + if !ok { + return fmt.Errorf("transfer returned an unexpected value") + } + + if !transferred { + return fmt.Errorf("transfer not successful") + } + + return nil +} From 17cdcaba5ca59f3fa16e344caae03e5b76b4fb3c Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Mon, 7 Oct 2024 22:46:21 +0200 Subject: [PATCH 03/10] cleaning up --- precompiles/bank/method_withdraw.go | 2 +- x/fungible/keeper/zrc20_unlock_token.go | 8 ++++++++ x/fungible/types/keys.go | 8 +------- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/precompiles/bank/method_withdraw.go b/precompiles/bank/method_withdraw.go index 66df0ca05e..e0893b08c2 100644 --- a/precompiles/bank/method_withdraw.go +++ b/precompiles/bank/method_withdraw.go @@ -55,7 +55,7 @@ func (c *Contract) withdraw( return nil, err } - // Safety check: token has to be a valid whitelisted ZRC20 and not be paused. + // Safety check: token has to be a non-paused whitelisted ZRC20. if err := c.fungibleKeeper.IsValidZRC20(ctx, zrc20Addr); err != nil { return nil, &ptypes.ErrInvalidToken{ Got: zrc20Addr.String(), diff --git a/x/fungible/keeper/zrc20_unlock_token.go b/x/fungible/keeper/zrc20_unlock_token.go index d932e1bcd7..9d6243b2da 100644 --- a/x/fungible/keeper/zrc20_unlock_token.go +++ b/x/fungible/keeper/zrc20_unlock_token.go @@ -32,6 +32,14 @@ func (k Keeper) UnlockZRC20( return fmt.Errorf("zrc20 address cannot be zero") } + if err := k.IsValidZRC20(ctx, zrc20Address); err != nil { + return err + } + + if err := k.CheckFungibleZRC20Balance(ctx, zrc20ABI, zrc20Address, amount); err != nil { + return err + } + args := []interface{}{to, amount} res, err := k.CallEVM( ctx, diff --git a/x/fungible/types/keys.go b/x/fungible/types/keys.go index 777cfd7c41..7e81328c15 100644 --- a/x/fungible/types/keys.go +++ b/x/fungible/types/keys.go @@ -27,17 +27,11 @@ func KeyPrefix(p string) []byte { } var ( - ModuleAddress = authtypes.NewModuleAddress(ModuleName) - //ModuleAddressEVM common.EVMAddress + ModuleAddress = authtypes.NewModuleAddress(ModuleName) ModuleAddressEVM = common.BytesToAddress(ModuleAddress.Bytes()) AdminAddress = "zeta1rx9r8hff0adaqhr5tuadkzj4e7ns2ntg446vtt" ) -func init() { - //fmt.Printf("ModuleAddressEVM of %s: %s\n", ModuleName, ModuleAddressEVM.String()) - // 0x735b14BB79463307AAcBED86DAf3322B1e6226aB -} - const ( SystemContractKey = "SystemContract-value-" ) From d5190a5485f9482cb3444f5dee3cb33b4d9b83ab Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Tue, 8 Oct 2024 10:28:21 +0200 Subject: [PATCH 04/10] modify e2e tests --- changelog.md | 1 + e2e/e2etests/test_precompiles_bank.go | 15 +++++++------- .../test_precompiles_bank_through_contract.go | 19 +++++++++--------- precompiles/bank/method_deposit.go | 2 +- precompiles/bank/method_test.go | 20 +++++++++---------- x/fungible/keeper/zrc20_check_allowance.go | 4 ++-- x/fungible/keeper/zrc20_check_balance.go | 4 ++-- x/fungible/keeper/zrc20_lock_token.go | 4 ++-- x/fungible/keeper/zrc20_unlock_token.go | 4 ++-- x/fungible/types/keys.go | 5 ++++- 10 files changed, 42 insertions(+), 36 deletions(-) diff --git a/changelog.md b/changelog.md index 78b9ab0b59..1f49f12ecb 100644 --- a/changelog.md +++ b/changelog.md @@ -17,6 +17,7 @@ * [2904](https://github.com/zeta-chain/node/pull/2904) - integrate authenticated calls smart contract functionality into protocol * [2919](https://github.com/zeta-chain/node/pull/2919) - add inbound sender to revert context * [2957](https://github.com/zeta-chain/node/pull/2957) - enable Bitcoin inscription support on testnet +* [2979](https://github.com/zeta-chain/node/pull/2979) - add fungible keeper ability to lock/unlock ZRC20 tokens ### Refactor diff --git a/e2e/e2etests/test_precompiles_bank.go b/e2e/e2etests/test_precompiles_bank.go index 2517410339..1afd52d9cf 100644 --- a/e2e/e2etests/test_precompiles_bank.go +++ b/e2e/e2etests/test_precompiles_bank.go @@ -10,6 +10,7 @@ import ( "github.com/zeta-chain/node/e2e/runner" "github.com/zeta-chain/node/e2e/utils" "github.com/zeta-chain/node/precompiles/bank" + fungibletypes "github.com/zeta-chain/node/x/fungible/types" ) func TestPrecompilesBank(r *runner.E2ERunner, args []string) { @@ -29,7 +30,7 @@ func TestPrecompilesBank(r *runner.E2ERunner, args []string) { // 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)) + tx, err := r.ERC20ZRC20.Approve(r.ZEVMAuth, fungibletypes.ModuleAddressZEVM, 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") @@ -59,7 +60,7 @@ func TestPrecompilesBank(r *runner.E2ERunner, args []string) { 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) + tx, err := r.ERC20ZRC20.Approve(r.ZEVMAuth, fungibletypes.ModuleAddressZEVM, 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") @@ -72,7 +73,7 @@ func TestPrecompilesBank(r *runner.E2ERunner, args []string) { 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)) + tx, err = r.ERC20ZRC20.Approve(r.ZEVMAuth, fungibletypes.ModuleAddressZEVM, 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") @@ -103,7 +104,7 @@ func TestPrecompilesBank(r *runner.E2ERunner, args []string) { 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) + bankZRC20Balance, err := r.ERC20ZRC20.BalanceOf(&bind.CallOpts{Context: r.Ctx}, fungibletypes.ModuleAddressZEVM) require.NoError(r, err, "Call ERC20ZRC20.BalanceOf") require.Equal(r, uint64(500), bankZRC20Balance.Uint64(), "bank ERC20ZRC20 balance should be 500") @@ -115,7 +116,7 @@ func TestPrecompilesBank(r *runner.E2ERunner, args []string) { // 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) + bankZRC20Balance, err = r.ERC20ZRC20.BalanceOf(&bind.CallOpts{Context: r.Ctx}, fungibletypes.ModuleAddressZEVM) require.NoError(r, err, "Call ERC20ZRC20.BalanceOf") require.Equal(r, uint64(500), bankZRC20Balance.Uint64(), "bank ERC20ZRC20 balance should be 500") @@ -143,7 +144,7 @@ func TestPrecompilesBank(r *runner.E2ERunner, args []string) { 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) + bankZRC20Balance, err = r.ERC20ZRC20.BalanceOf(&bind.CallOpts{Context: r.Ctx}, fungibletypes.ModuleAddressZEVM) require.NoError(r, err, "Call ERC20ZRC20.BalanceOf") require.Equal(r, uint64(0), bankZRC20Balance.Uint64(), "bank ERC20ZRC20 balance should be 0") } @@ -158,7 +159,7 @@ func TestPrecompilesBankNonZRC20(r *runner.E2ERunner, args []string) { r.ZEVMAuth.GasLimit = previousGasLimit }() - spender, bankAddr := r.EVMAddress(), bank.ContractAddress + spender, bankAddr := r.EVMAddress(), fungibletypes.ModuleAddressZEVM // Create a bank contract caller. bankContract, err := bank.NewIBank(bank.ContractAddress, r.ZEVMClient) diff --git a/e2e/e2etests/test_precompiles_bank_through_contract.go b/e2e/e2etests/test_precompiles_bank_through_contract.go index 480663284e..178db56c1d 100644 --- a/e2e/e2etests/test_precompiles_bank_through_contract.go +++ b/e2e/e2etests/test_precompiles_bank_through_contract.go @@ -12,6 +12,7 @@ import ( "github.com/zeta-chain/node/e2e/runner" "github.com/zeta-chain/node/e2e/utils" "github.com/zeta-chain/node/precompiles/bank" + fungibletypes "github.com/zeta-chain/node/x/fungible/types" ) func TestPrecompilesBankThroughContract(r *runner.E2ERunner, args []string) { @@ -59,7 +60,7 @@ func TestPrecompilesBankThroughContract(r *runner.E2ERunner, args []string) { // Check initial balances. balanceShouldBe(r, 0, checkCosmosBalance(r, testBank, zrc20Address, spender)) balanceShouldBe(r, 1000, checkZRC20Balance(r, spender)) - balanceShouldBe(r, 0, checkZRC20Balance(r, bank.ContractAddress)) + balanceShouldBe(r, 0, checkZRC20Balance(r, fungibletypes.ModuleAddressZEVM)) // Deposit without previous alllowance should fail. receipt = depositThroughTestBank(r, testBank, zrc20Address, oneThousand) @@ -68,10 +69,10 @@ func TestPrecompilesBankThroughContract(r *runner.E2ERunner, args []string) { // Check balances, should be the same. balanceShouldBe(r, 0, checkCosmosBalance(r, testBank, zrc20Address, spender)) balanceShouldBe(r, 1000, checkZRC20Balance(r, spender)) - balanceShouldBe(r, 0, checkZRC20Balance(r, bank.ContractAddress)) + balanceShouldBe(r, 0, checkZRC20Balance(r, fungibletypes.ModuleAddressZEVM)) // Allow 500 ZRC20 to bank precompile. - approveAllowance(r, bank.ContractAddress, fiveHundred) + approveAllowance(r, fungibletypes.ModuleAddressZEVM, fiveHundred) // Deposit 501 ERC20ZRC20 tokens to the bank contract, through TestBank. // It's higher than allowance but lower than balance, should fail. @@ -81,10 +82,10 @@ func TestPrecompilesBankThroughContract(r *runner.E2ERunner, args []string) { // Balances shouldn't change. balanceShouldBe(r, 0, checkCosmosBalance(r, testBank, zrc20Address, spender)) balanceShouldBe(r, 1000, checkZRC20Balance(r, spender)) - balanceShouldBe(r, 0, checkZRC20Balance(r, bank.ContractAddress)) + balanceShouldBe(r, 0, checkZRC20Balance(r, fungibletypes.ModuleAddressZEVM)) // Allow 1000 ZRC20 to bank precompile. - approveAllowance(r, bank.ContractAddress, oneThousand) + approveAllowance(r, fungibletypes.ModuleAddressZEVM, oneThousand) // Deposit 1001 ERC20ZRC20 tokens to the bank contract. // It's higher than spender balance but within approved allowance, should fail. @@ -94,7 +95,7 @@ func TestPrecompilesBankThroughContract(r *runner.E2ERunner, args []string) { // Balances shouldn't change. balanceShouldBe(r, 0, checkCosmosBalance(r, testBank, zrc20Address, spender)) balanceShouldBe(r, 1000, checkZRC20Balance(r, spender)) - balanceShouldBe(r, 0, checkZRC20Balance(r, bank.ContractAddress)) + balanceShouldBe(r, 0, checkZRC20Balance(r, fungibletypes.ModuleAddressZEVM)) // Deposit 500 ERC20ZRC20 tokens to the bank contract, it's within allowance and balance. Should pass. receipt = depositThroughTestBank(r, testBank, zrc20Address, fiveHundred) @@ -103,7 +104,7 @@ func TestPrecompilesBankThroughContract(r *runner.E2ERunner, args []string) { // Balances should be transferred. Bank now locks 500 ZRC20 tokens. balanceShouldBe(r, 500, checkCosmosBalance(r, testBank, zrc20Address, spender)) balanceShouldBe(r, 500, checkZRC20Balance(r, spender)) - balanceShouldBe(r, 500, checkZRC20Balance(r, bank.ContractAddress)) + balanceShouldBe(r, 500, checkZRC20Balance(r, fungibletypes.ModuleAddressZEVM)) // Check the deposit event. eventDeposit, err := bankPrecompileCaller.ParseDeposit(*receipt.Logs[0]) @@ -119,7 +120,7 @@ func TestPrecompilesBankThroughContract(r *runner.E2ERunner, args []string) { // Balances shouldn't change. balanceShouldBe(r, 500, checkCosmosBalance(r, testBank, zrc20Address, spender)) balanceShouldBe(r, 500, checkZRC20Balance(r, spender)) - balanceShouldBe(r, 500, checkZRC20Balance(r, bank.ContractAddress)) + balanceShouldBe(r, 500, checkZRC20Balance(r, fungibletypes.ModuleAddressZEVM)) // Try to withdraw 500 ERC20ZRC20 tokens. Should pass. receipt = withdrawThroughTestBank(r, testBank, zrc20Address, fiveHundred) @@ -128,7 +129,7 @@ func TestPrecompilesBankThroughContract(r *runner.E2ERunner, args []string) { // Balances should be reverted to initial state. balanceShouldBe(r, 0, checkCosmosBalance(r, testBank, zrc20Address, spender)) balanceShouldBe(r, 1000, checkZRC20Balance(r, spender)) - balanceShouldBe(r, 0, checkZRC20Balance(r, bank.ContractAddress)) + balanceShouldBe(r, 0, checkZRC20Balance(r, fungibletypes.ModuleAddressZEVM)) // Check the withdraw event. eventWithdraw, err := bankPrecompileCaller.ParseWithdraw(*receipt.Logs[0]) diff --git a/precompiles/bank/method_deposit.go b/precompiles/bank/method_deposit.go index 37fdd53da4..62cb59dd51 100644 --- a/precompiles/bank/method_deposit.go +++ b/precompiles/bank/method_deposit.go @@ -18,7 +18,7 @@ import ( // 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. +// - The caller has to allow the Fungible ZEVM address 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. diff --git a/precompiles/bank/method_test.go b/precompiles/bank/method_test.go index 9ce21732ae..b556c57720 100644 --- a/precompiles/bank/method_test.go +++ b/precompiles/bank/method_test.go @@ -28,7 +28,7 @@ import ( func Test_Methods(t *testing.T) { t.Run("should fail when trying to run deposit as read only method", func(t *testing.T) { ts := setupChain(t) - caller := fungibletypes.ModuleAddressEVM + caller := fungibletypes.ModuleAddressZEVM methodID := ts.bankABI.Methods[DepositMethodName] // Set CallerAddress and evm.Origin to the caller address. @@ -55,7 +55,7 @@ func Test_Methods(t *testing.T) { t.Run("should fail when trying to run withdraw as read only method", func(t *testing.T) { ts := setupChain(t) - caller := fungibletypes.ModuleAddressEVM + caller := fungibletypes.ModuleAddressZEVM methodID := ts.bankABI.Methods[WithdrawMethodName] // Set CallerAddress and evm.Origin to the caller address. @@ -82,7 +82,7 @@ func Test_Methods(t *testing.T) { t.Run("should fail when caller has 0 token balance", func(t *testing.T) { ts := setupChain(t) - caller := fungibletypes.ModuleAddressEVM + caller := fungibletypes.ModuleAddressZEVM methodID := ts.bankABI.Methods[DepositMethodName] // Set CallerAddress and evm.Origin to the caller address. @@ -116,7 +116,7 @@ func Test_Methods(t *testing.T) { t.Run("should fail when bank has 0 token allowance", func(t *testing.T) { ts := setupChain(t) - caller := fungibletypes.ModuleAddressEVM + caller := fungibletypes.ModuleAddressZEVM ts.fungibleKeeper.DepositZRC20(ts.ctx, ts.zrc20Address, caller, big.NewInt(1000)) methodID := ts.bankABI.Methods[DepositMethodName] @@ -152,7 +152,7 @@ func Test_Methods(t *testing.T) { t.Run("should fail when trying to deposit more than allowed to bank", func(t *testing.T) { ts := setupChain(t) - caller := fungibletypes.ModuleAddressEVM + caller := fungibletypes.ModuleAddressZEVM ts.fungibleKeeper.DepositZRC20(ts.ctx, ts.zrc20Address, caller, big.NewInt(1000)) // Allow bank to spend 500 ZRC20 tokens. @@ -200,7 +200,7 @@ func Test_Methods(t *testing.T) { t.Run("should fail when trying to deposit more than user balance", func(t *testing.T) { ts := setupChain(t) - caller := fungibletypes.ModuleAddressEVM + caller := fungibletypes.ModuleAddressZEVM ts.fungibleKeeper.DepositZRC20(ts.ctx, ts.zrc20Address, caller, big.NewInt(1000)) // Allow bank to spend 500 ZRC20 tokens. @@ -254,7 +254,7 @@ func Test_Methods(t *testing.T) { t.Run("should deposit tokens and retrieve balance of cosmos coin", func(t *testing.T) { ts := setupChain(t) - caller := fungibletypes.ModuleAddressEVM + caller := fungibletypes.ModuleAddressZEVM ts.fungibleKeeper.DepositZRC20(ts.ctx, ts.zrc20Address, caller, big.NewInt(1000)) methodID := ts.bankABI.Methods[DepositMethodName] @@ -300,7 +300,7 @@ func Test_Methods(t *testing.T) { t.Run("should deposit tokens, withdraw and check with balanceOf", func(t *testing.T) { ts := setupChain(t) - caller := fungibletypes.ModuleAddressEVM + caller := fungibletypes.ModuleAddressZEVM ts.fungibleKeeper.DepositZRC20(ts.ctx, ts.zrc20Address, caller, big.NewInt(1000)) methodID := ts.bankABI.Methods[DepositMethodName] @@ -379,7 +379,7 @@ func Test_Methods(t *testing.T) { t.Run("should deposit tokens and fail when withdrawing more than depositted", func(t *testing.T) { ts := setupChain(t) - caller := fungibletypes.ModuleAddressEVM + caller := fungibletypes.ModuleAddressZEVM ts.fungibleKeeper.DepositZRC20(ts.ctx, ts.zrc20Address, caller, big.NewInt(1000)) methodID := ts.bankABI.Methods[DepositMethodName] @@ -548,7 +548,7 @@ func allowBank(t *testing.T, ts testSuite, amount *big.Int) { ts.ctx, ts.fungibleKeeper, &ts.zrc20ABI, - fungibletypes.ModuleAddressEVM, + fungibletypes.ModuleAddressZEVM, ts.zrc20Address, "approve", []interface{}{ts.bankContract.Address(), amount}, diff --git a/x/fungible/keeper/zrc20_check_allowance.go b/x/fungible/keeper/zrc20_check_allowance.go index 19a9846817..538f985c47 100644 --- a/x/fungible/keeper/zrc20_check_allowance.go +++ b/x/fungible/keeper/zrc20_check_allowance.go @@ -33,11 +33,11 @@ func (k Keeper) CheckFungibleZRC20Allowance( return fmt.Errorf("zrc20 address cannot be zero") } - args := []interface{}{from, fungibletypes.ModuleAddressEVM} + args := []interface{}{from, fungibletypes.ModuleAddressZEVM} res, err := k.CallEVM( ctx, *zrc20ABI, - fungibletypes.ModuleAddressEVM, + fungibletypes.ModuleAddressZEVM, zrc20Address, big.NewInt(0), nil, diff --git a/x/fungible/keeper/zrc20_check_balance.go b/x/fungible/keeper/zrc20_check_balance.go index a9f0272c99..34e393d6b9 100644 --- a/x/fungible/keeper/zrc20_check_balance.go +++ b/x/fungible/keeper/zrc20_check_balance.go @@ -32,14 +32,14 @@ func (k Keeper) CheckFungibleZRC20Balance( res, err := k.CallEVM( ctx, *zrc20ABI, - fungibletypes.ModuleAddressEVM, + fungibletypes.ModuleAddressZEVM, zrc20Address, big.NewInt(0), nil, true, true, balanceOf, - fungibletypes.ModuleAddressEVM, + fungibletypes.ModuleAddressZEVM, ) if err != nil { return err diff --git a/x/fungible/keeper/zrc20_lock_token.go b/x/fungible/keeper/zrc20_lock_token.go index 8d79843ef6..1f96e20e55 100644 --- a/x/fungible/keeper/zrc20_lock_token.go +++ b/x/fungible/keeper/zrc20_lock_token.go @@ -43,11 +43,11 @@ func (k Keeper) LockZRC20( return err } - args := []interface{}{from, fungibletypes.ModuleAddressEVM, amount} + args := []interface{}{from, fungibletypes.ModuleAddressZEVM, amount} res, err := k.CallEVM( ctx, *zrc20ABI, - fungibletypes.ModuleAddressEVM, + fungibletypes.ModuleAddressZEVM, zrc20Address, big.NewInt(0), nil, diff --git a/x/fungible/keeper/zrc20_unlock_token.go b/x/fungible/keeper/zrc20_unlock_token.go index 9d6243b2da..26f917fe82 100644 --- a/x/fungible/keeper/zrc20_unlock_token.go +++ b/x/fungible/keeper/zrc20_unlock_token.go @@ -44,7 +44,7 @@ func (k Keeper) UnlockZRC20( res, err := k.CallEVM( ctx, *zrc20ABI, - fungibletypes.ModuleAddressEVM, + fungibletypes.ModuleAddressZEVM, zrc20Address, big.NewInt(0), nil, @@ -61,7 +61,7 @@ func (k Keeper) UnlockZRC20( return fmt.Errorf("%s", res.VmError) } - ret, err := zrc20ABI.Methods[transferFrom].Outputs.Unpack(res.Ret) + ret, err := zrc20ABI.Methods[transfer].Outputs.Unpack(res.Ret) if err != nil { return err } diff --git a/x/fungible/types/keys.go b/x/fungible/types/keys.go index 7e81328c15..339cc63560 100644 --- a/x/fungible/types/keys.go +++ b/x/fungible/types/keys.go @@ -29,7 +29,10 @@ func KeyPrefix(p string) []byte { var ( ModuleAddress = authtypes.NewModuleAddress(ModuleName) ModuleAddressEVM = common.BytesToAddress(ModuleAddress.Bytes()) - AdminAddress = "zeta1rx9r8hff0adaqhr5tuadkzj4e7ns2ntg446vtt" + // ModuleAddressZEVM is calculated in the same way as ModuleAddressEVM. + // Maintain it for legibility in functions calling fungible module address from zEVM. + ModuleAddressZEVM = common.BytesToAddress(ModuleAddress.Bytes()) + AdminAddress = "zeta1rx9r8hff0adaqhr5tuadkzj4e7ns2ntg446vtt" ) const ( From c2b86d44b940f64bf04c4ea587877a96fc9cb638 Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Tue, 8 Oct 2024 12:52:48 +0200 Subject: [PATCH 05/10] fix unit testing --- precompiles/bank/method_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/precompiles/bank/method_test.go b/precompiles/bank/method_test.go index b556c57720..f06027cd8a 100644 --- a/precompiles/bank/method_test.go +++ b/precompiles/bank/method_test.go @@ -551,7 +551,7 @@ func allowBank(t *testing.T, ts testSuite, amount *big.Int) { fungibletypes.ModuleAddressZEVM, ts.zrc20Address, "approve", - []interface{}{ts.bankContract.Address(), amount}, + []interface{}{fungibletypes.ModuleAddressZEVM, amount}, ) require.NoError(t, err, "error allowing bank to spend ZRC20 tokens") From 8ef6a056bfb429b1dbd66fd8ff203db8c4b6d087 Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Tue, 8 Oct 2024 13:19:05 +0200 Subject: [PATCH 06/10] make deposit and withdraw fail with amount 0 --- precompiles/bank/method_deposit.go | 2 +- precompiles/bank/method_test.go | 37 ++++++++++++++++++++++------- precompiles/bank/method_withdraw.go | 2 +- 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/precompiles/bank/method_deposit.go b/precompiles/bank/method_deposit.go index 62cb59dd51..b3ae3e0506 100644 --- a/precompiles/bank/method_deposit.go +++ b/precompiles/bank/method_deposit.go @@ -142,7 +142,7 @@ func unpackDepositArgs(args []interface{}) (zrc20Addr common.Address, amount *bi } amount, ok = args[1].(*big.Int) - if !ok || amount.Sign() < 0 || amount == nil || amount == new(big.Int) { + if !ok || amount.Sign() <= 0 || amount == nil || amount == new(big.Int) { return common.Address{}, nil, &ptypes.ErrInvalidAmount{ Got: amount.String(), } diff --git a/precompiles/bank/method_test.go b/precompiles/bank/method_test.go index f06027cd8a..e8ad80fb8b 100644 --- a/precompiles/bank/method_test.go +++ b/precompiles/bank/method_test.go @@ -130,18 +130,12 @@ func Test_Methods(t *testing.T) { ts.mockVMContract.Input = packInputArgs( t, methodID, - []interface{}{ts.zrc20Address, big.NewInt(0)}..., + []interface{}{ts.zrc20Address, big.NewInt(1000)}..., ) success, err := ts.bankContract.Run(ts.mockEVM, ts.mockVMContract, false) require.Error(t, err) - require.ErrorAs( - t, - ptypes.ErrInvalidCoin{ - Empty: true, - }, - err, - ) + require.Contains(t, err.Error(), "invalid allowance, got: 0") res, err := ts.bankABI.Methods[DepositMethodName].Outputs.Unpack(success) require.NoError(t, err) @@ -150,6 +144,33 @@ func Test_Methods(t *testing.T) { require.False(t, ok) }) + t.Run("should fail when trying to deposit 0", func(t *testing.T) { + ts := setupChain(t) + caller := fungibletypes.ModuleAddressZEVM + ts.fungibleKeeper.DepositZRC20(ts.ctx, ts.zrc20Address, caller, big.NewInt(1000)) + + methodID := ts.bankABI.Methods[DepositMethodName] + + // Allow bank to spend 500 ZRC20 tokens. + allowBank(t, ts, big.NewInt(500)) + + // 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 = caller + ts.mockEVM.Origin = caller + + // Set the input arguments for the deposit method. + ts.mockVMContract.Input = packInputArgs( + t, + methodID, + []interface{}{ts.zrc20Address, big.NewInt(0)}..., + ) + + _, err := ts.bankContract.Run(ts.mockEVM, ts.mockVMContract, false) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid token amount: 0") + }) + t.Run("should fail when trying to deposit more than allowed to bank", func(t *testing.T) { ts := setupChain(t) caller := fungibletypes.ModuleAddressZEVM diff --git a/precompiles/bank/method_withdraw.go b/precompiles/bank/method_withdraw.go index e0893b08c2..f7848c5e43 100644 --- a/precompiles/bank/method_withdraw.go +++ b/precompiles/bank/method_withdraw.go @@ -134,7 +134,7 @@ func unpackWithdrawArgs(args []interface{}) (zrc20Addr common.Address, amount *b } amount, ok = args[1].(*big.Int) - if !ok || amount.Sign() < 0 || amount == nil || amount == new(big.Int) { + if !ok || amount.Sign() <= 0 || amount == nil || amount == new(big.Int) { return common.Address{}, nil, &ptypes.ErrInvalidAmount{ Got: amount.String(), } From 445b5c3287eeec113488bbfec6f8bcd26ee83413 Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Tue, 8 Oct 2024 17:43:44 +0200 Subject: [PATCH 07/10] first round of review --- precompiles/bank/method_deposit.go | 2 +- precompiles/bank/method_withdraw.go | 8 +- x/fungible/keeper/zrc20_check_allowance.go | 72 ---- x/fungible/keeper/zrc20_check_balance.go | 67 ---- .../keeper/zrc20_check_token_validity.go | 26 -- .../keeper/zrc20_cosmos_coins_mapping.go | 321 ++++++++++++++++++ x/fungible/keeper/zrc20_lock_token.go | 82 ----- x/fungible/keeper/zrc20_unlock_token.go | 79 ----- 8 files changed, 326 insertions(+), 331 deletions(-) delete mode 100644 x/fungible/keeper/zrc20_check_allowance.go delete mode 100644 x/fungible/keeper/zrc20_check_balance.go delete mode 100644 x/fungible/keeper/zrc20_check_token_validity.go create mode 100644 x/fungible/keeper/zrc20_cosmos_coins_mapping.go delete mode 100644 x/fungible/keeper/zrc20_lock_token.go delete mode 100644 x/fungible/keeper/zrc20_unlock_token.go diff --git a/precompiles/bank/method_deposit.go b/precompiles/bank/method_deposit.go index b3ae3e0506..b262438b87 100644 --- a/precompiles/bank/method_deposit.go +++ b/precompiles/bank/method_deposit.go @@ -142,7 +142,7 @@ func unpackDepositArgs(args []interface{}) (zrc20Addr common.Address, amount *bi } amount, ok = args[1].(*big.Int) - if !ok || amount.Sign() <= 0 || amount == nil || amount == new(big.Int) { + if !ok || amount == nil || amount.Sign() <= 0 { return common.Address{}, nil, &ptypes.ErrInvalidAmount{ Got: amount.String(), } diff --git a/precompiles/bank/method_withdraw.go b/precompiles/bank/method_withdraw.go index f7848c5e43..32bc570cd3 100644 --- a/precompiles/bank/method_withdraw.go +++ b/precompiles/bank/method_withdraw.go @@ -86,9 +86,9 @@ func (c *Contract) withdraw( // Check if fungible address has enough ZRC20 balance. if err := c.fungibleKeeper.CheckFungibleZRC20Balance(ctx, c.zrc20ABI, zrc20Addr, amount); err != nil { - return nil, &ptypes.ErrUnexpected{ - When: "balanceOf", - Got: err.Error(), + return nil, &ptypes.ErrInsufficientBalance{ + Requested: amount.String(), + Got: err.Error(), } } @@ -134,7 +134,7 @@ func unpackWithdrawArgs(args []interface{}) (zrc20Addr common.Address, amount *b } amount, ok = args[1].(*big.Int) - if !ok || amount.Sign() <= 0 || amount == nil || amount == new(big.Int) { + if !ok || amount == nil || amount.Sign() <= 0 { return common.Address{}, nil, &ptypes.ErrInvalidAmount{ Got: amount.String(), } diff --git a/x/fungible/keeper/zrc20_check_allowance.go b/x/fungible/keeper/zrc20_check_allowance.go deleted file mode 100644 index 538f985c47..0000000000 --- a/x/fungible/keeper/zrc20_check_allowance.go +++ /dev/null @@ -1,72 +0,0 @@ -package keeper - -import ( - "fmt" - "math/big" - - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/ethereum/go-ethereum/accounts/abi" - "github.com/ethereum/go-ethereum/common" - - fungibletypes "github.com/zeta-chain/node/x/fungible/types" -) - -const allowance = "allowance" - -// CheckFungibleZRC20Allowance checks if the allowance of ZRC20 tokens, -// is equal or greater than the provided amount. -func (k Keeper) CheckFungibleZRC20Allowance( - ctx sdk.Context, - zrc20ABI *abi.ABI, - from, zrc20Address common.Address, - amount *big.Int, -) error { - if amount.Sign() <= 0 { - return fmt.Errorf("amount must be positive, got: %s", amount.String()) - } - - if from == zeroAddress { - return fmt.Errorf("from address cannot be zero") - } - - if zrc20Address == zeroAddress { - return fmt.Errorf("zrc20 address cannot be zero") - } - - args := []interface{}{from, fungibletypes.ModuleAddressZEVM} - res, err := k.CallEVM( - ctx, - *zrc20ABI, - fungibletypes.ModuleAddressZEVM, - zrc20Address, - big.NewInt(0), - nil, - true, - true, - allowance, - args..., - ) - if err != nil { - return err - } - - if res.VmError != "" { - return fmt.Errorf("%s", res.VmError) - } - - ret, err := zrc20ABI.Methods[allowance].Outputs.Unpack(res.Ret) - if err != nil { - return err - } - - allowance, ok := ret[0].(*big.Int) - if !ok { - return fmt.Errorf("ZRC20 allowance returned an unexpected type") - } - - if allowance.Cmp(amount) < 0 || allowance.Cmp(big.NewInt(0)) <= 0 { - return fmt.Errorf("invalid allowance, got: %s", allowance.String()) - } - - return nil -} diff --git a/x/fungible/keeper/zrc20_check_balance.go b/x/fungible/keeper/zrc20_check_balance.go deleted file mode 100644 index 34e393d6b9..0000000000 --- a/x/fungible/keeper/zrc20_check_balance.go +++ /dev/null @@ -1,67 +0,0 @@ -package keeper - -import ( - "fmt" - "math/big" - - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/ethereum/go-ethereum/accounts/abi" - "github.com/ethereum/go-ethereum/common" - - fungibletypes "github.com/zeta-chain/node/x/fungible/types" -) - -const balanceOf = "balanceOf" - -// CheckFungibleZRC20Balance checks if the balance of ZRC20 tokens, -// is equal or greater than the provided amount. -func (k Keeper) CheckFungibleZRC20Balance( - ctx sdk.Context, - zrc20ABI *abi.ABI, - zrc20Address common.Address, - amount *big.Int, -) error { - if amount.Sign() <= 0 { - return fmt.Errorf("amount must be positive, got: %s", amount.String()) - } - - if zrc20Address == zeroAddress { - return fmt.Errorf("zrc20 address cannot be zero") - } - - res, err := k.CallEVM( - ctx, - *zrc20ABI, - fungibletypes.ModuleAddressZEVM, - zrc20Address, - big.NewInt(0), - nil, - true, - true, - balanceOf, - fungibletypes.ModuleAddressZEVM, - ) - if err != nil { - return err - } - - if res.VmError != "" { - return fmt.Errorf("%s", res.VmError) - } - - ret, err := zrc20ABI.Methods[balanceOf].Outputs.Unpack(res.Ret) - if err != nil { - return err - } - - balance, ok := ret[0].(*big.Int) - if !ok { - return fmt.Errorf("ZRC20 balanceOf returned an unexpected type") - } - - if balance.Cmp(amount) == -1 { - return fmt.Errorf("invalid balance, got: %s", balance.String()) - } - - return nil -} diff --git a/x/fungible/keeper/zrc20_check_token_validity.go b/x/fungible/keeper/zrc20_check_token_validity.go deleted file mode 100644 index bd4bf21c5b..0000000000 --- a/x/fungible/keeper/zrc20_check_token_validity.go +++ /dev/null @@ -1,26 +0,0 @@ -package keeper - -import ( - "fmt" - - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/ethereum/go-ethereum/common" -) - -// IsValidZRC20 returns an error whenever a ZRC20 is not whitelisted or paused. -func (k Keeper) IsValidZRC20(ctx sdk.Context, zrc20Address common.Address) error { - if zrc20Address == zeroAddress { - return fmt.Errorf("zrc20 address cannot be zero") - } - - t, found := k.GetForeignCoins(ctx, zrc20Address.String()) - if !found { - return fmt.Errorf("ZRC20 is not whitelisted, address: %s", zrc20Address.String()) - } - - if t.Paused { - return fmt.Errorf("ZRC20 is paused, address: %s", zrc20Address.String()) - } - - return nil -} diff --git a/x/fungible/keeper/zrc20_cosmos_coins_mapping.go b/x/fungible/keeper/zrc20_cosmos_coins_mapping.go new file mode 100644 index 0000000000..4d5e86ae21 --- /dev/null +++ b/x/fungible/keeper/zrc20_cosmos_coins_mapping.go @@ -0,0 +1,321 @@ +package keeper + +import ( + "fmt" + "math/big" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + + "github.com/zeta-chain/node/pkg/crypto" + fungibletypes "github.com/zeta-chain/node/x/fungible/types" +) + +const ( + transferFrom = "transferFrom" + transfer = "transfer" + balanceOf = "balanceOf" + allowance = "allowance" +) + +var ( + ErrZRC20ZeroAddress = fmt.Errorf("ZRC20 address cannot be zero") + ErrZRC20NotWhiteListed = fmt.Errorf("ZRC20 is not whitelisted") + ErrZRC20Paused = fmt.Errorf("ZRC20 is paused") + ErrZRC20NilABI = fmt.Errorf("ZRC20 ABI is nil") + ErrZeroAddress = fmt.Errorf("address cannot be zero") + ErrInvalidAmount = fmt.Errorf("amount must be positive") +) + +// LockZRC20 locks ZRC20 tokens in the bank contract. +// The caller must have approved the bank contract to spend the amount of ZRC20 tokens. +func (k Keeper) LockZRC20( + ctx sdk.Context, + zrc20ABI *abi.ABI, + zrc20Address, from common.Address, + amount *big.Int, +) error { + if zrc20ABI == nil { + return ErrZRC20NilABI + } + + if amount.Sign() <= 0 || amount == nil { + return ErrInvalidAmount + } + + if crypto.IsEmptyAddress(from) { + return ErrZeroAddress + } + + if crypto.IsEmptyAddress(zrc20Address) { + return ErrZRC20ZeroAddress + } + + if err := k.IsValidZRC20(ctx, zrc20Address); err != nil { + return err + } + + if err := k.CheckFungibleZRC20Allowance(ctx, zrc20ABI, from, zrc20Address, amount); err != nil { + return err + } + + args := []interface{}{from, fungibletypes.ModuleAddressZEVM, amount} + res, err := k.CallEVM( + ctx, + *zrc20ABI, + fungibletypes.ModuleAddressZEVM, + zrc20Address, + big.NewInt(0), + nil, + true, + true, + transferFrom, + args..., + ) + if err != nil { + return err + } + + if res.VmError != "" { + return fmt.Errorf("EVM execution error in LockZRC20: %s", res.VmError) + } + + ret, err := zrc20ABI.Methods[transferFrom].Outputs.Unpack(res.Ret) + if err != nil { + return err + } + + if len(ret) == 0 { + return fmt.Errorf("no data returned from 'transferFrom' method") + } + + transferred, ok := ret[0].(bool) + if !ok { + return fmt.Errorf("transferFrom returned an unexpected value") + } + + if !transferred { + return fmt.Errorf("transferFrom not successful") + } + + return nil +} + +// UnlockZRC20 unlocks ZRC20 tokens and sends them to the "to" address. +func (k Keeper) UnlockZRC20( + ctx sdk.Context, + zrc20ABI *abi.ABI, + zrc20Address, to common.Address, + amount *big.Int, +) error { + if zrc20ABI == nil { + return ErrZRC20NilABI + } + + if amount.Sign() <= 0 || amount == nil { + return ErrInvalidAmount + } + + if crypto.IsEmptyAddress(to) { + return ErrZeroAddress + } + + if crypto.IsEmptyAddress(zrc20Address) { + return ErrZRC20ZeroAddress + } + + if err := k.IsValidZRC20(ctx, zrc20Address); err != nil { + return err + } + + if err := k.CheckFungibleZRC20Balance(ctx, zrc20ABI, zrc20Address, amount); err != nil { + return err + } + + args := []interface{}{to, amount} + res, err := k.CallEVM( + ctx, + *zrc20ABI, + fungibletypes.ModuleAddressZEVM, + zrc20Address, + big.NewInt(0), + nil, + true, + true, + transfer, + args..., + ) + if err != nil { + return err + } + + if res.VmError != "" { + return fmt.Errorf("EVM execution error in UnlockZRC20: %s", res.VmError) + } + + ret, err := zrc20ABI.Methods[transfer].Outputs.Unpack(res.Ret) + if err != nil { + return err + } + + if len(ret) == 0 { + return fmt.Errorf("no data returned from 'transfer' method") + } + + transferred, ok := ret[0].(bool) + if !ok { + return fmt.Errorf("transfer returned an unexpected value") + } + + if !transferred { + return fmt.Errorf("transfer not successful") + } + + return nil +} + +// CheckFungibleZRC20Allowance checks if the allowance of ZRC20 tokens, +// is equal or greater than the provided amount. +func (k Keeper) CheckFungibleZRC20Allowance( + ctx sdk.Context, + zrc20ABI *abi.ABI, + from, zrc20Address common.Address, + amount *big.Int, +) error { + if zrc20ABI == nil { + return ErrZRC20NilABI + } + + if amount.Sign() <= 0 || amount == nil { + return ErrInvalidAmount + } + + if crypto.IsEmptyAddress(from) { + return ErrZeroAddress + } + + if crypto.IsEmptyAddress(zrc20Address) { + return ErrZRC20ZeroAddress + } + + args := []interface{}{from, fungibletypes.ModuleAddressZEVM} + res, err := k.CallEVM( + ctx, + *zrc20ABI, + fungibletypes.ModuleAddressZEVM, + zrc20Address, + big.NewInt(0), + nil, + true, + true, + allowance, + args..., + ) + if err != nil { + return err + } + + if res.VmError != "" { + return fmt.Errorf("EVM execution error calling allowance: %s", res.VmError) + } + + ret, err := zrc20ABI.Methods[allowance].Outputs.Unpack(res.Ret) + if err != nil { + return err + } + + if len(ret) == 0 { + return fmt.Errorf("no data returned from 'allowance' method") + } + + allowanceValue, ok := ret[0].(*big.Int) + if !ok { + return fmt.Errorf("ZRC20 allowance returned an unexpected type") + } + + if allowanceValue.Cmp(amount) < 0 || allowanceValue.Cmp(big.NewInt(0)) <= 0 { + return fmt.Errorf("invalid allowance, got: %s", allowanceValue.String()) + } + + return nil +} + +// CheckFungibleZRC20Balance checks if the balance of ZRC20 tokens, +// is equal or greater than the provided amount. +func (k Keeper) CheckFungibleZRC20Balance( + ctx sdk.Context, + zrc20ABI *abi.ABI, + zrc20Address common.Address, + amount *big.Int, +) error { + if zrc20ABI == nil { + return ErrZRC20NilABI + } + + if amount.Sign() <= 0 || amount == nil { + return ErrInvalidAmount + } + + if crypto.IsEmptyAddress(zrc20Address) { + return ErrZRC20ZeroAddress + } + + res, err := k.CallEVM( + ctx, + *zrc20ABI, + fungibletypes.ModuleAddressZEVM, + zrc20Address, + big.NewInt(0), + nil, + true, + true, + balanceOf, + fungibletypes.ModuleAddressZEVM, + ) + if err != nil { + return err + } + + if res.VmError != "" { + return fmt.Errorf("EVM execution error calling balanceOf: %s", res.VmError) + } + + ret, err := zrc20ABI.Methods[balanceOf].Outputs.Unpack(res.Ret) + if err != nil { + return err + } + + if len(ret) == 0 { + return fmt.Errorf("no data returned from 'balanceOf' method") + } + + balance, ok := ret[0].(*big.Int) + if !ok { + return fmt.Errorf("ZRC20 balanceOf returned an unexpected type") + } + + if balance.Cmp(amount) == -1 { + return fmt.Errorf("invalid balance, got: %s", balance.String()) + } + + return nil +} + +// IsValidZRC20 returns an error whenever a ZRC20 is not whitelisted or paused. +func (k Keeper) IsValidZRC20(ctx sdk.Context, zrc20Address common.Address) error { + if crypto.IsEmptyAddress(zrc20Address) { + return ErrZRC20ZeroAddress + } + + t, found := k.GetForeignCoins(ctx, zrc20Address.String()) + if !found { + return ErrZRC20NotWhiteListed + } + + if t.Paused { + return ErrZRC20Paused + } + + return nil +} diff --git a/x/fungible/keeper/zrc20_lock_token.go b/x/fungible/keeper/zrc20_lock_token.go deleted file mode 100644 index 1f96e20e55..0000000000 --- a/x/fungible/keeper/zrc20_lock_token.go +++ /dev/null @@ -1,82 +0,0 @@ -package keeper - -import ( - "fmt" - "math/big" - - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/ethereum/go-ethereum/accounts/abi" - "github.com/ethereum/go-ethereum/common" - - fungibletypes "github.com/zeta-chain/node/x/fungible/types" -) - -const transferFrom = "transferFrom" - -var zeroAddress common.Address = common.HexToAddress("0x0000000000000000000000000000000000000000") - -// LockZRC20 locks ZRC20 tokens in the bank contract. -// The caller must have approved the bank contract to spend the amount of ZRC20 tokens. -func (k Keeper) LockZRC20( - ctx sdk.Context, - zrc20ABI *abi.ABI, - zrc20Address, from common.Address, - amount *big.Int, -) error { - if amount.Sign() <= 0 { - return fmt.Errorf("amount must be positive, got: %s", amount.String()) - } - - if from == zeroAddress { - return fmt.Errorf("from address cannot be zero") - } - - if zrc20Address == zeroAddress { - return fmt.Errorf("zrc20 address cannot be zero") - } - - if err := k.IsValidZRC20(ctx, zrc20Address); err != nil { - return err - } - - if err := k.CheckFungibleZRC20Allowance(ctx, zrc20ABI, from, zrc20Address, amount); err != nil { - return err - } - - args := []interface{}{from, fungibletypes.ModuleAddressZEVM, amount} - res, err := k.CallEVM( - ctx, - *zrc20ABI, - fungibletypes.ModuleAddressZEVM, - zrc20Address, - big.NewInt(0), - nil, - true, - true, - transferFrom, - args..., - ) - if err != nil { - return err - } - - if res.VmError != "" { - return fmt.Errorf("%s", res.VmError) - } - - ret, err := zrc20ABI.Methods[transferFrom].Outputs.Unpack(res.Ret) - if err != nil { - return err - } - - transferred, ok := ret[0].(bool) - if !ok { - return fmt.Errorf("transferFrom returned an unexpected value") - } - - if !transferred { - return fmt.Errorf("transferFrom not successful") - } - - return nil -} diff --git a/x/fungible/keeper/zrc20_unlock_token.go b/x/fungible/keeper/zrc20_unlock_token.go deleted file mode 100644 index 26f917fe82..0000000000 --- a/x/fungible/keeper/zrc20_unlock_token.go +++ /dev/null @@ -1,79 +0,0 @@ -package keeper - -import ( - "fmt" - "math/big" - - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/ethereum/go-ethereum/accounts/abi" - "github.com/ethereum/go-ethereum/common" - - fungibletypes "github.com/zeta-chain/node/x/fungible/types" -) - -const transfer = "transfer" - -// UnlockZRC20 unlocks ZRC20 tokens and sends them to the "to" address. -func (k Keeper) UnlockZRC20( - ctx sdk.Context, - zrc20ABI *abi.ABI, - zrc20Address, to common.Address, - amount *big.Int, -) error { - if amount.Sign() <= 0 { - return fmt.Errorf("amount must be positive, got: %s", amount.String()) - } - - if to == zeroAddress { - return fmt.Errorf("from address cannot be zero") - } - - if zrc20Address == zeroAddress { - return fmt.Errorf("zrc20 address cannot be zero") - } - - if err := k.IsValidZRC20(ctx, zrc20Address); err != nil { - return err - } - - if err := k.CheckFungibleZRC20Balance(ctx, zrc20ABI, zrc20Address, amount); err != nil { - return err - } - - args := []interface{}{to, amount} - res, err := k.CallEVM( - ctx, - *zrc20ABI, - fungibletypes.ModuleAddressZEVM, - zrc20Address, - big.NewInt(0), - nil, - true, - true, - transfer, - args..., - ) - if err != nil { - return err - } - - if res.VmError != "" { - return fmt.Errorf("%s", res.VmError) - } - - ret, err := zrc20ABI.Methods[transfer].Outputs.Unpack(res.Ret) - if err != nil { - return err - } - - transferred, ok := ret[0].(bool) - if !ok { - return fmt.Errorf("transfer returned an unexpected value") - } - - if !transferred { - return fmt.Errorf("transfer not successful") - } - - return nil -} From 81997c8dccf7ba58a1de90adffb424040b6d5d1b Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Thu, 10 Oct 2024 14:16:19 +0200 Subject: [PATCH 08/10] add full unit testing suite --- e2e/e2etests/test_precompiles_bank.go | 20 +- .../test_precompiles_bank_through_contract.go | 20 +- precompiles/bank/method_deposit.go | 4 +- precompiles/bank/method_test.go | 2 +- precompiles/bank/method_withdraw.go | 6 +- .../keeper/zrc20_cosmos_coin_mapping_test.go | 405 ++++++++++++++++++ .../keeper/zrc20_cosmos_coins_mapping.go | 258 +++-------- x/fungible/keeper/zrc20_methods.go | 301 +++++++++++++ x/fungible/keeper/zrc20_methods_test.go | 321 ++++++++++++++ x/fungible/types/errors.go | 5 + 10 files changed, 1122 insertions(+), 220 deletions(-) create mode 100644 x/fungible/keeper/zrc20_cosmos_coin_mapping_test.go create mode 100644 x/fungible/keeper/zrc20_methods.go create mode 100644 x/fungible/keeper/zrc20_methods_test.go diff --git a/e2e/e2etests/test_precompiles_bank.go b/e2e/e2etests/test_precompiles_bank.go index 1afd52d9cf..308f684572 100644 --- a/e2e/e2etests/test_precompiles_bank.go +++ b/e2e/e2etests/test_precompiles_bank.go @@ -10,7 +10,6 @@ import ( "github.com/zeta-chain/node/e2e/runner" "github.com/zeta-chain/node/e2e/utils" "github.com/zeta-chain/node/precompiles/bank" - fungibletypes "github.com/zeta-chain/node/x/fungible/types" ) func TestPrecompilesBank(r *runner.E2ERunner, args []string) { @@ -21,6 +20,7 @@ func TestPrecompilesBank(r *runner.E2ERunner, args []string) { higherBalanceAmount := big.NewInt(1001) higherAllowanceAmount := big.NewInt(501) spender := r.EVMAddress() + bankAddress := bank.ContractAddress // Increase the gasLimit. It's required because of the gas consumed by precompiled functions. previousGasLimit := r.ZEVMAuth.GasLimit @@ -30,7 +30,7 @@ func TestPrecompilesBank(r *runner.E2ERunner, args []string) { // Reset the allowance to 0; this is needed when running upgrade tests where // this test runs twice. - tx, err := r.ERC20ZRC20.Approve(r.ZEVMAuth, fungibletypes.ModuleAddressZEVM, big.NewInt(0)) + tx, err := r.ERC20ZRC20.Approve(r.ZEVMAuth, bankAddress, 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") @@ -60,7 +60,7 @@ func TestPrecompilesBank(r *runner.E2ERunner, args []string) { 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, fungibletypes.ModuleAddressZEVM, depositAmount) + tx, err := r.ERC20ZRC20.Approve(r.ZEVMAuth, bankAddress, 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") @@ -73,7 +73,7 @@ func TestPrecompilesBank(r *runner.E2ERunner, args []string) { 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, fungibletypes.ModuleAddressZEVM, big.NewInt(1e3)) + tx, err = r.ERC20ZRC20.Approve(r.ZEVMAuth, bankAddress, 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") @@ -104,7 +104,7 @@ func TestPrecompilesBank(r *runner.E2ERunner, args []string) { 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}, fungibletypes.ModuleAddressZEVM) + bankZRC20Balance, err := r.ERC20ZRC20.BalanceOf(&bind.CallOpts{Context: r.Ctx}, bankAddress) require.NoError(r, err, "Call ERC20ZRC20.BalanceOf") require.Equal(r, uint64(500), bankZRC20Balance.Uint64(), "bank ERC20ZRC20 balance should be 500") @@ -116,7 +116,7 @@ func TestPrecompilesBank(r *runner.E2ERunner, args []string) { // 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}, fungibletypes.ModuleAddressZEVM) + bankZRC20Balance, err = r.ERC20ZRC20.BalanceOf(&bind.CallOpts{Context: r.Ctx}, bankAddress) require.NoError(r, err, "Call ERC20ZRC20.BalanceOf") require.Equal(r, uint64(500), bankZRC20Balance.Uint64(), "bank ERC20ZRC20 balance should be 500") @@ -144,7 +144,7 @@ func TestPrecompilesBank(r *runner.E2ERunner, args []string) { 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}, fungibletypes.ModuleAddressZEVM) + bankZRC20Balance, err = r.ERC20ZRC20.BalanceOf(&bind.CallOpts{Context: r.Ctx}, bankAddress) require.NoError(r, err, "Call ERC20ZRC20.BalanceOf") require.Equal(r, uint64(0), bankZRC20Balance.Uint64(), "bank ERC20ZRC20 balance should be 0") } @@ -159,7 +159,7 @@ func TestPrecompilesBankNonZRC20(r *runner.E2ERunner, args []string) { r.ZEVMAuth.GasLimit = previousGasLimit }() - spender, bankAddr := r.EVMAddress(), fungibletypes.ModuleAddressZEVM + spender, bankAddress := r.EVMAddress(), bank.ContractAddress // Create a bank contract caller. bankContract, err := bank.NewIBank(bank.ContractAddress, r.ZEVMClient) @@ -180,13 +180,13 @@ func TestPrecompilesBankNonZRC20(r *runner.E2ERunner, args []string) { ) // Allow the bank contract to spend 25 WZeta tokens. - tx, err := r.WZeta.Approve(r.ZEVMAuth, bankAddr, big.NewInt(25)) + tx, err := r.WZeta.Approve(r.ZEVMAuth, bankAddress, 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) + allowance, err := r.WZeta.Allowance(&bind.CallOpts{Context: r.Ctx}, spender, bankAddress) require.NoError(r, err, "Error retrieving bank allowance") require.EqualValues(r, uint64(25), allowance.Uint64(), "Error allowance for bank contract") diff --git a/e2e/e2etests/test_precompiles_bank_through_contract.go b/e2e/e2etests/test_precompiles_bank_through_contract.go index 178db56c1d..6d6384fd9e 100644 --- a/e2e/e2etests/test_precompiles_bank_through_contract.go +++ b/e2e/e2etests/test_precompiles_bank_through_contract.go @@ -12,13 +12,13 @@ import ( "github.com/zeta-chain/node/e2e/runner" "github.com/zeta-chain/node/e2e/utils" "github.com/zeta-chain/node/precompiles/bank" - fungibletypes "github.com/zeta-chain/node/x/fungible/types" ) func TestPrecompilesBankThroughContract(r *runner.E2ERunner, args []string) { require.Len(r, args, 0, "No arguments expected") spender := r.EVMAddress() + bankAddress := bank.ContractAddress zrc20Address := r.ERC20ZRC20Addr oneThousand := big.NewInt(1e3) oneThousandOne := big.NewInt(1001) @@ -60,7 +60,7 @@ func TestPrecompilesBankThroughContract(r *runner.E2ERunner, args []string) { // Check initial balances. balanceShouldBe(r, 0, checkCosmosBalance(r, testBank, zrc20Address, spender)) balanceShouldBe(r, 1000, checkZRC20Balance(r, spender)) - balanceShouldBe(r, 0, checkZRC20Balance(r, fungibletypes.ModuleAddressZEVM)) + balanceShouldBe(r, 0, checkZRC20Balance(r, bankAddress)) // Deposit without previous alllowance should fail. receipt = depositThroughTestBank(r, testBank, zrc20Address, oneThousand) @@ -69,10 +69,10 @@ func TestPrecompilesBankThroughContract(r *runner.E2ERunner, args []string) { // Check balances, should be the same. balanceShouldBe(r, 0, checkCosmosBalance(r, testBank, zrc20Address, spender)) balanceShouldBe(r, 1000, checkZRC20Balance(r, spender)) - balanceShouldBe(r, 0, checkZRC20Balance(r, fungibletypes.ModuleAddressZEVM)) + balanceShouldBe(r, 0, checkZRC20Balance(r, bankAddress)) // Allow 500 ZRC20 to bank precompile. - approveAllowance(r, fungibletypes.ModuleAddressZEVM, fiveHundred) + approveAllowance(r, bankAddress, fiveHundred) // Deposit 501 ERC20ZRC20 tokens to the bank contract, through TestBank. // It's higher than allowance but lower than balance, should fail. @@ -82,10 +82,10 @@ func TestPrecompilesBankThroughContract(r *runner.E2ERunner, args []string) { // Balances shouldn't change. balanceShouldBe(r, 0, checkCosmosBalance(r, testBank, zrc20Address, spender)) balanceShouldBe(r, 1000, checkZRC20Balance(r, spender)) - balanceShouldBe(r, 0, checkZRC20Balance(r, fungibletypes.ModuleAddressZEVM)) + balanceShouldBe(r, 0, checkZRC20Balance(r, bankAddress)) // Allow 1000 ZRC20 to bank precompile. - approveAllowance(r, fungibletypes.ModuleAddressZEVM, oneThousand) + approveAllowance(r, bankAddress, oneThousand) // Deposit 1001 ERC20ZRC20 tokens to the bank contract. // It's higher than spender balance but within approved allowance, should fail. @@ -95,7 +95,7 @@ func TestPrecompilesBankThroughContract(r *runner.E2ERunner, args []string) { // Balances shouldn't change. balanceShouldBe(r, 0, checkCosmosBalance(r, testBank, zrc20Address, spender)) balanceShouldBe(r, 1000, checkZRC20Balance(r, spender)) - balanceShouldBe(r, 0, checkZRC20Balance(r, fungibletypes.ModuleAddressZEVM)) + balanceShouldBe(r, 0, checkZRC20Balance(r, bankAddress)) // Deposit 500 ERC20ZRC20 tokens to the bank contract, it's within allowance and balance. Should pass. receipt = depositThroughTestBank(r, testBank, zrc20Address, fiveHundred) @@ -104,7 +104,7 @@ func TestPrecompilesBankThroughContract(r *runner.E2ERunner, args []string) { // Balances should be transferred. Bank now locks 500 ZRC20 tokens. balanceShouldBe(r, 500, checkCosmosBalance(r, testBank, zrc20Address, spender)) balanceShouldBe(r, 500, checkZRC20Balance(r, spender)) - balanceShouldBe(r, 500, checkZRC20Balance(r, fungibletypes.ModuleAddressZEVM)) + balanceShouldBe(r, 500, checkZRC20Balance(r, bankAddress)) // Check the deposit event. eventDeposit, err := bankPrecompileCaller.ParseDeposit(*receipt.Logs[0]) @@ -120,7 +120,7 @@ func TestPrecompilesBankThroughContract(r *runner.E2ERunner, args []string) { // Balances shouldn't change. balanceShouldBe(r, 500, checkCosmosBalance(r, testBank, zrc20Address, spender)) balanceShouldBe(r, 500, checkZRC20Balance(r, spender)) - balanceShouldBe(r, 500, checkZRC20Balance(r, fungibletypes.ModuleAddressZEVM)) + balanceShouldBe(r, 500, checkZRC20Balance(r, bankAddress)) // Try to withdraw 500 ERC20ZRC20 tokens. Should pass. receipt = withdrawThroughTestBank(r, testBank, zrc20Address, fiveHundred) @@ -129,7 +129,7 @@ func TestPrecompilesBankThroughContract(r *runner.E2ERunner, args []string) { // Balances should be reverted to initial state. balanceShouldBe(r, 0, checkCosmosBalance(r, testBank, zrc20Address, spender)) balanceShouldBe(r, 1000, checkZRC20Balance(r, spender)) - balanceShouldBe(r, 0, checkZRC20Balance(r, fungibletypes.ModuleAddressZEVM)) + balanceShouldBe(r, 0, checkZRC20Balance(r, bankAddress)) // Check the withdraw event. eventWithdraw, err := bankPrecompileCaller.ParseWithdraw(*receipt.Logs[0]) diff --git a/precompiles/bank/method_deposit.go b/precompiles/bank/method_deposit.go index b262438b87..ae620c34a9 100644 --- a/precompiles/bank/method_deposit.go +++ b/precompiles/bank/method_deposit.go @@ -18,7 +18,7 @@ import ( // 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 Fungible ZEVM address to spend a certain amount ZRC20 token coins on its behalf. This is mandatory. +// - The caller has to allow the bank precompile address 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. @@ -100,7 +100,7 @@ func (c *Contract) deposit( } // 2. Effect: subtract balance. - if err := c.fungibleKeeper.LockZRC20(ctx, c.zrc20ABI, zrc20Addr, caller, amount); err != nil { + if err := c.fungibleKeeper.LockZRC20(ctx, c.zrc20ABI, zrc20Addr, caller, c.Address(), amount); err != nil { return nil, &ptypes.ErrUnexpected{ When: "LockZRC20InBank", Got: err.Error(), diff --git a/precompiles/bank/method_test.go b/precompiles/bank/method_test.go index e8ad80fb8b..f6601bccbe 100644 --- a/precompiles/bank/method_test.go +++ b/precompiles/bank/method_test.go @@ -572,7 +572,7 @@ func allowBank(t *testing.T, ts testSuite, amount *big.Int) { fungibletypes.ModuleAddressZEVM, ts.zrc20Address, "approve", - []interface{}{fungibletypes.ModuleAddressZEVM, amount}, + []interface{}{ts.bankContract.Address(), amount}, ) require.NoError(t, err, "error allowing bank to spend ZRC20 tokens") diff --git a/precompiles/bank/method_withdraw.go b/precompiles/bank/method_withdraw.go index 32bc570cd3..e071ed04cd 100644 --- a/precompiles/bank/method_withdraw.go +++ b/precompiles/bank/method_withdraw.go @@ -84,8 +84,8 @@ func (c *Contract) withdraw( return nil, err } - // Check if fungible address has enough ZRC20 balance. - if err := c.fungibleKeeper.CheckFungibleZRC20Balance(ctx, c.zrc20ABI, zrc20Addr, amount); err != nil { + // Check if bank address has enough ZRC20 balance. + if err := c.fungibleKeeper.CheckZRC20Balance(ctx, c.zrc20ABI, zrc20Addr, c.Address(), amount); err != nil { return nil, &ptypes.ErrInsufficientBalance{ Requested: amount.String(), Got: err.Error(), @@ -108,7 +108,7 @@ func (c *Contract) withdraw( } // 3. Interactions: send ZRC20. - if err := c.fungibleKeeper.UnlockZRC20(ctx, c.zrc20ABI, zrc20Addr, caller, amount); err != nil { + if err := c.fungibleKeeper.UnlockZRC20(ctx, c.zrc20ABI, zrc20Addr, caller, c.Address(), amount); err != nil { return nil, &ptypes.ErrUnexpected{ When: "UnlockZRC20InBank", Got: err.Error(), diff --git a/x/fungible/keeper/zrc20_cosmos_coin_mapping_test.go b/x/fungible/keeper/zrc20_cosmos_coin_mapping_test.go new file mode 100644 index 0000000000..578ee2dfb9 --- /dev/null +++ b/x/fungible/keeper/zrc20_cosmos_coin_mapping_test.go @@ -0,0 +1,405 @@ +package keeper_test + +import ( + "math/big" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + "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" + "github.com/zeta-chain/protocol-contracts/v2/pkg/zrc20.sol" +) + +func Test_LockZRC20(t *testing.T) { + zrc20ABI, err := zrc20.ZRC20MetaData.GetAbi() + require.NoError(t, err) + + ts := setupChain(t) + + owner := fungibletypes.ModuleAddressZEVM + locker := sample.EthAddress() + depositTotal := big.NewInt(1000) + allowanceTotal := big.NewInt(100) + higherThanAllowance := big.NewInt(101) + smallerThanAllowance := big.NewInt(99) + + // Make sure locker account exists in state. + accAddress := sdk.AccAddress(locker.Bytes()) + ts.fungibleKeeper.GetAuthKeeper().SetAccount(ts.ctx, authtypes.NewBaseAccount(accAddress, nil, 0, 0)) + + // Deposit 1000 ZRC20 tokens into the fungible. + ts.fungibleKeeper.DepositZRC20(ts.ctx, ts.zrc20Address, owner, depositTotal) + + t.Run("should fail when trying to lock zero amount", func(t *testing.T) { + // Check lock with zero amount. + err = ts.fungibleKeeper.LockZRC20(ts.ctx, zrc20ABI, ts.zrc20Address, owner, locker, big.NewInt(0)) + require.Error(t, err) + require.ErrorIs(t, err, fungibletypes.ErrInvalidAmount) + }) + + t.Run("should fail when ZRC20 ABI is not properly initialized", func(t *testing.T) { + // Check lock with nil ABI. + err = ts.fungibleKeeper.LockZRC20(ts.ctx, nil, ts.zrc20Address, owner, locker, big.NewInt(10)) + require.Error(t, err) + require.ErrorIs(t, err, fungibletypes.ErrZRC20NilABI) + }) + + t.Run("should fail when trying to lock a zero address ZRC20", func(t *testing.T) { + // Check lock with ZRC20 zero address. + err = ts.fungibleKeeper.LockZRC20(ts.ctx, zrc20ABI, common.Address{}, owner, locker, big.NewInt(10)) + require.Error(t, err) + require.ErrorIs(t, err, fungibletypes.ErrZRC20ZeroAddress) + }) + + t.Run("should fail when trying to lock a non whitelisted ZRC20", func(t *testing.T) { + // Check lock with non whitelisted ZRC20. + err = ts.fungibleKeeper.LockZRC20(ts.ctx, zrc20ABI, sample.EthAddress(), owner, locker, big.NewInt(10)) + require.Error(t, err) + require.ErrorIs(t, err, fungibletypes.ErrZRC20NotWhiteListed) + }) + + t.Run("should fail when trying to lock a higher amount than totalSupply", func(t *testing.T) { + approveAllowance(t, ts, zrc20ABI, owner, locker, big.NewInt(1000000000000000)) + + // Check lock with higher amount than totalSupply. + err = ts.fungibleKeeper.LockZRC20( + ts.ctx, + zrc20ABI, + ts.zrc20Address, + owner, + locker, + big.NewInt(1000000000000000), + ) + require.Error(t, err) + require.ErrorIs(t, err, fungibletypes.ErrInvalidAmount) + }) + + t.Run("should fail when trying to lock a higher amount than owned balance", func(t *testing.T) { + approveAllowance(t, ts, zrc20ABI, owner, locker, big.NewInt(1001)) + + // Check allowance smaller, equal and bigger than the amount. + err = ts.fungibleKeeper.LockZRC20(ts.ctx, zrc20ABI, ts.zrc20Address, owner, locker, big.NewInt(1001)) + require.Error(t, err) + + // We do not check in LockZRC20 explicitly if the amount is bigger than the balance. + // Instead, the ERC20 transferFrom function will revert the transaction if the amount is bigger than the balance. + require.Contains(t, err.Error(), "execution reverted") + }) + + t.Run("should fail when trying to lock an amount higher than approved", func(t *testing.T) { + approveAllowance(t, ts, zrc20ABI, owner, locker, allowanceTotal) + + // Check allowance smaller, equal and bigger than the amount. + err = ts.fungibleKeeper.LockZRC20(ts.ctx, zrc20ABI, ts.zrc20Address, owner, locker, higherThanAllowance) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid allowance, got: 100") + }) + + t.Run("should pass when trying to lock a valid approved amount", func(t *testing.T) { + approveAllowance(t, ts, zrc20ABI, owner, locker, allowanceTotal) + + err = ts.fungibleKeeper.LockZRC20(ts.ctx, zrc20ABI, ts.zrc20Address, owner, locker, allowanceTotal) + require.NoError(t, err) + + ownerBalance, err := ts.fungibleKeeper.ZRC20BalanceOf(ts.ctx, zrc20ABI, ts.zrc20Address, owner) + require.NoError(t, err) + require.Equal(t, uint64(900), ownerBalance.Uint64()) + + lockerBalance, err := ts.fungibleKeeper.ZRC20BalanceOf(ts.ctx, zrc20ABI, ts.zrc20Address, locker) + require.NoError(t, err) + require.Equal(t, uint64(100), lockerBalance.Uint64()) + }) + + t.Run("should pass when trying to lock an amount smaller than approved", func(t *testing.T) { + approveAllowance(t, ts, zrc20ABI, owner, locker, allowanceTotal) + + err = ts.fungibleKeeper.LockZRC20(ts.ctx, zrc20ABI, ts.zrc20Address, owner, locker, smallerThanAllowance) + require.NoError(t, err) + + // Note that balances are cumulative for all tests. That's why we check 801 and 199 here. + ownerBalance, err := ts.fungibleKeeper.ZRC20BalanceOf(ts.ctx, zrc20ABI, ts.zrc20Address, owner) + require.NoError(t, err) + require.Equal(t, uint64(801), ownerBalance.Uint64()) + + lockerBalance, err := ts.fungibleKeeper.ZRC20BalanceOf(ts.ctx, zrc20ABI, ts.zrc20Address, locker) + require.NoError(t, err) + require.Equal(t, uint64(199), lockerBalance.Uint64()) + }) +} + +func Test_UnlockZRC20(t *testing.T) { + zrc20ABI, err := zrc20.ZRC20MetaData.GetAbi() + require.NoError(t, err) + + ts := setupChain(t) + + owner := fungibletypes.ModuleAddressZEVM + locker := sample.EthAddress() + depositTotal := big.NewInt(1000) + allowanceTotal := big.NewInt(100) + + // Make sure locker account exists in state. + accAddress := sdk.AccAddress(locker.Bytes()) + ts.fungibleKeeper.GetAuthKeeper().SetAccount(ts.ctx, authtypes.NewBaseAccount(accAddress, nil, 0, 0)) + + // Deposit 1000 ZRC20 tokens into the fungible. + ts.fungibleKeeper.DepositZRC20(ts.ctx, ts.zrc20Address, owner, depositTotal) + + // Approve allowance for locker to spend owner's ZRC20 tokens. + approveAllowance(t, ts, zrc20ABI, owner, locker, allowanceTotal) + + // Lock 100 ZRC20. + err = ts.fungibleKeeper.LockZRC20(ts.ctx, zrc20ABI, ts.zrc20Address, owner, locker, allowanceTotal) + require.NoError(t, err) + + t.Run("should fail when trying to unlock zero amount", func(t *testing.T) { + err = ts.fungibleKeeper.UnlockZRC20(ts.ctx, zrc20ABI, ts.zrc20Address, owner, locker, big.NewInt(0)) + require.Error(t, err) + require.ErrorIs(t, err, fungibletypes.ErrInvalidAmount) + }) + + t.Run("should fail when ZRC20 ABI is not properly initialized", func(t *testing.T) { + err = ts.fungibleKeeper.UnlockZRC20(ts.ctx, nil, ts.zrc20Address, owner, locker, big.NewInt(10)) + require.Error(t, err) + require.ErrorIs(t, err, fungibletypes.ErrZRC20NilABI) + }) + + t.Run("should fail when trying to unlock a zero address ZRC20", func(t *testing.T) { + err = ts.fungibleKeeper.UnlockZRC20(ts.ctx, zrc20ABI, common.Address{}, owner, locker, big.NewInt(10)) + require.Error(t, err) + require.ErrorIs(t, err, fungibletypes.ErrZRC20ZeroAddress) + }) + + t.Run("should fail when trying to unlock a non whitelisted ZRC20", func(t *testing.T) { + err = ts.fungibleKeeper.UnlockZRC20(ts.ctx, zrc20ABI, sample.EthAddress(), owner, locker, big.NewInt(10)) + require.Error(t, err) + require.ErrorIs(t, err, fungibletypes.ErrZRC20NotWhiteListed) + }) + + t.Run("should fail when trying to unlock an amount bigger than locker's balance", func(t *testing.T) { + err = ts.fungibleKeeper.UnlockZRC20(ts.ctx, zrc20ABI, ts.zrc20Address, owner, locker, big.NewInt(1001)) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid balance, got: 100") + }) + + t.Run("should pass when trying to unlock a correct amount", func(t *testing.T) { + err = ts.fungibleKeeper.UnlockZRC20(ts.ctx, zrc20ABI, ts.zrc20Address, owner, locker, allowanceTotal) + require.NoError(t, err) + + ownerBalance, err := ts.fungibleKeeper.ZRC20BalanceOf(ts.ctx, zrc20ABI, ts.zrc20Address, owner) + require.NoError(t, err) + require.Equal(t, uint64(1000), ownerBalance.Uint64()) + + lockerBalance, err := ts.fungibleKeeper.ZRC20BalanceOf(ts.ctx, zrc20ABI, ts.zrc20Address, locker) + require.NoError(t, err) + require.Equal(t, uint64(0), lockerBalance.Uint64()) + }) +} + +func Test_CheckZRC20Allowance(t *testing.T) { + zrc20ABI, err := zrc20.ZRC20MetaData.GetAbi() + require.NoError(t, err) + + ts := setupChain(t) + + owner := fungibletypes.ModuleAddressZEVM + spender := sample.EthAddress() + depositTotal := big.NewInt(1000) + allowanceTotal := big.NewInt(100) + higherThanAllowance := big.NewInt(101) + smallerThanAllowance := big.NewInt(99) + + // Make sure locker account exists in state. + accAddress := sdk.AccAddress(spender.Bytes()) + ts.fungibleKeeper.GetAuthKeeper().SetAccount(ts.ctx, authtypes.NewBaseAccount(accAddress, nil, 0, 0)) + + // Deposit ZRC20 tokens into the fungible. + ts.fungibleKeeper.DepositZRC20(ts.ctx, ts.zrc20Address, fungibletypes.ModuleAddressZEVM, depositTotal) + + t.Run("should fail when checking zero amount", func(t *testing.T) { + err = ts.fungibleKeeper.CheckZRC20Allowance(ts.ctx, zrc20ABI, owner, spender, ts.zrc20Address, big.NewInt(0)) + require.Error(t, err) + require.ErrorAs(t, err, &fungibletypes.ErrInvalidAmount) + }) + + t.Run("should fail when allowance is not approved", func(t *testing.T) { + err = ts.fungibleKeeper.CheckZRC20Allowance(ts.ctx, zrc20ABI, owner, spender, ts.zrc20Address, big.NewInt(10)) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid allowance, got: 0") + }) + + t.Run("should fail when checking a higher amount than approved", func(t *testing.T) { + approveAllowance(t, ts, zrc20ABI, owner, spender, allowanceTotal) + + err = ts.fungibleKeeper.CheckZRC20Allowance( + ts.ctx, + zrc20ABI, + owner, + spender, + ts.zrc20Address, + higherThanAllowance, + ) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid allowance, got: 100") + }) + + t.Run("should pass when checking the same amount as approved", func(t *testing.T) { + approveAllowance(t, ts, zrc20ABI, owner, spender, allowanceTotal) + + err = ts.fungibleKeeper.CheckZRC20Allowance(ts.ctx, zrc20ABI, owner, spender, ts.zrc20Address, allowanceTotal) + require.NoError(t, err) + }) + + t.Run("should pass when checking a lower amount than approved", func(t *testing.T) { + approveAllowance(t, ts, zrc20ABI, owner, spender, allowanceTotal) + + err = ts.fungibleKeeper.CheckZRC20Allowance( + ts.ctx, + zrc20ABI, + owner, + spender, + ts.zrc20Address, + smallerThanAllowance, + ) + require.NoError(t, err) + }) +} + +func Test_IsValidZRC20(t *testing.T) { + ts := setupChain(t) + + t.Run("should fail when zrc20 address is zero", func(t *testing.T) { + err := ts.fungibleKeeper.IsValidZRC20(ts.ctx, common.Address{}) + require.Error(t, err) + require.ErrorAs(t, err, &fungibletypes.ErrZeroAddress) + }) + + t.Run("should fail when zrc20 is not whitelisted", func(t *testing.T) { + err := ts.fungibleKeeper.IsValidZRC20(ts.ctx, sample.EthAddress()) + require.Error(t, err) + require.ErrorAs(t, err, &fungibletypes.ErrZRC20NotWhiteListed) + }) + + t.Run("should pass when zrc20 is a valid whitelisted token", func(t *testing.T) { + err := ts.fungibleKeeper.IsValidZRC20(ts.ctx, ts.zrc20Address) + require.NoError(t, err) + }) +} + +func Test_IsValidDepositAmount(t *testing.T) { + ts := setupChain(t) + + t.Run("should fail when any input is nil", func(t *testing.T) { + isValid := ts.fungibleKeeper.IsValidDepositAmount(nil, big.NewInt(0), big.NewInt(0)) + require.False(t, isValid) + + isValid = ts.fungibleKeeper.IsValidDepositAmount(big.NewInt(0), nil, big.NewInt(0)) + require.False(t, isValid) + + isValid = ts.fungibleKeeper.IsValidDepositAmount(big.NewInt(0), big.NewInt(0), nil) + require.False(t, isValid) + }) + + t.Run("should fail when alreadyLocked + amountToDeposit > totalSupply", func(t *testing.T) { + isValid := ts.fungibleKeeper.IsValidDepositAmount(big.NewInt(1000), big.NewInt(500), big.NewInt(501)) + require.False(t, isValid) + }) + + t.Run("should pass when alreadyLocked + amountToDeposit = totalSupply", func(t *testing.T) { + isValid := ts.fungibleKeeper.IsValidDepositAmount(big.NewInt(1000), big.NewInt(500), big.NewInt(500)) + require.True(t, isValid) + }) + + t.Run("should pass when alreadyLocked + amountToDeposit < totalSupply", func(t *testing.T) { + isValid := ts.fungibleKeeper.IsValidDepositAmount(big.NewInt(1000), big.NewInt(500), big.NewInt(499)) + require.True(t, isValid) + }) +} + +/* + Test utils. +*/ + +type testSuite struct { + ctx sdk.Context + fungibleKeeper *fungiblekeeper.Keeper + sdkKeepers keeper.SDKKeepers + 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") + + return testSuite{ + ctx, + fungibleKeeper, + sdkKeepers, + zrc20Address, + } +} + +func approveAllowance(t *testing.T, ts testSuite, zrc20ABI *abi.ABI, owner, spender common.Address, amount *big.Int) { + resAllowance, err := callEVM( + t, + ts.ctx, + ts.fungibleKeeper, + zrc20ABI, + owner, + ts.zrc20Address, + "approve", + []interface{}{spender, amount}, + ) + require.NoError(t, err, "error allowing bank to spend ZRC20 tokens") + + allowed, ok := resAllowance[0].(bool) + require.True(t, ok) + require.True(t, allowed) +} + +func callEVM( + t *testing.T, + ctx sdk.Context, + fungibleKeeper *fungiblekeeper.Keeper, + abi *abi.ABI, + from common.Address, + dst common.Address, + method string, + args []interface{}, +) ([]interface{}, error) { + res, err := fungibleKeeper.CallEVM( + ctx, // ctx + *abi, // abi + from, // from + dst, // to + big.NewInt(0), // value + nil, // gasLimit + true, // commit + true, // noEthereumTxEvent + method, // method + args..., // args + ) + require.NoError(t, err, "CallEVM error") + require.Equal(t, "", res.VmError, "res.VmError should be empty") + + ret, err := abi.Methods[method].Outputs.Unpack(res.Ret) + require.NoError(t, err, "Unpack error") + + return ret, nil +} diff --git a/x/fungible/keeper/zrc20_cosmos_coins_mapping.go b/x/fungible/keeper/zrc20_cosmos_coins_mapping.go index 4d5e86ae21..2013514758 100644 --- a/x/fungible/keeper/zrc20_cosmos_coins_mapping.go +++ b/x/fungible/keeper/zrc20_cosmos_coins_mapping.go @@ -12,228 +12,110 @@ import ( fungibletypes "github.com/zeta-chain/node/x/fungible/types" ) -const ( - transferFrom = "transferFrom" - transfer = "transfer" - balanceOf = "balanceOf" - allowance = "allowance" -) - -var ( - ErrZRC20ZeroAddress = fmt.Errorf("ZRC20 address cannot be zero") - ErrZRC20NotWhiteListed = fmt.Errorf("ZRC20 is not whitelisted") - ErrZRC20Paused = fmt.Errorf("ZRC20 is paused") - ErrZRC20NilABI = fmt.Errorf("ZRC20 ABI is nil") - ErrZeroAddress = fmt.Errorf("address cannot be zero") - ErrInvalidAmount = fmt.Errorf("amount must be positive") -) - -// LockZRC20 locks ZRC20 tokens in the bank contract. -// The caller must have approved the bank contract to spend the amount of ZRC20 tokens. +// LockZRC20 locks ZRC20 tokens in the specified address +// The caller must have approved the locker contract to spend the amount of ZRC20 tokens. +// Warning: This function does not mint cosmos coins, if the depositor needs to be rewarded +// it has to be implemented by the caller of this function. func (k Keeper) LockZRC20( ctx sdk.Context, zrc20ABI *abi.ABI, - zrc20Address, from common.Address, + zrc20Address, owner, locker common.Address, amount *big.Int, ) error { - if zrc20ABI == nil { - return ErrZRC20NilABI - } - - if amount.Sign() <= 0 || amount == nil { - return ErrInvalidAmount - } - - if crypto.IsEmptyAddress(from) { - return ErrZeroAddress - } - - if crypto.IsEmptyAddress(zrc20Address) { - return ErrZRC20ZeroAddress - } - - if err := k.IsValidZRC20(ctx, zrc20Address); err != nil { + // owner is the EOA owner of the ZRC20 tokens. + // locker is the address that will lock the ZRC20 tokens, i.e: bank precompile. + if err := k.CheckZRC20Allowance(ctx, zrc20ABI, owner, locker, zrc20Address, amount); err != nil { return err } - if err := k.CheckFungibleZRC20Allowance(ctx, zrc20ABI, from, zrc20Address, amount); err != nil { + // Check amount_to_be_locked <= total_erc20_balance - already_locked + // Max amount of ZRC20 tokens that exists in zEVM are the total supply. + totalSupply, err := k.ZRC20TotalSupply(ctx, zrc20ABI, zrc20Address) + if err != nil { return err } - args := []interface{}{from, fungibletypes.ModuleAddressZEVM, amount} - res, err := k.CallEVM( - ctx, - *zrc20ABI, - fungibletypes.ModuleAddressZEVM, - zrc20Address, - big.NewInt(0), - nil, - true, - true, - transferFrom, - args..., - ) + // The alreadyLocked amount is the amount of ZRC20 tokens that have been locked by the locker. + // TODO: Implement list of whitelisted locker addresses. + alreadyLocked, err := k.ZRC20BalanceOf(ctx, zrc20ABI, zrc20Address, locker) if err != nil { return err } - if res.VmError != "" { - return fmt.Errorf("EVM execution error in LockZRC20: %s", res.VmError) + if !k.IsValidDepositAmount(totalSupply, alreadyLocked, amount) { + return fungibletypes.ErrInvalidAmount } - ret, err := zrc20ABI.Methods[transferFrom].Outputs.Unpack(res.Ret) + // Initiate a transferFrom the owner to the locker. This will lock the ZRC20 tokens. + // locker has to initiate the transaction and have enough allowance from owner. + transferred, err := k.ZRC20TransferFrom(ctx, zrc20ABI, zrc20Address, owner, locker, amount) if err != nil { return err } - if len(ret) == 0 { - return fmt.Errorf("no data returned from 'transferFrom' method") - } - - transferred, ok := ret[0].(bool) - if !ok { - return fmt.Errorf("transferFrom returned an unexpected value") - } - if !transferred { - return fmt.Errorf("transferFrom not successful") + return fmt.Errorf("lock ZRC20 not successful") } return nil } -// UnlockZRC20 unlocks ZRC20 tokens and sends them to the "to" address. +// UnlockZRC20 unlocks ZRC20 tokens and sends them to the owner. +// Warning: Before unlocking ZRC20 tokens, the caller must check if +// the owner has enough collateral (cosmos coins) to be exchanged (burnt) for the ZRC20 tokens. func (k Keeper) UnlockZRC20( ctx sdk.Context, zrc20ABI *abi.ABI, - zrc20Address, to common.Address, + zrc20Address, owner, locker common.Address, amount *big.Int, ) error { - if zrc20ABI == nil { - return ErrZRC20NilABI - } - - if amount.Sign() <= 0 || amount == nil { - return ErrInvalidAmount - } - - if crypto.IsEmptyAddress(to) { - return ErrZeroAddress - } - - if crypto.IsEmptyAddress(zrc20Address) { - return ErrZRC20ZeroAddress - } - - if err := k.IsValidZRC20(ctx, zrc20Address); err != nil { - return err - } - - if err := k.CheckFungibleZRC20Balance(ctx, zrc20ABI, zrc20Address, amount); err != nil { + // Check if the account locking the ZRC20 tokens has enough balance. + if err := k.CheckZRC20Balance(ctx, zrc20ABI, zrc20Address, locker, amount); err != nil { return err } - args := []interface{}{to, amount} - res, err := k.CallEVM( - ctx, - *zrc20ABI, - fungibletypes.ModuleAddressZEVM, - zrc20Address, - big.NewInt(0), - nil, - true, - true, - transfer, - args..., - ) + // transfer from the EOA locking the assets to the owner. + transferred, err := k.ZRC20Transfer(ctx, zrc20ABI, zrc20Address, locker, owner, amount) if err != nil { return err } - if res.VmError != "" { - return fmt.Errorf("EVM execution error in UnlockZRC20: %s", res.VmError) - } - - ret, err := zrc20ABI.Methods[transfer].Outputs.Unpack(res.Ret) - if err != nil { - return err - } - - if len(ret) == 0 { - return fmt.Errorf("no data returned from 'transfer' method") - } - - transferred, ok := ret[0].(bool) - if !ok { - return fmt.Errorf("transfer returned an unexpected value") - } - if !transferred { - return fmt.Errorf("transfer not successful") + return fmt.Errorf("unlock ZRC20 not successful") } return nil } -// CheckFungibleZRC20Allowance checks if the allowance of ZRC20 tokens, +// CheckZRC20Allowance checks if the allowance of ZRC20 tokens, // is equal or greater than the provided amount. -func (k Keeper) CheckFungibleZRC20Allowance( +func (k Keeper) CheckZRC20Allowance( ctx sdk.Context, zrc20ABI *abi.ABI, - from, zrc20Address common.Address, + owner, spender, zrc20Address common.Address, amount *big.Int, ) error { if zrc20ABI == nil { - return ErrZRC20NilABI + return fungibletypes.ErrZRC20NilABI } if amount.Sign() <= 0 || amount == nil { - return ErrInvalidAmount + return fungibletypes.ErrInvalidAmount } - if crypto.IsEmptyAddress(from) { - return ErrZeroAddress - } - - if crypto.IsEmptyAddress(zrc20Address) { - return ErrZRC20ZeroAddress + if crypto.IsEmptyAddress(owner) || crypto.IsEmptyAddress(spender) { + return fungibletypes.ErrZeroAddress } - args := []interface{}{from, fungibletypes.ModuleAddressZEVM} - res, err := k.CallEVM( - ctx, - *zrc20ABI, - fungibletypes.ModuleAddressZEVM, - zrc20Address, - big.NewInt(0), - nil, - true, - true, - allowance, - args..., - ) - if err != nil { + if err := k.IsValidZRC20(ctx, zrc20Address); err != nil { return err } - if res.VmError != "" { - return fmt.Errorf("EVM execution error calling allowance: %s", res.VmError) - } - - ret, err := zrc20ABI.Methods[allowance].Outputs.Unpack(res.Ret) + allowanceValue, err := k.ZRC20Allowance(ctx, zrc20ABI, zrc20Address, owner, spender) if err != nil { return err } - if len(ret) == 0 { - return fmt.Errorf("no data returned from 'allowance' method") - } - - allowanceValue, ok := ret[0].(*big.Int) - if !ok { - return fmt.Errorf("ZRC20 allowance returned an unexpected type") - } - if allowanceValue.Cmp(amount) < 0 || allowanceValue.Cmp(big.NewInt(0)) <= 0 { return fmt.Errorf("invalid allowance, got: %s", allowanceValue.String()) } @@ -241,60 +123,37 @@ func (k Keeper) CheckFungibleZRC20Allowance( return nil } -// CheckFungibleZRC20Balance checks if the balance of ZRC20 tokens, +// CheckZRC20Balance checks if the balance of ZRC20 tokens, // is equal or greater than the provided amount. -func (k Keeper) CheckFungibleZRC20Balance( +func (k Keeper) CheckZRC20Balance( ctx sdk.Context, zrc20ABI *abi.ABI, - zrc20Address common.Address, + zrc20Address, owner common.Address, amount *big.Int, ) error { if zrc20ABI == nil { - return ErrZRC20NilABI + return fungibletypes.ErrZRC20NilABI } if amount.Sign() <= 0 || amount == nil { - return ErrInvalidAmount + return fungibletypes.ErrInvalidAmount } - if crypto.IsEmptyAddress(zrc20Address) { - return ErrZRC20ZeroAddress - } - - res, err := k.CallEVM( - ctx, - *zrc20ABI, - fungibletypes.ModuleAddressZEVM, - zrc20Address, - big.NewInt(0), - nil, - true, - true, - balanceOf, - fungibletypes.ModuleAddressZEVM, - ) - if err != nil { + if err := k.IsValidZRC20(ctx, zrc20Address); err != nil { return err } - if res.VmError != "" { - return fmt.Errorf("EVM execution error calling balanceOf: %s", res.VmError) + if crypto.IsEmptyAddress(owner) { + return fungibletypes.ErrZeroAddress } - ret, err := zrc20ABI.Methods[balanceOf].Outputs.Unpack(res.Ret) + // Check the ZRC20 balance of a given account. + // function balanceOf(address account) + balance, err := k.ZRC20BalanceOf(ctx, zrc20ABI, zrc20Address, owner) if err != nil { return err } - if len(ret) == 0 { - return fmt.Errorf("no data returned from 'balanceOf' method") - } - - balance, ok := ret[0].(*big.Int) - if !ok { - return fmt.Errorf("ZRC20 balanceOf returned an unexpected type") - } - if balance.Cmp(amount) == -1 { return fmt.Errorf("invalid balance, got: %s", balance.String()) } @@ -305,17 +164,28 @@ func (k Keeper) CheckFungibleZRC20Balance( // IsValidZRC20 returns an error whenever a ZRC20 is not whitelisted or paused. func (k Keeper) IsValidZRC20(ctx sdk.Context, zrc20Address common.Address) error { if crypto.IsEmptyAddress(zrc20Address) { - return ErrZRC20ZeroAddress + return fungibletypes.ErrZRC20ZeroAddress } t, found := k.GetForeignCoins(ctx, zrc20Address.String()) if !found { - return ErrZRC20NotWhiteListed + return fungibletypes.ErrZRC20NotWhiteListed } if t.Paused { - return ErrZRC20Paused + return fungibletypes.ErrPausedZRC20 } return nil } + +// IsValidDepositAmount checks "totalSupply >= amount_to_be_deposited + amount_already_locked". +// A failure here means the user is trying to lock more than the available ZRC20 supply. +// This suggests that an actor is minting ZRC20 tokens out of thin air. +func (k Keeper) IsValidDepositAmount(totalSupply, alreadyLocked, amountToDeposit *big.Int) bool { + if totalSupply == nil || alreadyLocked == nil || amountToDeposit == nil { + return false + } + + return totalSupply.Cmp(alreadyLocked.Add(alreadyLocked, amountToDeposit)) >= 0 +} diff --git a/x/fungible/keeper/zrc20_methods.go b/x/fungible/keeper/zrc20_methods.go new file mode 100644 index 0000000000..b3b636dfe4 --- /dev/null +++ b/x/fungible/keeper/zrc20_methods.go @@ -0,0 +1,301 @@ +package keeper + +import ( + "fmt" + "math/big" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + + "github.com/zeta-chain/node/pkg/crypto" + fungibletypes "github.com/zeta-chain/node/x/fungible/types" +) + +const ( + allowance = "allowance" + balanceOf = "balanceOf" + totalSupply = "totalSupply" + transfer = "transfer" + transferFrom = "transferFrom" +) + +// ZRC20Allowance returns the ZRC20 allowance for a given spender. +func (k Keeper) ZRC20Allowance( + ctx sdk.Context, + zrc20ABI *abi.ABI, + zrc20Address, owner, spender common.Address, +) (*big.Int, error) { + if zrc20ABI == nil { + return nil, fungibletypes.ErrZRC20NilABI + } + + if crypto.IsEmptyAddress(owner) || crypto.IsEmptyAddress(spender) { + return nil, fungibletypes.ErrZeroAddress + } + + if err := k.IsValidZRC20(ctx, zrc20Address); err != nil { + return nil, err + } + + // function allowance(address owner, address spender) + args := []interface{}{owner, spender} + res, err := k.CallEVM( + ctx, + *zrc20ABI, + fungibletypes.ModuleAddressZEVM, + zrc20Address, + big.NewInt(0), + nil, + true, + true, + allowance, + args..., + ) + if err != nil { + return nil, err + } + + if res.VmError != "" { + return nil, fmt.Errorf("EVM execution error calling allowance: %s", res.VmError) + } + + ret, err := zrc20ABI.Methods[allowance].Outputs.Unpack(res.Ret) + if err != nil { + return nil, err + } + + if len(ret) == 0 { + return nil, fmt.Errorf("no data returned from 'allowance' method") + } + + allowanceValue, ok := ret[0].(*big.Int) + if !ok { + return nil, fmt.Errorf("ZRC20 allowance returned an unexpected type") + } + + return allowanceValue, nil +} + +// ZRC20BalanceOf checks the ZRC20 balance of a given EOA. +func (k Keeper) ZRC20BalanceOf( + ctx sdk.Context, + zrc20ABI *abi.ABI, + zrc20Address, owner common.Address, +) (*big.Int, error) { + if zrc20ABI == nil { + return nil, fungibletypes.ErrZRC20NilABI + } + + if crypto.IsEmptyAddress(owner) { + return nil, fungibletypes.ErrZeroAddress + } + + if err := k.IsValidZRC20(ctx, zrc20Address); err != nil { + return nil, err + } + + res, err := k.CallEVM( + ctx, + *zrc20ABI, + fungibletypes.ModuleAddressZEVM, + zrc20Address, + big.NewInt(0), + nil, + true, + true, + balanceOf, + owner, + ) + if err != nil { + return nil, err + } + + if res.VmError != "" { + return nil, fmt.Errorf("EVM execution error calling balanceOf: %s", res.VmError) + } + + ret, err := zrc20ABI.Methods[balanceOf].Outputs.Unpack(res.Ret) + if err != nil { + return nil, err + } + + if len(ret) == 0 { + return nil, fmt.Errorf("no data returned from 'balanceOf' method") + } + + balance, ok := ret[0].(*big.Int) + if !ok { + return nil, fmt.Errorf("ZRC20 balanceOf returned an unexpected type") + } + + return balance, nil +} + +// ZRC20TotalSupply returns the total supply of a ZRC20 token. +func (k Keeper) ZRC20TotalSupply( + ctx sdk.Context, + zrc20ABI *abi.ABI, + zrc20Address common.Address, +) (*big.Int, error) { + if zrc20ABI == nil { + return nil, fungibletypes.ErrZRC20NilABI + } + + if err := k.IsValidZRC20(ctx, zrc20Address); err != nil { + return nil, err + } + + // function totalSupply() public view virtual override returns (uint256) + res, err := k.CallEVM( + ctx, + *zrc20ABI, + fungibletypes.ModuleAddressZEVM, + zrc20Address, + big.NewInt(0), + nil, + true, + true, + totalSupply, + ) + if err != nil { + return nil, err + } + + if res.VmError != "" { + return nil, fmt.Errorf("EVM execution error calling totalSupply: %s", res.VmError) + } + + ret, err := zrc20ABI.Methods[totalSupply].Outputs.Unpack(res.Ret) + if err != nil { + return nil, err + } + + if len(ret) == 0 { + return nil, fmt.Errorf("no data returned from 'totalSupply' method") + } + + totalSupply, ok := ret[0].(*big.Int) + if !ok { + return nil, fmt.Errorf("ZRC20 totalSupply returned an unexpected type") + } + + return totalSupply, nil +} + +// ZRC20Transfer transfers ZRC20 tokens from the sender to the recipient. +func (k Keeper) ZRC20Transfer( + ctx sdk.Context, + zrc20ABI *abi.ABI, + zrc20Address, from, to common.Address, + amount *big.Int, +) (bool, error) { + if zrc20ABI == nil { + return false, fungibletypes.ErrZRC20NilABI + } + + if crypto.IsEmptyAddress(from) || crypto.IsEmptyAddress(to) { + return false, fungibletypes.ErrZeroAddress + } + + if err := k.IsValidZRC20(ctx, zrc20Address); err != nil { + return false, err + } + + // transfer from the EOA locking the assets to the owner. + args := []interface{}{to, amount} + res, err := k.CallEVM( + ctx, + *zrc20ABI, + from, + zrc20Address, + big.NewInt(0), + nil, + true, + true, + transfer, + args..., + ) + if err != nil { + return false, err + } + + if res.VmError != "" { + return false, fmt.Errorf("EVM execution error in transfer: %s", res.VmError) + } + + ret, err := zrc20ABI.Methods[transfer].Outputs.Unpack(res.Ret) + if err != nil { + return false, err + } + + if len(ret) == 0 { + return false, fmt.Errorf("no data returned from 'transfer' method") + } + + transferred, ok := ret[0].(bool) + if !ok { + return false, fmt.Errorf("transfer returned an unexpected value") + } + + return transferred, nil +} + +// ZRC20TransferFrom transfers ZRC20 tokens from the owner to the spender. +// The transaction is started by the spender. +// This requires the spender to have been approved by the owner. +func (k Keeper) ZRC20TransferFrom( + ctx sdk.Context, + zrc20ABI *abi.ABI, + zrc20Address, from, to common.Address, + amount *big.Int, +) (bool, error) { + if zrc20ABI == nil { + return false, fungibletypes.ErrZRC20NilABI + } + + if crypto.IsEmptyAddress(from) || crypto.IsEmptyAddress(to) { + return false, fungibletypes.ErrZeroAddress + } + + if err := k.IsValidZRC20(ctx, zrc20Address); err != nil { + return false, err + } + + args := []interface{}{from, to, amount} + res, err := k.CallEVM( + ctx, + *zrc20ABI, + to, + zrc20Address, + big.NewInt(0), + nil, + true, + true, + transferFrom, + args..., + ) + if err != nil { + return false, err + } + + if res.VmError != "" { + return false, fmt.Errorf("EVM execution error in transferFrom: %s", res.VmError) + } + + ret, err := zrc20ABI.Methods[transferFrom].Outputs.Unpack(res.Ret) + if err != nil { + return false, err + } + + if len(ret) == 0 { + return false, fmt.Errorf("no data returned from 'transferFrom' method") + } + + transferred, ok := ret[0].(bool) + if !ok { + return false, fmt.Errorf("transferFrom returned an unexpected value") + } + + return transferred, nil +} diff --git a/x/fungible/keeper/zrc20_methods_test.go b/x/fungible/keeper/zrc20_methods_test.go new file mode 100644 index 0000000000..2fb6992508 --- /dev/null +++ b/x/fungible/keeper/zrc20_methods_test.go @@ -0,0 +1,321 @@ +package keeper_test + +import ( + "math/big" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/testutil/sample" + fungibletypes "github.com/zeta-chain/node/x/fungible/types" + "github.com/zeta-chain/protocol-contracts/v2/pkg/zrc20.sol" +) + +func Test_ZRC20Allowance(t *testing.T) { + // Instantiate the ZRC20 ABI only one time. + // This avoids instantiating it every time deposit or withdraw are called. + zrc20ABI, err := zrc20.ZRC20MetaData.GetAbi() + require.NoError(t, err) + + ts := setupChain(t) + + t.Run("should fail when ZRC20ABI is nil", func(t *testing.T) { + _, err := ts.fungibleKeeper.ZRC20Allowance(ts.ctx, nil, ts.zrc20Address, common.Address{}, common.Address{}) + require.Error(t, err) + require.ErrorAs(t, err, &fungibletypes.ErrZRC20NilABI) + }) + + t.Run("should fail when owner is zero address", func(t *testing.T) { + _, err := ts.fungibleKeeper.ZRC20Allowance( + ts.ctx, + zrc20ABI, + ts.zrc20Address, + common.Address{}, + sample.EthAddress(), + ) + require.Error(t, err) + require.ErrorAs(t, err, &fungibletypes.ErrZeroAddress) + }) + + t.Run("should fail when spender is zero address", func(t *testing.T) { + _, err := ts.fungibleKeeper.ZRC20Allowance( + ts.ctx, + zrc20ABI, + ts.zrc20Address, + sample.EthAddress(), + common.Address{}, + ) + require.Error(t, err) + require.ErrorAs(t, err, &fungibletypes.ErrZeroAddress) + }) + + t.Run("should fail when zrc20 address is zero address", func(t *testing.T) { + _, err := ts.fungibleKeeper.ZRC20Allowance( + ts.ctx, + zrc20ABI, + common.Address{}, + sample.EthAddress(), + fungibletypes.ModuleAddressZEVM, + ) + require.Error(t, err) + require.ErrorAs(t, err, &fungibletypes.ErrZRC20ZeroAddress) + }) + + t.Run("should pass with correct input", func(t *testing.T) { + allowance, err := ts.fungibleKeeper.ZRC20Allowance( + ts.ctx, + zrc20ABI, + ts.zrc20Address, + fungibletypes.ModuleAddressZEVM, + sample.EthAddress(), + ) + require.NoError(t, err) + require.Equal(t, uint64(0), allowance.Uint64()) + }) +} + +func Test_ZRC20BalanceOf(t *testing.T) { + // Instantiate the ZRC20 ABI only one time. + // This avoids instantiating it every time deposit or withdraw are called. + zrc20ABI, err := zrc20.ZRC20MetaData.GetAbi() + require.NoError(t, err) + + ts := setupChain(t) + + t.Run("should fail when ZRC20ABI is nil", func(t *testing.T) { + _, err := ts.fungibleKeeper.ZRC20BalanceOf(ts.ctx, nil, ts.zrc20Address, common.Address{}) + require.Error(t, err) + require.ErrorAs(t, err, &fungibletypes.ErrZRC20NilABI) + }) + + t.Run("should fail when owner is zero address", func(t *testing.T) { + _, err := ts.fungibleKeeper.ZRC20BalanceOf(ts.ctx, zrc20ABI, ts.zrc20Address, common.Address{}) + require.Error(t, err) + require.ErrorAs(t, err, &fungibletypes.ErrZeroAddress) + }) + + t.Run("should fail when zrc20 address is zero address", func(t *testing.T) { + _, err := ts.fungibleKeeper.ZRC20BalanceOf(ts.ctx, zrc20ABI, common.Address{}, sample.EthAddress()) + require.Error(t, err) + require.ErrorAs(t, err, &fungibletypes.ErrZRC20ZeroAddress) + }) + + t.Run("should pass with correct input", func(t *testing.T) { + balance, err := ts.fungibleKeeper.ZRC20BalanceOf( + ts.ctx, + zrc20ABI, + ts.zrc20Address, + fungibletypes.ModuleAddressZEVM, + ) + require.NoError(t, err) + require.Equal(t, uint64(0), balance.Uint64()) + }) +} + +func Test_ZRC20TotalSupply(t *testing.T) { + // Instantiate the ZRC20 ABI only one time. + // This avoids instantiating it every time deposit or withdraw are called. + zrc20ABI, err := zrc20.ZRC20MetaData.GetAbi() + require.NoError(t, err) + + ts := setupChain(t) + + t.Run("should fail when ZRC20ABI is nil", func(t *testing.T) { + _, err := ts.fungibleKeeper.ZRC20TotalSupply(ts.ctx, nil, ts.zrc20Address) + require.Error(t, err) + require.ErrorAs(t, err, &fungibletypes.ErrZRC20NilABI) + }) + + t.Run("should fail when zrc20 address is zero address", func(t *testing.T) { + _, err := ts.fungibleKeeper.ZRC20TotalSupply(ts.ctx, zrc20ABI, common.Address{}) + require.Error(t, err) + require.ErrorAs(t, err, &fungibletypes.ErrZRC20ZeroAddress) + }) + + t.Run("should pass with correct input", func(t *testing.T) { + totalSupply, err := ts.fungibleKeeper.ZRC20TotalSupply(ts.ctx, zrc20ABI, ts.zrc20Address) + require.NoError(t, err) + require.Equal(t, uint64(10000000), totalSupply.Uint64()) + }) +} + +func Test_ZRC20Transfer(t *testing.T) { + // Instantiate the ZRC20 ABI only one time. + // This avoids instantiating it every time deposit or withdraw are called. + zrc20ABI, err := zrc20.ZRC20MetaData.GetAbi() + require.NoError(t, err) + + ts := setupChain(t) + + // Make sure sample.EthAddress() exists as an ethermint account in state. + accAddress := sdk.AccAddress(sample.EthAddress().Bytes()) + ts.fungibleKeeper.GetAuthKeeper().SetAccount(ts.ctx, authtypes.NewBaseAccount(accAddress, nil, 0, 0)) + + t.Run("should fail when ZRC20ABI is nil", func(t *testing.T) { + _, err := ts.fungibleKeeper.ZRC20Transfer( + ts.ctx, + nil, + ts.zrc20Address, + common.Address{}, + common.Address{}, + big.NewInt(0), + ) + require.Error(t, err) + require.ErrorAs(t, err, &fungibletypes.ErrZRC20NilABI) + }) + + t.Run("should fail when owner is zero address", func(t *testing.T) { + _, err := ts.fungibleKeeper.ZRC20Transfer( + ts.ctx, + zrc20ABI, + ts.zrc20Address, + common.Address{}, + sample.EthAddress(), + big.NewInt(0), + ) + require.Error(t, err) + require.ErrorAs(t, err, &fungibletypes.ErrZeroAddress) + }) + + t.Run("should fail when spender is zero address", func(t *testing.T) { + _, err := ts.fungibleKeeper.ZRC20Transfer( + ts.ctx, + zrc20ABI, + ts.zrc20Address, + sample.EthAddress(), + common.Address{}, + big.NewInt(0), + ) + require.Error(t, err) + require.ErrorAs(t, err, &fungibletypes.ErrZeroAddress) + }) + + t.Run("should fail when zrc20 address is zero address", func(t *testing.T) { + _, err := ts.fungibleKeeper.ZRC20Transfer( + ts.ctx, + zrc20ABI, + common.Address{}, + sample.EthAddress(), + fungibletypes.ModuleAddressZEVM, + big.NewInt(0), + ) + require.Error(t, err) + require.ErrorAs(t, err, &fungibletypes.ErrZRC20ZeroAddress) + }) + + t.Run("should pass with correct input", func(t *testing.T) { + ts.fungibleKeeper.DepositZRC20(ts.ctx, ts.zrc20Address, fungibletypes.ModuleAddressZEVM, big.NewInt(10)) + transferred, err := ts.fungibleKeeper.ZRC20Transfer( + ts.ctx, + zrc20ABI, + ts.zrc20Address, + fungibletypes.ModuleAddressZEVM, + sample.EthAddress(), + big.NewInt(10), + ) + require.NoError(t, err) + require.True(t, transferred) + }) +} + +func Test_ZRC20TransferFrom(t *testing.T) { + // Instantiate the ZRC20 ABI only one time. + // This avoids instantiating it every time deposit or withdraw are called. + zrc20ABI, err := zrc20.ZRC20MetaData.GetAbi() + require.NoError(t, err) + + ts := setupChain(t) + + // Make sure sample.EthAddress() exists as an ethermint account in state. + accAddress := sdk.AccAddress(sample.EthAddress().Bytes()) + ts.fungibleKeeper.GetAuthKeeper().SetAccount(ts.ctx, authtypes.NewBaseAccount(accAddress, nil, 0, 0)) + + t.Run("should fail when ZRC20ABI is nil", func(t *testing.T) { + _, err := ts.fungibleKeeper.ZRC20TransferFrom( + ts.ctx, + nil, + ts.zrc20Address, + common.Address{}, + common.Address{}, + big.NewInt(0), + ) + require.Error(t, err) + require.ErrorAs(t, err, &fungibletypes.ErrZRC20NilABI) + }) + + t.Run("should fail when owner is zero address", func(t *testing.T) { + _, err := ts.fungibleKeeper.ZRC20TransferFrom( + ts.ctx, + zrc20ABI, + ts.zrc20Address, + common.Address{}, + sample.EthAddress(), + big.NewInt(0), + ) + require.Error(t, err) + require.ErrorAs(t, err, &fungibletypes.ErrZeroAddress) + }) + + t.Run("should fail when spender is zero address", func(t *testing.T) { + _, err := ts.fungibleKeeper.ZRC20TransferFrom( + ts.ctx, + zrc20ABI, + ts.zrc20Address, + sample.EthAddress(), + common.Address{}, + big.NewInt(0), + ) + require.Error(t, err) + require.ErrorAs(t, err, &fungibletypes.ErrZeroAddress) + }) + + t.Run("should fail when zrc20 address is zero address", func(t *testing.T) { + _, err := ts.fungibleKeeper.ZRC20TransferFrom( + ts.ctx, + zrc20ABI, + common.Address{}, + sample.EthAddress(), + fungibletypes.ModuleAddressZEVM, + big.NewInt(0), + ) + require.Error(t, err) + require.ErrorAs(t, err, &fungibletypes.ErrZRC20ZeroAddress) + }) + + t.Run("should fail without an allowance approval", func(t *testing.T) { + // Deposit ZRC20 into fungible EOA. + ts.fungibleKeeper.DepositZRC20(ts.ctx, ts.zrc20Address, fungibletypes.ModuleAddressZEVM, big.NewInt(1000)) + + // Transferring the tokens with transferFrom without approval should fail. + _, err = ts.fungibleKeeper.ZRC20TransferFrom( + ts.ctx, + zrc20ABI, + ts.zrc20Address, + sample.EthAddress(), + fungibletypes.ModuleAddressZEVM, + big.NewInt(10), + ) + require.Error(t, err) + }) + + t.Run("should success with an allowance approval", func(t *testing.T) { + // Deposit ZRC20 into fungible EOA. + ts.fungibleKeeper.DepositZRC20(ts.ctx, ts.zrc20Address, fungibletypes.ModuleAddressZEVM, big.NewInt(1000)) + + // Approve allowance to sample.EthAddress() to spend 10 ZRC20 tokens. + approveAllowance(t, ts, zrc20ABI, fungibletypes.ModuleAddressZEVM, sample.EthAddress(), big.NewInt(10)) + + // Transferring the tokens with transferFrom without approval should fail. + _, err = ts.fungibleKeeper.ZRC20TransferFrom( + ts.ctx, + zrc20ABI, + ts.zrc20Address, + sample.EthAddress(), + fungibletypes.ModuleAddressZEVM, + big.NewInt(10), + ) + require.Error(t, err) + }) +} diff --git a/x/fungible/types/errors.go b/x/fungible/types/errors.go index 7e426a3178..cf333c9545 100644 --- a/x/fungible/types/errors.go +++ b/x/fungible/types/errors.go @@ -29,4 +29,9 @@ var ( ErrNilGasPrice = cosmoserrors.Register(ModuleName, 1127, "nil gas price") ErrAccountNotFound = cosmoserrors.Register(ModuleName, 1128, "account not found") ErrGatewayContractNotSet = cosmoserrors.Register(ModuleName, 1129, "gateway contract not set") + ErrZRC20ZeroAddress = cosmoserrors.Register(ModuleName, 1130, "ZRC20 address cannot be zero") + ErrZRC20NotWhiteListed = cosmoserrors.Register(ModuleName, 1131, "ZRC20 is not whitelisted") + ErrZRC20NilABI = cosmoserrors.Register(ModuleName, 1132, "ZRC20 ABI is nil") + ErrZeroAddress = cosmoserrors.Register(ModuleName, 1133, "address cannot be zero") + ErrInvalidAmount = cosmoserrors.Register(ModuleName, 1134, "invalid amount") ) From 10dcfd6e6c2551eaf19db37355db499c2798ff2d Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Fri, 11 Oct 2024 19:35:36 +0200 Subject: [PATCH 09/10] add reviewed changes --- precompiles/bank/method_deposit.go | 2 +- precompiles/bank/method_test.go | 8 +++- .../keeper/zrc20_cosmos_coin_mapping_test.go | 35 ++++++++++------ .../keeper/zrc20_cosmos_coins_mapping.go | 41 ++++++++++--------- x/fungible/keeper/zrc20_methods.go | 36 ++++++++-------- x/fungible/keeper/zrc20_methods_test.go | 24 ++++++++++- 6 files changed, 92 insertions(+), 54 deletions(-) diff --git a/precompiles/bank/method_deposit.go b/precompiles/bank/method_deposit.go index ae620c34a9..60f166de97 100644 --- a/precompiles/bank/method_deposit.go +++ b/precompiles/bank/method_deposit.go @@ -100,7 +100,7 @@ func (c *Contract) deposit( } // 2. Effect: subtract balance. - if err := c.fungibleKeeper.LockZRC20(ctx, c.zrc20ABI, zrc20Addr, caller, c.Address(), amount); err != nil { + if err := c.fungibleKeeper.LockZRC20(ctx, c.zrc20ABI, zrc20Addr, c.Address(), caller, c.Address(), amount); err != nil { return nil, &ptypes.ErrUnexpected{ When: "LockZRC20InBank", Got: err.Error(), diff --git a/precompiles/bank/method_test.go b/precompiles/bank/method_test.go index f6601bccbe..8921c787af 100644 --- a/precompiles/bank/method_test.go +++ b/precompiles/bank/method_test.go @@ -135,7 +135,7 @@ func Test_Methods(t *testing.T) { success, err := ts.bankContract.Run(ts.mockEVM, ts.mockVMContract, false) require.Error(t, err) - require.Contains(t, err.Error(), "invalid allowance, got: 0") + require.Contains(t, err.Error(), "invalid allowance, got 0") res, err := ts.bankABI.Methods[DepositMethodName].Outputs.Unpack(success) require.NoError(t, err) @@ -194,7 +194,11 @@ func Test_Methods(t *testing.T) { success, err := ts.bankContract.Run(ts.mockEVM, ts.mockVMContract, false) require.Error(t, err) - require.Contains(t, err.Error(), "unexpected error in LockZRC20InBank: invalid allowance, got: 500") + require.Contains( + t, + err.Error(), + "unexpected error in LockZRC20InBank: failed allowance check: invalid allowance, got 500, wanted 501", + ) res, err := ts.bankABI.Methods[DepositMethodName].Outputs.Unpack(success) require.NoError(t, err) diff --git a/x/fungible/keeper/zrc20_cosmos_coin_mapping_test.go b/x/fungible/keeper/zrc20_cosmos_coin_mapping_test.go index 578ee2dfb9..941b81e98c 100644 --- a/x/fungible/keeper/zrc20_cosmos_coin_mapping_test.go +++ b/x/fungible/keeper/zrc20_cosmos_coin_mapping_test.go @@ -38,28 +38,28 @@ func Test_LockZRC20(t *testing.T) { t.Run("should fail when trying to lock zero amount", func(t *testing.T) { // Check lock with zero amount. - err = ts.fungibleKeeper.LockZRC20(ts.ctx, zrc20ABI, ts.zrc20Address, owner, locker, big.NewInt(0)) + err = ts.fungibleKeeper.LockZRC20(ts.ctx, zrc20ABI, ts.zrc20Address, locker, owner, locker, big.NewInt(0)) require.Error(t, err) require.ErrorIs(t, err, fungibletypes.ErrInvalidAmount) }) t.Run("should fail when ZRC20 ABI is not properly initialized", func(t *testing.T) { // Check lock with nil ABI. - err = ts.fungibleKeeper.LockZRC20(ts.ctx, nil, ts.zrc20Address, owner, locker, big.NewInt(10)) + err = ts.fungibleKeeper.LockZRC20(ts.ctx, nil, ts.zrc20Address, locker, owner, locker, big.NewInt(10)) require.Error(t, err) require.ErrorIs(t, err, fungibletypes.ErrZRC20NilABI) }) t.Run("should fail when trying to lock a zero address ZRC20", func(t *testing.T) { // Check lock with ZRC20 zero address. - err = ts.fungibleKeeper.LockZRC20(ts.ctx, zrc20ABI, common.Address{}, owner, locker, big.NewInt(10)) + err = ts.fungibleKeeper.LockZRC20(ts.ctx, zrc20ABI, common.Address{}, locker, owner, locker, big.NewInt(10)) require.Error(t, err) require.ErrorIs(t, err, fungibletypes.ErrZRC20ZeroAddress) }) t.Run("should fail when trying to lock a non whitelisted ZRC20", func(t *testing.T) { // Check lock with non whitelisted ZRC20. - err = ts.fungibleKeeper.LockZRC20(ts.ctx, zrc20ABI, sample.EthAddress(), owner, locker, big.NewInt(10)) + err = ts.fungibleKeeper.LockZRC20(ts.ctx, zrc20ABI, sample.EthAddress(), locker, owner, locker, big.NewInt(10)) require.Error(t, err) require.ErrorIs(t, err, fungibletypes.ErrZRC20NotWhiteListed) }) @@ -72,6 +72,7 @@ func Test_LockZRC20(t *testing.T) { ts.ctx, zrc20ABI, ts.zrc20Address, + locker, owner, locker, big.NewInt(1000000000000000), @@ -84,7 +85,7 @@ func Test_LockZRC20(t *testing.T) { approveAllowance(t, ts, zrc20ABI, owner, locker, big.NewInt(1001)) // Check allowance smaller, equal and bigger than the amount. - err = ts.fungibleKeeper.LockZRC20(ts.ctx, zrc20ABI, ts.zrc20Address, owner, locker, big.NewInt(1001)) + err = ts.fungibleKeeper.LockZRC20(ts.ctx, zrc20ABI, ts.zrc20Address, locker, owner, locker, big.NewInt(1001)) require.Error(t, err) // We do not check in LockZRC20 explicitly if the amount is bigger than the balance. @@ -96,15 +97,15 @@ func Test_LockZRC20(t *testing.T) { approveAllowance(t, ts, zrc20ABI, owner, locker, allowanceTotal) // Check allowance smaller, equal and bigger than the amount. - err = ts.fungibleKeeper.LockZRC20(ts.ctx, zrc20ABI, ts.zrc20Address, owner, locker, higherThanAllowance) + err = ts.fungibleKeeper.LockZRC20(ts.ctx, zrc20ABI, ts.zrc20Address, locker, owner, locker, higherThanAllowance) require.Error(t, err) - require.Contains(t, err.Error(), "invalid allowance, got: 100") + require.Contains(t, err.Error(), "invalid allowance, got 100") }) t.Run("should pass when trying to lock a valid approved amount", func(t *testing.T) { approveAllowance(t, ts, zrc20ABI, owner, locker, allowanceTotal) - err = ts.fungibleKeeper.LockZRC20(ts.ctx, zrc20ABI, ts.zrc20Address, owner, locker, allowanceTotal) + err = ts.fungibleKeeper.LockZRC20(ts.ctx, zrc20ABI, ts.zrc20Address, locker, owner, locker, allowanceTotal) require.NoError(t, err) ownerBalance, err := ts.fungibleKeeper.ZRC20BalanceOf(ts.ctx, zrc20ABI, ts.zrc20Address, owner) @@ -119,7 +120,15 @@ func Test_LockZRC20(t *testing.T) { t.Run("should pass when trying to lock an amount smaller than approved", func(t *testing.T) { approveAllowance(t, ts, zrc20ABI, owner, locker, allowanceTotal) - err = ts.fungibleKeeper.LockZRC20(ts.ctx, zrc20ABI, ts.zrc20Address, owner, locker, smallerThanAllowance) + err = ts.fungibleKeeper.LockZRC20( + ts.ctx, + zrc20ABI, + ts.zrc20Address, + locker, + owner, + locker, + smallerThanAllowance, + ) require.NoError(t, err) // Note that balances are cumulative for all tests. That's why we check 801 and 199 here. @@ -155,7 +164,7 @@ func Test_UnlockZRC20(t *testing.T) { approveAllowance(t, ts, zrc20ABI, owner, locker, allowanceTotal) // Lock 100 ZRC20. - err = ts.fungibleKeeper.LockZRC20(ts.ctx, zrc20ABI, ts.zrc20Address, owner, locker, allowanceTotal) + err = ts.fungibleKeeper.LockZRC20(ts.ctx, zrc20ABI, ts.zrc20Address, locker, owner, locker, allowanceTotal) require.NoError(t, err) t.Run("should fail when trying to unlock zero amount", func(t *testing.T) { @@ -185,7 +194,7 @@ func Test_UnlockZRC20(t *testing.T) { t.Run("should fail when trying to unlock an amount bigger than locker's balance", func(t *testing.T) { err = ts.fungibleKeeper.UnlockZRC20(ts.ctx, zrc20ABI, ts.zrc20Address, owner, locker, big.NewInt(1001)) require.Error(t, err) - require.Contains(t, err.Error(), "invalid balance, got: 100") + require.Contains(t, err.Error(), "invalid balance, got 100") }) t.Run("should pass when trying to unlock a correct amount", func(t *testing.T) { @@ -231,7 +240,7 @@ func Test_CheckZRC20Allowance(t *testing.T) { t.Run("should fail when allowance is not approved", func(t *testing.T) { err = ts.fungibleKeeper.CheckZRC20Allowance(ts.ctx, zrc20ABI, owner, spender, ts.zrc20Address, big.NewInt(10)) require.Error(t, err) - require.Contains(t, err.Error(), "invalid allowance, got: 0") + require.Contains(t, err.Error(), "invalid allowance, got 0") }) t.Run("should fail when checking a higher amount than approved", func(t *testing.T) { @@ -246,7 +255,7 @@ func Test_CheckZRC20Allowance(t *testing.T) { higherThanAllowance, ) require.Error(t, err) - require.Contains(t, err.Error(), "invalid allowance, got: 100") + require.Contains(t, err.Error(), "invalid allowance, got 100") }) t.Run("should pass when checking the same amount as approved", func(t *testing.T) { diff --git a/x/fungible/keeper/zrc20_cosmos_coins_mapping.go b/x/fungible/keeper/zrc20_cosmos_coins_mapping.go index 2013514758..f7d152f749 100644 --- a/x/fungible/keeper/zrc20_cosmos_coins_mapping.go +++ b/x/fungible/keeper/zrc20_cosmos_coins_mapping.go @@ -4,6 +4,7 @@ import ( "fmt" "math/big" + "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" @@ -19,42 +20,42 @@ import ( func (k Keeper) LockZRC20( ctx sdk.Context, zrc20ABI *abi.ABI, - zrc20Address, owner, locker common.Address, + zrc20Address, spender, owner, locker common.Address, amount *big.Int, ) error { // owner is the EOA owner of the ZRC20 tokens. // locker is the address that will lock the ZRC20 tokens, i.e: bank precompile. if err := k.CheckZRC20Allowance(ctx, zrc20ABI, owner, locker, zrc20Address, amount); err != nil { - return err + return errors.Wrap(err, "failed allowance check") } // Check amount_to_be_locked <= total_erc20_balance - already_locked // Max amount of ZRC20 tokens that exists in zEVM are the total supply. totalSupply, err := k.ZRC20TotalSupply(ctx, zrc20ABI, zrc20Address) if err != nil { - return err + return errors.Wrap(err, "failed totalSupply check") } // The alreadyLocked amount is the amount of ZRC20 tokens that have been locked by the locker. - // TODO: Implement list of whitelisted locker addresses. + // TODO: Implement list of whitelisted locker addresses (https://github.com/zeta-chain/node/issues/2991) alreadyLocked, err := k.ZRC20BalanceOf(ctx, zrc20ABI, zrc20Address, locker) if err != nil { - return err + return errors.Wrap(err, "failed getting the ZRC20 already locked amount") } if !k.IsValidDepositAmount(totalSupply, alreadyLocked, amount) { - return fungibletypes.ErrInvalidAmount + return errors.Wrap(fungibletypes.ErrInvalidAmount, "amount to be locked is not valid") } // Initiate a transferFrom the owner to the locker. This will lock the ZRC20 tokens. // locker has to initiate the transaction and have enough allowance from owner. - transferred, err := k.ZRC20TransferFrom(ctx, zrc20ABI, zrc20Address, owner, locker, amount) + transferred, err := k.ZRC20TransferFrom(ctx, zrc20ABI, zrc20Address, spender, owner, locker, amount) if err != nil { - return err + return errors.Wrap(err, "failed executing transferFrom") } if !transferred { - return fmt.Errorf("lock ZRC20 not successful") + return fmt.Errorf("transferFrom returned false (no success)") } return nil @@ -71,17 +72,17 @@ func (k Keeper) UnlockZRC20( ) error { // Check if the account locking the ZRC20 tokens has enough balance. if err := k.CheckZRC20Balance(ctx, zrc20ABI, zrc20Address, locker, amount); err != nil { - return err + return errors.Wrap(err, "failed balance check") } // transfer from the EOA locking the assets to the owner. transferred, err := k.ZRC20Transfer(ctx, zrc20ABI, zrc20Address, locker, owner, amount) if err != nil { - return err + return errors.Wrap(err, "failed executing transfer") } if !transferred { - return fmt.Errorf("unlock ZRC20 not successful") + return fmt.Errorf("transfer returned false (no success)") } return nil @@ -108,16 +109,16 @@ func (k Keeper) CheckZRC20Allowance( } if err := k.IsValidZRC20(ctx, zrc20Address); err != nil { - return err + return errors.Wrap(err, "ZRC20 is not valid") } allowanceValue, err := k.ZRC20Allowance(ctx, zrc20ABI, zrc20Address, owner, spender) if err != nil { - return err + return errors.Wrap(err, "failed while checking spender's allowance") } if allowanceValue.Cmp(amount) < 0 || allowanceValue.Cmp(big.NewInt(0)) <= 0 { - return fmt.Errorf("invalid allowance, got: %s", allowanceValue.String()) + return fmt.Errorf("invalid allowance, got %s, wanted %s", allowanceValue.String(), amount.String()) } return nil @@ -140,7 +141,7 @@ func (k Keeper) CheckZRC20Balance( } if err := k.IsValidZRC20(ctx, zrc20Address); err != nil { - return err + return errors.Wrap(err, "ZRC20 is not valid") } if crypto.IsEmptyAddress(owner) { @@ -151,11 +152,11 @@ func (k Keeper) CheckZRC20Balance( // function balanceOf(address account) balance, err := k.ZRC20BalanceOf(ctx, zrc20ABI, zrc20Address, owner) if err != nil { - return err + return errors.Wrap(err, "failed getting owner's ZRC20 balance") } - if balance.Cmp(amount) == -1 { - return fmt.Errorf("invalid balance, got: %s", balance.String()) + if balance.Cmp(amount) < 0 { + return fmt.Errorf("invalid balance, got %s, wanted %s", balance.String(), amount.String()) } return nil @@ -179,7 +180,7 @@ func (k Keeper) IsValidZRC20(ctx sdk.Context, zrc20Address common.Address) error return nil } -// IsValidDepositAmount checks "totalSupply >= amount_to_be_deposited + amount_already_locked". +// IsValidDepositAmount checks "totalSupply >= amount_to_be_locked + amount_already_locked". // A failure here means the user is trying to lock more than the available ZRC20 supply. // This suggests that an actor is minting ZRC20 tokens out of thin air. func (k Keeper) IsValidDepositAmount(totalSupply, alreadyLocked, amountToDeposit *big.Int) bool { diff --git a/x/fungible/keeper/zrc20_methods.go b/x/fungible/keeper/zrc20_methods.go index b3b636dfe4..a90cc511bb 100644 --- a/x/fungible/keeper/zrc20_methods.go +++ b/x/fungible/keeper/zrc20_methods.go @@ -4,6 +4,7 @@ import ( "fmt" "math/big" + "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" @@ -21,6 +22,7 @@ const ( ) // ZRC20Allowance returns the ZRC20 allowance for a given spender. +// The allowance has to be previously approved by the ZRC20 tokens owner. func (k Keeper) ZRC20Allowance( ctx sdk.Context, zrc20ABI *abi.ABI, @@ -53,7 +55,7 @@ func (k Keeper) ZRC20Allowance( args..., ) if err != nil { - return nil, err + return nil, errors.Wrap(err, "EVM error calling ZRC20 allowance function") } if res.VmError != "" { @@ -62,7 +64,7 @@ func (k Keeper) ZRC20Allowance( ret, err := zrc20ABI.Methods[allowance].Outputs.Unpack(res.Ret) if err != nil { - return nil, err + return nil, errors.Wrap(err, "failed to unpack ZRC20 allowance return value") } if len(ret) == 0 { @@ -95,6 +97,7 @@ func (k Keeper) ZRC20BalanceOf( return nil, err } + // function balanceOf(address account) res, err := k.CallEVM( ctx, *zrc20ABI, @@ -108,7 +111,7 @@ func (k Keeper) ZRC20BalanceOf( owner, ) if err != nil { - return nil, err + return nil, errors.Wrap(err, "EVM error calling ZRC20 balanceOf function") } if res.VmError != "" { @@ -117,7 +120,7 @@ func (k Keeper) ZRC20BalanceOf( ret, err := zrc20ABI.Methods[balanceOf].Outputs.Unpack(res.Ret) if err != nil { - return nil, err + return nil, errors.Wrap(err, "failed to unpack ZRC20 balanceOf return value") } if len(ret) == 0 { @@ -159,7 +162,7 @@ func (k Keeper) ZRC20TotalSupply( totalSupply, ) if err != nil { - return nil, err + return nil, errors.Wrap(err, "EVM error calling ZRC20 totalSupply function") } if res.VmError != "" { @@ -168,7 +171,7 @@ func (k Keeper) ZRC20TotalSupply( ret, err := zrc20ABI.Methods[totalSupply].Outputs.Unpack(res.Ret) if err != nil { - return nil, err + return nil, errors.Wrap(err, "failed to unpack ZRC20 totalSupply return value") } if len(ret) == 0 { @@ -202,7 +205,7 @@ func (k Keeper) ZRC20Transfer( return false, err } - // transfer from the EOA locking the assets to the owner. + // function transfer(address recipient, uint256 amount) args := []interface{}{to, amount} res, err := k.CallEVM( ctx, @@ -217,7 +220,7 @@ func (k Keeper) ZRC20Transfer( args..., ) if err != nil { - return false, err + return false, errors.Wrap(err, "EVM error calling ZRC20 transfer function") } if res.VmError != "" { @@ -226,7 +229,7 @@ func (k Keeper) ZRC20Transfer( ret, err := zrc20ABI.Methods[transfer].Outputs.Unpack(res.Ret) if err != nil { - return false, err + return false, errors.Wrap(err, "failed to unpack ZRC20 transfer return value") } if len(ret) == 0 { @@ -241,20 +244,20 @@ func (k Keeper) ZRC20Transfer( return transferred, nil } -// ZRC20TransferFrom transfers ZRC20 tokens from the owner to the spender. +// ZRC20TransferFrom transfers ZRC20 tokens "from" to the EOA "to". // The transaction is started by the spender. -// This requires the spender to have been approved by the owner. +// Requisite: the original EOA must have approved the spender to spend the tokens. func (k Keeper) ZRC20TransferFrom( ctx sdk.Context, zrc20ABI *abi.ABI, - zrc20Address, from, to common.Address, + zrc20Address, spender, from, to common.Address, amount *big.Int, ) (bool, error) { if zrc20ABI == nil { return false, fungibletypes.ErrZRC20NilABI } - if crypto.IsEmptyAddress(from) || crypto.IsEmptyAddress(to) { + if crypto.IsEmptyAddress(from) || crypto.IsEmptyAddress(to) || crypto.IsEmptyAddress(spender) { return false, fungibletypes.ErrZeroAddress } @@ -262,11 +265,12 @@ func (k Keeper) ZRC20TransferFrom( return false, err } + // function transferFrom(address sender, address recipient, uint256 amount) args := []interface{}{from, to, amount} res, err := k.CallEVM( ctx, *zrc20ABI, - to, + spender, zrc20Address, big.NewInt(0), nil, @@ -276,7 +280,7 @@ func (k Keeper) ZRC20TransferFrom( args..., ) if err != nil { - return false, err + return false, errors.Wrap(err, "EVM error calling ZRC20 transferFrom function") } if res.VmError != "" { @@ -285,7 +289,7 @@ func (k Keeper) ZRC20TransferFrom( ret, err := zrc20ABI.Methods[transferFrom].Outputs.Unpack(res.Ret) if err != nil { - return false, err + return false, errors.Wrap(err, "failed to unpack ZRC20 transferFrom return value") } if len(ret) == 0 { diff --git a/x/fungible/keeper/zrc20_methods_test.go b/x/fungible/keeper/zrc20_methods_test.go index 2fb6992508..a5010c332a 100644 --- a/x/fungible/keeper/zrc20_methods_test.go +++ b/x/fungible/keeper/zrc20_methods_test.go @@ -239,17 +239,19 @@ func Test_ZRC20TransferFrom(t *testing.T) { ts.zrc20Address, common.Address{}, common.Address{}, + common.Address{}, big.NewInt(0), ) require.Error(t, err) require.ErrorAs(t, err, &fungibletypes.ErrZRC20NilABI) }) - t.Run("should fail when owner is zero address", func(t *testing.T) { + t.Run("should fail when from is zero address", func(t *testing.T) { _, err := ts.fungibleKeeper.ZRC20TransferFrom( ts.ctx, zrc20ABI, ts.zrc20Address, + sample.EthAddress(), common.Address{}, sample.EthAddress(), big.NewInt(0), @@ -258,13 +260,28 @@ func Test_ZRC20TransferFrom(t *testing.T) { require.ErrorAs(t, err, &fungibletypes.ErrZeroAddress) }) - t.Run("should fail when spender is zero address", func(t *testing.T) { + t.Run("should fail when to is zero address", func(t *testing.T) { _, err := ts.fungibleKeeper.ZRC20TransferFrom( ts.ctx, zrc20ABI, ts.zrc20Address, sample.EthAddress(), + sample.EthAddress(), + common.Address{}, + big.NewInt(0), + ) + require.Error(t, err) + require.ErrorAs(t, err, &fungibletypes.ErrZeroAddress) + }) + + t.Run("should fail when spender is zero address", func(t *testing.T) { + _, err := ts.fungibleKeeper.ZRC20TransferFrom( + ts.ctx, + zrc20ABI, + ts.zrc20Address, common.Address{}, + sample.EthAddress(), + sample.EthAddress(), big.NewInt(0), ) require.Error(t, err) @@ -277,6 +294,7 @@ func Test_ZRC20TransferFrom(t *testing.T) { zrc20ABI, common.Address{}, sample.EthAddress(), + sample.EthAddress(), fungibletypes.ModuleAddressZEVM, big.NewInt(0), ) @@ -293,6 +311,7 @@ func Test_ZRC20TransferFrom(t *testing.T) { ts.ctx, zrc20ABI, ts.zrc20Address, + fungibletypes.ModuleAddressZEVM, sample.EthAddress(), fungibletypes.ModuleAddressZEVM, big.NewInt(10), @@ -312,6 +331,7 @@ func Test_ZRC20TransferFrom(t *testing.T) { ts.ctx, zrc20ABI, ts.zrc20Address, + fungibletypes.ModuleAddressZEVM, sample.EthAddress(), fungibletypes.ModuleAddressZEVM, big.NewInt(10), From f20129df6881a8c92d6fc46e4ef56479823a2177 Mon Sep 17 00:00:00 2001 From: Francisco de Borja Aranda Castillejo Date: Wed, 16 Oct 2024 16:10:55 +0200 Subject: [PATCH 10/10] add review suggestions --- precompiles/bank/method_deposit.go | 4 +-- precompiles/bank/method_test.go | 22 +++++++-------- .../keeper/zrc20_cosmos_coin_mapping_test.go | 8 +++--- x/fungible/keeper/zrc20_methods.go | 6 ++-- x/fungible/keeper/zrc20_methods_test.go | 28 +++++++++---------- x/fungible/types/keys.go | 4 --- 6 files changed, 34 insertions(+), 38 deletions(-) diff --git a/precompiles/bank/method_deposit.go b/precompiles/bank/method_deposit.go index 60f166de97..1d5792d8a3 100644 --- a/precompiles/bank/method_deposit.go +++ b/precompiles/bank/method_deposit.go @@ -92,8 +92,8 @@ func (c *Contract) deposit( // 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. + // - Mint coins to the fungible module. + // - Send coins from fungible to the caller. coinSet, err := createCoinSet(ZRC20ToCosmosDenom(zrc20Addr), amount) if err != nil { return nil, err diff --git a/precompiles/bank/method_test.go b/precompiles/bank/method_test.go index 8921c787af..e2eda3237d 100644 --- a/precompiles/bank/method_test.go +++ b/precompiles/bank/method_test.go @@ -28,7 +28,7 @@ import ( func Test_Methods(t *testing.T) { t.Run("should fail when trying to run deposit as read only method", func(t *testing.T) { ts := setupChain(t) - caller := fungibletypes.ModuleAddressZEVM + caller := fungibletypes.ModuleAddressEVM methodID := ts.bankABI.Methods[DepositMethodName] // Set CallerAddress and evm.Origin to the caller address. @@ -55,7 +55,7 @@ func Test_Methods(t *testing.T) { t.Run("should fail when trying to run withdraw as read only method", func(t *testing.T) { ts := setupChain(t) - caller := fungibletypes.ModuleAddressZEVM + caller := fungibletypes.ModuleAddressEVM methodID := ts.bankABI.Methods[WithdrawMethodName] // Set CallerAddress and evm.Origin to the caller address. @@ -82,7 +82,7 @@ func Test_Methods(t *testing.T) { t.Run("should fail when caller has 0 token balance", func(t *testing.T) { ts := setupChain(t) - caller := fungibletypes.ModuleAddressZEVM + caller := fungibletypes.ModuleAddressEVM methodID := ts.bankABI.Methods[DepositMethodName] // Set CallerAddress and evm.Origin to the caller address. @@ -116,7 +116,7 @@ func Test_Methods(t *testing.T) { t.Run("should fail when bank has 0 token allowance", func(t *testing.T) { ts := setupChain(t) - caller := fungibletypes.ModuleAddressZEVM + caller := fungibletypes.ModuleAddressEVM ts.fungibleKeeper.DepositZRC20(ts.ctx, ts.zrc20Address, caller, big.NewInt(1000)) methodID := ts.bankABI.Methods[DepositMethodName] @@ -146,7 +146,7 @@ func Test_Methods(t *testing.T) { t.Run("should fail when trying to deposit 0", func(t *testing.T) { ts := setupChain(t) - caller := fungibletypes.ModuleAddressZEVM + caller := fungibletypes.ModuleAddressEVM ts.fungibleKeeper.DepositZRC20(ts.ctx, ts.zrc20Address, caller, big.NewInt(1000)) methodID := ts.bankABI.Methods[DepositMethodName] @@ -173,7 +173,7 @@ func Test_Methods(t *testing.T) { t.Run("should fail when trying to deposit more than allowed to bank", func(t *testing.T) { ts := setupChain(t) - caller := fungibletypes.ModuleAddressZEVM + caller := fungibletypes.ModuleAddressEVM ts.fungibleKeeper.DepositZRC20(ts.ctx, ts.zrc20Address, caller, big.NewInt(1000)) // Allow bank to spend 500 ZRC20 tokens. @@ -225,7 +225,7 @@ func Test_Methods(t *testing.T) { t.Run("should fail when trying to deposit more than user balance", func(t *testing.T) { ts := setupChain(t) - caller := fungibletypes.ModuleAddressZEVM + caller := fungibletypes.ModuleAddressEVM ts.fungibleKeeper.DepositZRC20(ts.ctx, ts.zrc20Address, caller, big.NewInt(1000)) // Allow bank to spend 500 ZRC20 tokens. @@ -279,7 +279,7 @@ func Test_Methods(t *testing.T) { t.Run("should deposit tokens and retrieve balance of cosmos coin", func(t *testing.T) { ts := setupChain(t) - caller := fungibletypes.ModuleAddressZEVM + caller := fungibletypes.ModuleAddressEVM ts.fungibleKeeper.DepositZRC20(ts.ctx, ts.zrc20Address, caller, big.NewInt(1000)) methodID := ts.bankABI.Methods[DepositMethodName] @@ -325,7 +325,7 @@ func Test_Methods(t *testing.T) { t.Run("should deposit tokens, withdraw and check with balanceOf", func(t *testing.T) { ts := setupChain(t) - caller := fungibletypes.ModuleAddressZEVM + caller := fungibletypes.ModuleAddressEVM ts.fungibleKeeper.DepositZRC20(ts.ctx, ts.zrc20Address, caller, big.NewInt(1000)) methodID := ts.bankABI.Methods[DepositMethodName] @@ -404,7 +404,7 @@ func Test_Methods(t *testing.T) { t.Run("should deposit tokens and fail when withdrawing more than depositted", func(t *testing.T) { ts := setupChain(t) - caller := fungibletypes.ModuleAddressZEVM + caller := fungibletypes.ModuleAddressEVM ts.fungibleKeeper.DepositZRC20(ts.ctx, ts.zrc20Address, caller, big.NewInt(1000)) methodID := ts.bankABI.Methods[DepositMethodName] @@ -573,7 +573,7 @@ func allowBank(t *testing.T, ts testSuite, amount *big.Int) { ts.ctx, ts.fungibleKeeper, &ts.zrc20ABI, - fungibletypes.ModuleAddressZEVM, + fungibletypes.ModuleAddressEVM, ts.zrc20Address, "approve", []interface{}{ts.bankContract.Address(), amount}, diff --git a/x/fungible/keeper/zrc20_cosmos_coin_mapping_test.go b/x/fungible/keeper/zrc20_cosmos_coin_mapping_test.go index 941b81e98c..16803a917c 100644 --- a/x/fungible/keeper/zrc20_cosmos_coin_mapping_test.go +++ b/x/fungible/keeper/zrc20_cosmos_coin_mapping_test.go @@ -22,7 +22,7 @@ func Test_LockZRC20(t *testing.T) { ts := setupChain(t) - owner := fungibletypes.ModuleAddressZEVM + owner := fungibletypes.ModuleAddressEVM locker := sample.EthAddress() depositTotal := big.NewInt(1000) allowanceTotal := big.NewInt(100) @@ -148,7 +148,7 @@ func Test_UnlockZRC20(t *testing.T) { ts := setupChain(t) - owner := fungibletypes.ModuleAddressZEVM + owner := fungibletypes.ModuleAddressEVM locker := sample.EthAddress() depositTotal := big.NewInt(1000) allowanceTotal := big.NewInt(100) @@ -217,7 +217,7 @@ func Test_CheckZRC20Allowance(t *testing.T) { ts := setupChain(t) - owner := fungibletypes.ModuleAddressZEVM + owner := fungibletypes.ModuleAddressEVM spender := sample.EthAddress() depositTotal := big.NewInt(1000) allowanceTotal := big.NewInt(100) @@ -229,7 +229,7 @@ func Test_CheckZRC20Allowance(t *testing.T) { ts.fungibleKeeper.GetAuthKeeper().SetAccount(ts.ctx, authtypes.NewBaseAccount(accAddress, nil, 0, 0)) // Deposit ZRC20 tokens into the fungible. - ts.fungibleKeeper.DepositZRC20(ts.ctx, ts.zrc20Address, fungibletypes.ModuleAddressZEVM, depositTotal) + ts.fungibleKeeper.DepositZRC20(ts.ctx, ts.zrc20Address, fungibletypes.ModuleAddressEVM, depositTotal) t.Run("should fail when checking zero amount", func(t *testing.T) { err = ts.fungibleKeeper.CheckZRC20Allowance(ts.ctx, zrc20ABI, owner, spender, ts.zrc20Address, big.NewInt(0)) diff --git a/x/fungible/keeper/zrc20_methods.go b/x/fungible/keeper/zrc20_methods.go index a90cc511bb..5c9f2d4645 100644 --- a/x/fungible/keeper/zrc20_methods.go +++ b/x/fungible/keeper/zrc20_methods.go @@ -45,7 +45,7 @@ func (k Keeper) ZRC20Allowance( res, err := k.CallEVM( ctx, *zrc20ABI, - fungibletypes.ModuleAddressZEVM, + fungibletypes.ModuleAddressEVM, zrc20Address, big.NewInt(0), nil, @@ -101,7 +101,7 @@ func (k Keeper) ZRC20BalanceOf( res, err := k.CallEVM( ctx, *zrc20ABI, - fungibletypes.ModuleAddressZEVM, + fungibletypes.ModuleAddressEVM, zrc20Address, big.NewInt(0), nil, @@ -153,7 +153,7 @@ func (k Keeper) ZRC20TotalSupply( res, err := k.CallEVM( ctx, *zrc20ABI, - fungibletypes.ModuleAddressZEVM, + fungibletypes.ModuleAddressEVM, zrc20Address, big.NewInt(0), nil, diff --git a/x/fungible/keeper/zrc20_methods_test.go b/x/fungible/keeper/zrc20_methods_test.go index a5010c332a..7b124f3050 100644 --- a/x/fungible/keeper/zrc20_methods_test.go +++ b/x/fungible/keeper/zrc20_methods_test.go @@ -57,7 +57,7 @@ func Test_ZRC20Allowance(t *testing.T) { zrc20ABI, common.Address{}, sample.EthAddress(), - fungibletypes.ModuleAddressZEVM, + fungibletypes.ModuleAddressEVM, ) require.Error(t, err) require.ErrorAs(t, err, &fungibletypes.ErrZRC20ZeroAddress) @@ -68,7 +68,7 @@ func Test_ZRC20Allowance(t *testing.T) { ts.ctx, zrc20ABI, ts.zrc20Address, - fungibletypes.ModuleAddressZEVM, + fungibletypes.ModuleAddressEVM, sample.EthAddress(), ) require.NoError(t, err) @@ -107,7 +107,7 @@ func Test_ZRC20BalanceOf(t *testing.T) { ts.ctx, zrc20ABI, ts.zrc20Address, - fungibletypes.ModuleAddressZEVM, + fungibletypes.ModuleAddressEVM, ) require.NoError(t, err) require.Equal(t, uint64(0), balance.Uint64()) @@ -198,7 +198,7 @@ func Test_ZRC20Transfer(t *testing.T) { zrc20ABI, common.Address{}, sample.EthAddress(), - fungibletypes.ModuleAddressZEVM, + fungibletypes.ModuleAddressEVM, big.NewInt(0), ) require.Error(t, err) @@ -206,12 +206,12 @@ func Test_ZRC20Transfer(t *testing.T) { }) t.Run("should pass with correct input", func(t *testing.T) { - ts.fungibleKeeper.DepositZRC20(ts.ctx, ts.zrc20Address, fungibletypes.ModuleAddressZEVM, big.NewInt(10)) + ts.fungibleKeeper.DepositZRC20(ts.ctx, ts.zrc20Address, fungibletypes.ModuleAddressEVM, big.NewInt(10)) transferred, err := ts.fungibleKeeper.ZRC20Transfer( ts.ctx, zrc20ABI, ts.zrc20Address, - fungibletypes.ModuleAddressZEVM, + fungibletypes.ModuleAddressEVM, sample.EthAddress(), big.NewInt(10), ) @@ -295,7 +295,7 @@ func Test_ZRC20TransferFrom(t *testing.T) { common.Address{}, sample.EthAddress(), sample.EthAddress(), - fungibletypes.ModuleAddressZEVM, + fungibletypes.ModuleAddressEVM, big.NewInt(0), ) require.Error(t, err) @@ -304,16 +304,16 @@ func Test_ZRC20TransferFrom(t *testing.T) { t.Run("should fail without an allowance approval", func(t *testing.T) { // Deposit ZRC20 into fungible EOA. - ts.fungibleKeeper.DepositZRC20(ts.ctx, ts.zrc20Address, fungibletypes.ModuleAddressZEVM, big.NewInt(1000)) + ts.fungibleKeeper.DepositZRC20(ts.ctx, ts.zrc20Address, fungibletypes.ModuleAddressEVM, big.NewInt(1000)) // Transferring the tokens with transferFrom without approval should fail. _, err = ts.fungibleKeeper.ZRC20TransferFrom( ts.ctx, zrc20ABI, ts.zrc20Address, - fungibletypes.ModuleAddressZEVM, + fungibletypes.ModuleAddressEVM, sample.EthAddress(), - fungibletypes.ModuleAddressZEVM, + fungibletypes.ModuleAddressEVM, big.NewInt(10), ) require.Error(t, err) @@ -321,19 +321,19 @@ func Test_ZRC20TransferFrom(t *testing.T) { t.Run("should success with an allowance approval", func(t *testing.T) { // Deposit ZRC20 into fungible EOA. - ts.fungibleKeeper.DepositZRC20(ts.ctx, ts.zrc20Address, fungibletypes.ModuleAddressZEVM, big.NewInt(1000)) + ts.fungibleKeeper.DepositZRC20(ts.ctx, ts.zrc20Address, fungibletypes.ModuleAddressEVM, big.NewInt(1000)) // Approve allowance to sample.EthAddress() to spend 10 ZRC20 tokens. - approveAllowance(t, ts, zrc20ABI, fungibletypes.ModuleAddressZEVM, sample.EthAddress(), big.NewInt(10)) + approveAllowance(t, ts, zrc20ABI, fungibletypes.ModuleAddressEVM, sample.EthAddress(), big.NewInt(10)) // Transferring the tokens with transferFrom without approval should fail. _, err = ts.fungibleKeeper.ZRC20TransferFrom( ts.ctx, zrc20ABI, ts.zrc20Address, - fungibletypes.ModuleAddressZEVM, + fungibletypes.ModuleAddressEVM, sample.EthAddress(), - fungibletypes.ModuleAddressZEVM, + fungibletypes.ModuleAddressEVM, big.NewInt(10), ) require.Error(t, err) diff --git a/x/fungible/types/keys.go b/x/fungible/types/keys.go index 339cc63560..4c02d9c2ef 100644 --- a/x/fungible/types/keys.go +++ b/x/fungible/types/keys.go @@ -29,10 +29,6 @@ func KeyPrefix(p string) []byte { var ( ModuleAddress = authtypes.NewModuleAddress(ModuleName) ModuleAddressEVM = common.BytesToAddress(ModuleAddress.Bytes()) - // ModuleAddressZEVM is calculated in the same way as ModuleAddressEVM. - // Maintain it for legibility in functions calling fungible module address from zEVM. - ModuleAddressZEVM = common.BytesToAddress(ModuleAddress.Bytes()) - AdminAddress = "zeta1rx9r8hff0adaqhr5tuadkzj4e7ns2ntg446vtt" ) const (