Skip to content

Commit

Permalink
fix: remove minimum rent exempt check for SPL token withdrawals (#3374)
Browse files Browse the repository at this point in the history
* remove minimum rent exempt check for SPL tokens

* lower default SPL withdraw amount in E2E to ensure min rent exempt is not checked

* add changelog entry

* add comment to explain why rent exempt check is only applied to Solana gas token
  • Loading branch information
ws4charlie authored Jan 20, 2025
1 parent 4bdfc0b commit 80f13ec
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 22 deletions.
4 changes: 4 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
* [3360](https://github.com/zeta-chain/node/pull/3360) - update protocol contract imports using consolidated path
* [3349](https://github.com/zeta-chain/node/pull/3349) - implement new bitcoin rpc in zetaclient with improved performance and observability

### Fixes

* [3374](https://github.com/zeta-chain/node/pull/3374) - remove minimum rent exempt check for SPL token withdrawals

## v25.0.0

## Unreleased
Expand Down
2 changes: 1 addition & 1 deletion e2e/e2etests/e2etests.go
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,7 @@ var AllE2ETests = []runner.E2ETest{
TestSPLWithdrawName,
"withdraw SPL from ZEVM",
[]runner.ArgDefinition{
{Description: "amount in spl tokens", DefaultValue: "1000000"},
{Description: "amount in spl tokens", DefaultValue: "100000"},
},
TestSPLWithdraw,
),
Expand Down
1 change: 1 addition & 0 deletions x/crosschain/keeper/cctx_orchestrator_validate_outbound.go
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
23 changes: 18 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,
) 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,9 @@ 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 {
// The rent exempt check is not needed for ZRC20 (SPL) tokens because withdrawing SPL token
// already needs a non-trivial amount of SOL for potential ATA creation so we can skip the check.
if coinType == coin.CoinType_Gas && value.Cmp(big.NewInt(constant.SolanaWalletRentExempt)) < 0 {
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 @@ func (k Keeper) ProcessZEVMInboundV2(
}

// 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 {
return err
}

Expand Down

0 comments on commit 80f13ec

Please sign in to comment.