Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: remove minimum rent exempt check for SPL token withdrawals #3374

Merged
merged 6 commits into from
Jan 20, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ func (k Keeper) processFailedOutboundOnExternalChain(
err = k.validateZRC20Withdrawal(
ctx,
cctx.GetCurrentOutboundParam().ReceiverChainId,
cctx.InboundParams.CoinType,
cctx.GetCurrentOutboundParam().Amount.BigInt(),
[]byte(cctx.GetCurrentOutboundParam().Receiver),
)
Expand Down
21 changes: 16 additions & 5 deletions x/crosschain/keeper/evm_hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ func (k Keeper) ProcessZEVMInboundV1(

// If Validation fails, we will not process the event and return and error. This condition means that the event was correct, and emitted from a registered ZRC20 contract
// But the information entered by the user is incorrect. In this case we can return an error and roll back the transaction
if err := k.ValidateZRC20WithdrawEvent(ctx, eventZRC20Withdrawal, coin.ForeignChainId); err != nil {
if err := k.ValidateZRC20WithdrawEvent(ctx, eventZRC20Withdrawal, coin.ForeignChainId, coin.CoinType); err != nil {
return err
}
// If the event is valid, we will process it and create a new CCTX
Expand Down Expand Up @@ -305,14 +305,25 @@ func (k Keeper) ProcessZetaSentEvent(

// ValidateZRC20WithdrawEvent checks if the ZRC20Withdrawal event is valid
// It verifies event information for BTC chains and returns an error if the event is invalid
func (k Keeper) ValidateZRC20WithdrawEvent(ctx sdk.Context, event *zrc20.ZRC20Withdrawal, chainID int64) error {
func (k Keeper) ValidateZRC20WithdrawEvent(
ctx sdk.Context,
event *zrc20.ZRC20Withdrawal,
chainID int64,
coinType coin.CoinType,
ws4charlie marked this conversation as resolved.
Show resolved Hide resolved
) error {
// The event was parsed; that means the user has deposited tokens to the contract.
return k.validateZRC20Withdrawal(ctx, chainID, event.Value, event.To)
return k.validateZRC20Withdrawal(ctx, chainID, coinType, event.Value, event.To)
}

// validateZRC20Withdrawal validates the data of a ZRC20 Withdrawal event (version 1 or 2)
// it checks if the withdrawal amount is valid and the destination address is supported depending on the chain
func (k Keeper) validateZRC20Withdrawal(ctx sdk.Context, chainID int64, value *big.Int, to []byte) error {
func (k Keeper) validateZRC20Withdrawal(
ctx sdk.Context,
chainID int64,
coinType coin.CoinType,
value *big.Int,
to []byte,
) error {
additionalChains := k.GetAuthorityKeeper().GetAdditionalChainList(ctx)
if chains.IsBitcoinChain(chainID, additionalChains) {
if value.Cmp(big.NewInt(constant.BTCWithdrawalDustAmount)) < 0 {
Expand All @@ -331,7 +342,7 @@ func (k Keeper) validateZRC20Withdrawal(ctx sdk.Context, chainID int64, value *b
return errorsmod.Wrapf(types.ErrInvalidAddress, "unsupported address %s", string(to))
}
} else if chains.IsSolanaChain(chainID, additionalChains) {
if value.Cmp(big.NewInt(constant.SolanaWalletRentExempt)) < 0 {
if coinType == coin.CoinType_Gas && value.Cmp(big.NewInt(constant.SolanaWalletRentExempt)) < 0 {
lumtis marked this conversation as resolved.
Show resolved Hide resolved
return errorsmod.Wrapf(
types.ErrInvalidWithdrawalAmount,
"withdraw amount %s is less than rent exempt %d",
Expand Down
79 changes: 64 additions & 15 deletions x/crosschain/keeper/evm_hooks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (

"github.com/zeta-chain/node/cmd/zetacored/config"
"github.com/zeta-chain/node/pkg/chains"
"github.com/zeta-chain/node/pkg/coin"
"github.com/zeta-chain/node/pkg/constant"
keepertest "github.com/zeta-chain/node/testutil/keeper"
"github.com/zeta-chain/node/testutil/sample"
Expand Down Expand Up @@ -154,14 +155,47 @@ func TestParseZRC20WithdrawalEvent(t *testing.T) {
}
})
}

func TestValidateZrc20WithdrawEvent(t *testing.T) {
t.Run("successfully validate a valid event", func(t *testing.T) {
t.Run("successfully validate a valid BTC withdrawal event", func(t *testing.T) {
k, ctx, _, _ := keepertest.CrosschainKeeper(t)
btcMainNetWithdrawalEvent, err := crosschainkeeper.ParseZRC20WithdrawalEvent(
*sample.ValidZRC20WithdrawToBTCReceipt(t).Logs[3],
)
require.NoError(t, err)
err = k.ValidateZRC20WithdrawEvent(ctx, btcMainNetWithdrawalEvent, chains.BitcoinMainnet.ChainId)
err = k.ValidateZRC20WithdrawEvent(
ctx,
btcMainNetWithdrawalEvent,
chains.BitcoinMainnet.ChainId,
coin.CoinType_Gas,
)
require.NoError(t, err)
})

t.Run("successfully validate a valid SOL withdrawal event", func(t *testing.T) {
k, ctx, _, _ := keepertest.CrosschainKeeper(t)

// 1000000 lamports is the minimum amount (rent exempt) that can be withdrawn
chainID := chains.SolanaMainnet.ChainId
to := []byte(sample.SolanaAddress(t))
value := big.NewInt(constant.SolanaWalletRentExempt)
solWithdrawalEvent := sample.ZRC20Withdrawal(to, value)

// 1000000 lamports can be withdrawn
err := k.ValidateZRC20WithdrawEvent(ctx, solWithdrawalEvent, chainID, coin.CoinType_Gas)
require.NoError(t, err)
})

t.Run("successfully validate a small amount of SPL withdrawal event", func(t *testing.T) {
k, ctx, _, _ := keepertest.CrosschainKeeper(t)

// set SPL token amount to 1
chainID := chains.SolanaMainnet.ChainId
to := []byte(sample.SolanaAddress(t))
solWithdrawalEvent := sample.ZRC20Withdrawal(to, big.NewInt(1))

// should withdraw successfully
err := k.ValidateZRC20WithdrawEvent(ctx, solWithdrawalEvent, chainID, coin.CoinType_ERC20)
require.NoError(t, err)
})

Expand All @@ -174,12 +208,22 @@ func TestValidateZrc20WithdrawEvent(t *testing.T) {

// 1000 satoshis is the minimum amount that can be withdrawn
btcMainNetWithdrawalEvent.Value = big.NewInt(constant.BTCWithdrawalDustAmount)
err = k.ValidateZRC20WithdrawEvent(ctx, btcMainNetWithdrawalEvent, chains.BitcoinMainnet.ChainId)
err = k.ValidateZRC20WithdrawEvent(
ctx,
btcMainNetWithdrawalEvent,
chains.BitcoinMainnet.ChainId,
coin.CoinType_Gas,
)
require.NoError(t, err)

// 999 satoshis cannot be withdrawn
btcMainNetWithdrawalEvent.Value = big.NewInt(constant.BTCWithdrawalDustAmount - 1)
err = k.ValidateZRC20WithdrawEvent(ctx, btcMainNetWithdrawalEvent, chains.BitcoinMainnet.ChainId)
err = k.ValidateZRC20WithdrawEvent(
ctx,
btcMainNetWithdrawalEvent,
chains.BitcoinMainnet.ChainId,
coin.CoinType_Gas,
)
require.ErrorContains(t, err, "less than dust amount")
})

Expand All @@ -189,7 +233,12 @@ func TestValidateZrc20WithdrawEvent(t *testing.T) {
*sample.ValidZRC20WithdrawToBTCReceipt(t).Logs[3],
)
require.NoError(t, err)
err = k.ValidateZRC20WithdrawEvent(ctx, btcMainNetWithdrawalEvent, chains.BitcoinTestnet.ChainId)
err = k.ValidateZRC20WithdrawEvent(
ctx,
btcMainNetWithdrawalEvent,
chains.BitcoinTestnet.ChainId,
coin.CoinType_Gas,
)
require.ErrorContains(t, err, "invalid address")
})

Expand All @@ -201,7 +250,12 @@ func TestValidateZrc20WithdrawEvent(t *testing.T) {
require.NoError(t, err)
btcMainNetWithdrawalEvent.To = []byte("04b2891ba8cb491828db3ebc8a780d43b169e7b3974114e6e50f9bab6ec" +
"63c2f20f6d31b2025377d05c2a704d3bd799d0d56f3a8543d79a01ab6084a1cb204f260")
err = k.ValidateZRC20WithdrawEvent(ctx, btcMainNetWithdrawalEvent, chains.BitcoinMainnet.ChainId)
err = k.ValidateZRC20WithdrawEvent(
ctx,
btcMainNetWithdrawalEvent,
chains.BitcoinMainnet.ChainId,
coin.CoinType_Gas,
)
require.ErrorContains(t, err, "unsupported address")
})

Expand All @@ -213,26 +267,21 @@ func TestValidateZrc20WithdrawEvent(t *testing.T) {
value := big.NewInt(constant.SolanaWalletRentExempt)
solWithdrawalEvent := sample.ZRC20Withdrawal(to, value)

err := k.ValidateZRC20WithdrawEvent(ctx, solWithdrawalEvent, chains.SolanaMainnet.ChainId)
err := k.ValidateZRC20WithdrawEvent(ctx, solWithdrawalEvent, chains.SolanaMainnet.ChainId, coin.CoinType_Gas)
require.ErrorContains(t, err, "invalid address")
})

t.Run("unable to validate a solana withdrawal event with an invalid amount", func(t *testing.T) {
t.Run("unable to validate a SOL withdrawal event with an invalid amount", func(t *testing.T) {
k, ctx, _, _ := keepertest.CrosschainKeeper(t)

// 1000000 lamports is the minimum amount (rent exempt) that can be withdrawn
chainID := chains.SolanaMainnet.ChainId
to := []byte(sample.SolanaAddress(t))
value := big.NewInt(constant.SolanaWalletRentExempt)
value := big.NewInt(constant.SolanaWalletRentExempt - 1)
solWithdrawalEvent := sample.ZRC20Withdrawal(to, value)

// 1000000 lamports can be withdrawn
err := k.ValidateZRC20WithdrawEvent(ctx, solWithdrawalEvent, chainID)
require.NoError(t, err)

// 999999 lamports cannot be withdrawn
solWithdrawalEvent.Value = big.NewInt(constant.SolanaWalletRentExempt - 1)
err = k.ValidateZRC20WithdrawEvent(ctx, solWithdrawalEvent, chainID)
err := k.ValidateZRC20WithdrawEvent(ctx, solWithdrawalEvent, chainID, coin.CoinType_Gas)
require.ErrorContains(t, err, "less than rent exempt")
})
}
Expand Down
2 changes: 1 addition & 1 deletion x/crosschain/keeper/v2_zevm_inbound.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
}

// validate data of the withdrawal event
if err := k.validateZRC20Withdrawal(ctx, foreignCoin.ForeignChainId, value, receiver); err != nil {
if err := k.validateZRC20Withdrawal(ctx, foreignCoin.ForeignChainId, foreignCoin.CoinType, value, receiver); err != nil {

Check warning on line 78 in x/crosschain/keeper/v2_zevm_inbound.go

View check run for this annotation

Codecov / codecov/patch

x/crosschain/keeper/v2_zevm_inbound.go#L78

Added line #L78 was not covered by tests
return err
}

Expand Down
Loading