Skip to content

Commit a4d4fac

Browse files
kingpinXDlumtis
andauthored
feat: refund a portion of remaining unused tokens to user (#3734)
* add new field to cctx structure to track user gas burned * add unit tests * add doc * add unit test for migration * add check for pre upgrade cctxs * add older logic for handling cctxs created before upgrade * remove LegacyChainParams * add unit test for refund gas fee * remove old migration * add temp note for PR review * fix typo * fix typo * generate files * add missing test case * add missing test case * refactor based on comments * update EtherWithdrawRestricted test * update EtherWithdrawRestricted test * update EtherWithdrawRestricted test * enable all tests * update module version for observer migration * generated docs * generated docs * remove deprecated comments * use older chainparams for upgrade tests * use older chainparams for upgrade tests * changes based on comments 1 * add addition param to CallZRC20Deposit * update comments for legacy cctx and refactor unit tests to use the parent function ManageUnusedGasFee * adjust non zevm chain test cases * fix lint errors * fix comments and rebase develop * add statement to ErrParamsStabilityPoolPercentage * Update x/crosschain/keeper/msg_server_vote_outbound.go Co-authored-by: Lucas Bertrand <lucas.bertrand.22@gmail.com> * fix comments 3 * rebase develop 2 * rebase develop 2 * update admin test --------- Co-authored-by: Lucas Bertrand <lucas.bertrand.22@gmail.com>
1 parent 9ea65f6 commit a4d4fac

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1301
-516
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ start-upgrade-test-light: zetanode-upgrade
394394
start-upgrade-test-admin: zetanode-upgrade
395395
@echo "--> Starting admin upgrade test"
396396
export LOCALNET_MODE=upgrade && \
397-
export UPGRADE_HEIGHT=90 && \
397+
export UPGRADE_HEIGHT=60 && \
398398
export USE_ZETAE2E_ANTE=true && \
399399
export E2E_ARGS="${E2E_ARGS} --skip-regular --test-admin" && \
400400
cd contrib/localnet/ && $(DOCKER_COMPOSE) --profile upgrade -f docker-compose-upgrade.yml up -d

changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ by calling `updateAdditionalActionFee` admin function.
1818
* [4157](https://github.com/zeta-chain/node/pull/4157) - multiple evm calls in single tx
1919
* [4211](https://github.com/zeta-chain/node/pull/4211) - provide error information in cctx when Bitcoin deposit fail
2020
* [4218](https://github.com/zeta-chain/node/pull/4218) - enable NoAssetCall from Bitcoin chain
21+
* [3834](https://github.com/zeta-chain/node/pull/3734) - refund a portion of remaining unused tokens to user
2122

2223
### Refactor
2324

cmd/zetae2e/local/evm.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,11 @@ func startEVMTests(eg *errgroup.Group, conf config.Config, deployerRunner *runne
102102
evmTestRoutine(conf, "zeta", conf.AdditionalAccounts.UserZeta, color.FgRed, deployerRunner, verbose,
103103
e2etests.TestZetaDepositName,
104104
e2etests.TestETHDepositName,
105-
//e2etests.TestZetaDepositAndCallName,
106-
//e2etests.TestZetaDepositAndCallRevertName,
107-
//e2etests.TestZetaDepositRevertAndAbortName,
108-
//e2etests.TestZetaDepositAndCallRevertWithCallName,
109-
//e2etests.TestZetaDepositAndCallNoMessageName,
105+
e2etests.TestZetaDepositAndCallName,
106+
e2etests.TestZetaDepositAndCallRevertName,
107+
e2etests.TestZetaDepositRevertAndAbortName,
108+
e2etests.TestZetaDepositAndCallRevertWithCallName,
109+
e2etests.TestZetaDepositAndCallNoMessageName,
110110
e2etests.TestZetaWithdrawName,
111111
e2etests.TestZetaWithdrawAndCallName,
112112
e2etests.TestZetaWithdrawAndCallRevertName,

cmd/zetae2e/local/local.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ func localE2ETest(cmd *cobra.Command, _ []string) {
295295
deployerRunner.SetupZRC20(zrc20Deployment)
296296

297297
// Update the chain params to contains protocol contract addresses
298-
deployerRunner.UpdateProtocolContractsInChainParams(testLegacy)
298+
deployerRunner.UpdateEVMChainParams(testLegacy)
299299

300300
if shouldSetupTON {
301301
deployerRunner.SetupTON(
@@ -314,7 +314,10 @@ func localE2ETest(cmd *cobra.Command, _ []string) {
314314
deployerRunner.Logger.Print("Running post-upgrade setup for %s", runner.V36Version)
315315
err = OverwriteAccountData(cmd, &conf)
316316
require.NoError(deployerRunner, err, "Failed to override account data from the config file")
317-
deployerRunner.RunSetup(testLegacy)
317+
deployerRunner.RunSetup()
318+
if testAdmin {
319+
deployerRunner.UpdateEVMChainParams(false)
320+
}
318321
})
319322

320323
// if a config output is specified, write the config

docs/openapi/openapi.swagger.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54444,6 +54444,9 @@ definitions:
5444454444
confirmationMode:
5444554445
$ref: '#/definitions/zetachain.zetacore.crosschain.ConfirmationMode'
5444654446
title: confirmation mode used for the outbound
54447+
userGasFeePaid:
54448+
type: string
54449+
description: This field tracks the original gas fee paid by the user.
5444754450
zetachain.zetacore.crosschain.OutboundTracker:
5444854451
type: object
5444954452
properties:
@@ -55141,6 +55144,12 @@ definitions:
5514155144
description: |-
5514255145
Skip actions that require scanning the contents of each block.
5514355146
The main thing this disables is transfers directly to the TSS address.
55147+
stabilityPoolPercentage:
55148+
type: string
55149+
format: uint64
55150+
description: |-
55151+
Percentage of unused tokens for outbounds that are are sent to the
55152+
stability pool. The value should be between 0 and 100.
5514455153
zetachain.zetacore.observer.ChainParamsList:
5514555154
type: object
5514655155
properties:

docs/zetacore/gas_fee.md

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# ZetaChain Gas Fee Documentation
2+
3+
## Overview
4+
5+
Gas fees in ZetaChain refer to the amount paid by users to execute transactions on connected chains. These fees **do not** include any gas payments that users make on the source chain to initiate transactions.
6+
7+
## Key Concepts
8+
9+
- **Deposit**: Transfer tokens from a connected chain to ZetaChain
10+
- **Withdraw**: Transfer tokens from ZetaChain to a connected chain
11+
- **Coin Types**: Different token types used for transactions (Gas, ERC20, ZETA, No Asset)
12+
- **Revert**: When a transaction fails and needs to be reverted
13+
14+
## V2 Protocol Flows
15+
16+
### Deposit Transactions
17+
18+
Deposits transfer tokens from connected chains to ZetaChain.
19+
20+
- Gas Fee is always in GAS ZRC20. If the amount is in a different ERC20, it's swapped to buy Gas tokens
21+
22+
| Transaction Type | GAS FEE Deposit | GAS FEE Revert | Notes |
23+
|------------------|-----------------|----------------|-------|
24+
| Deposit | Free | User Pays based on cointype | Revert fees paid using the GAS ZRC20 on source chain |
25+
| DepositAndCall | Free | User Pays based on cointype | Revert fees paid using the GAS ZRC20 on source chain |
26+
| Call | Free | Revert Not supported | Revert fees paid using the GAS ZRC20 on source chain |
27+
28+
#### Gas Fee Payment by Coin Type
29+
30+
1. **CoinType ERC20**
31+
- User is using ERC20/ZRC20 tokens
32+
- `Protocol` calculates expected gas cost on connected chain, using `Current Gas Price` * `Revert Gas Limit` + Protocol Flat Fee
33+
- ERC20 tokens are swapped to buy gas ZRC20 tokens
34+
- Gas ZRC20 tokens are burned to pay for the gas fee
35+
36+
2. **CoinType Gas**
37+
- User paid in the native gas token on a connected chain
38+
- `Protocol` calculates expected gas cost on connected chain, using `Current Gas Price` * `Revert Gas Limit` + Protocol Flat Fee
39+
- Gas token can be directly burned to pay for the gas fee
40+
41+
3. **NoAssetCall**
42+
- No revert processing in this case, the cross-chain transaction is aborted
43+
44+
### Withdraw Transactions
45+
46+
Withdrawals transfer tokens from ZetaChain to connected chains.
47+
48+
| Transaction Type | GAS FEE Withdraw | GAS FEE Revert | Notes |
49+
|------------------|-----------------|----------------|-------|
50+
| Withdraw | User Pays based on withdraw type | Free call to OnRevert using fixed GasLimit | Initiated through Gateway smart contract |
51+
| WithdrawAndCall | User Pays based on withdraw type | Free call to OnRevert using fixed GasLimit | Initiated through Gateway smart contract |
52+
| Call | User Pays based on withdraw type | Free call to OnRevert using fixed GasLimit | Initiated through Gateway smart contract |
53+
54+
- Withdrawals are initiated by users through zEVM smart contract calls
55+
- Fees are always paid in the GAS ZRC20 token for the target connected chain, this fees is paid separately by the user an is not deducted from the amount.However when the transaction reverts, the revert fee is deducted from the amount
56+
- CCTX type is either GAS or ERC20 depending on the token
57+
58+
**Gas Fee Payment by withdraw type:**
59+
60+
1. **Withdraw**
61+
- `Gateway Smart contract` calculates expected gas cost on connected chain based on `gasPrice` * `ZRC20.GasLimit` + Protocol Flat Fee
62+
63+
2. **WithdrawAndCall**
64+
- `Gateway Smart contract` calculates expected gas cost on connected chain based on `gasPrice` * `CallOptions.GasLimit` + Protocol Flat Fee
65+
66+
3. **Call**
67+
- `Gateway Smart contract` calculates expected gas cost on connected chain based on `gasPrice` * `ZRC20.GasLimit` + Protocol Flat Fee
68+
69+
## V1 Protocol Flows
70+
71+
- V1 flows for ERC20/ZRC20 and GAS tokens have already been deprecated
72+
- V1 flows for ZETA token deposits and withdrawals are still supported but are planned to be deprecated soon.
73+
74+
### Deposit Transactions
75+
76+
| Transaction Type | GAS FEE Deposit | GAS FEE Revert | Notes |
77+
|------------------|-----------------|----------------|-------|
78+
| Deposit zEVM | Free | User pays based on coin type | Gas fee for revert based on cointype |
79+
| Deposit Connected Chain (Msg-Passing) | User pays based on coin type | User pays based on coin type | Fees in connected chain's gas token |
80+
81+
#### Gas Fee Payment by Coin Type
82+
83+
1. **CoinType Zeta**
84+
- User is using ERC20/ZRC20 "Zeta Token"
85+
- `Protocol` calculates expected gas cost on connected chain using `gasPrice * 2` * `CallOptions.GasLimit` + Protocol Flat Fee
86+
- Zeta tokens are swapped to buy gas ZRC20 tokens
87+
- Gas tokens are burned to pay for the gas fee
88+
- **Important**: Gas price is multiplied by 2× when calculating the fee
89+
90+
2. **CoinType ERC20**
91+
- User is using ERC20/ZRC20 tokens
92+
- `Protocol` calculates expected gas cost on connected chain using `gasPrice` * `GasZRC20.GasLimit` + Protocol Flat Fee
93+
- ERC20 tokens are swapped to buy gas ZRC20 tokens
94+
- Gas tokens are burned to pay for the gas fee
95+
96+
3. **CoinType Gas**
97+
- User paid in native gas token on connected chain
98+
- `Protocol` calculates expected gas cost on connected chain using `gasPrice` * `GasZRC20.GasLimit` + Protocol Flat Fee
99+
- Gas token can be directly burned to pay for fee
100+
101+
### Withdraw Transactions
102+
103+
| Transaction Type | GAS FEE Deposit | GAS FEE Revert | Notes |
104+
|------------------|-----------------|----------------|-------|
105+
| ZRC20 Withdraw | User pays | Not supported | Uses same mechanism as V2 withdrawals |
106+
| ZETA Sent | User pays | Free call to OnRevert using fixed GasLimit | Total Zeta (value + gas) is burned |
107+
108+
#### Details by Withdraw Type
109+
110+
1. **ZRC20 Withdraw**
111+
- Uses the same mechanism as V2 withdrawals but initiated through ZRC20 contract
112+
113+
2. **ZETA Sent**
114+
- Total Zeta amount (value + gas) is burned
115+
- A portion is minted to swap and pay for gas in outbound chain
116+
- `Protocol` calculates expected gas cost on a connected chain using `gasPrice * 2` * `CallOptions.GasLimit` + Protocol Flat Fee
117+
- Value portion is unlocked in connected chain after successful outbound transaction
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Management for Unused Gas Fee
2+
3+
## Overview
4+
5+
This document explains how gas fees are managed when processing outbound transactions on connected chains, particularly focusing on how differences between estimated and actual gas usage are handled.
6+
The Gas fee is always paid in the Gas ZRC20 token of the connected chain ,irrespective of coin type.
7+
8+
## Gas Fee Scenarios
9+
10+
When processing outbound transactions, there are two possible scenarios regarding gas fees:
11+
12+
1. **User Overpays** (Common)
13+
- Users often overpay gas fees to ensure transactions are processed
14+
- EVM chains automatically refund unused gas to the caller (TSS address)
15+
- This creates an opportunity to return a portion of these funds to users
16+
17+
2. **User Underpays**
18+
- Gas prices may increase after transaction submission
19+
- In these cases, the stability pool covers the difference
20+
- No refund mechanism applies in underpayment scenarios
21+
22+
## Refund Mechanism for Overpayments
23+
24+
For overpayment scenarios, we implement the following refund logic:
25+
26+
### Fee Tracking
27+
28+
- We track the initial gas fee paid by the user when initiating the transaction. This does not include the Protocol Fee.
29+
- When an outbound transaction completes (regardless of status), we calculate the actual fee used:
30+
```
31+
actualFee = receipt.GasUsed * transaction.GasPrice()
32+
```
33+
34+
### Difference in Fee Calculation
35+
36+
- The difference in fee is calculated as:
37+
```
38+
totalRemainingFees = userGasFeePaid - actualFee
39+
```
40+
- We then take 95% of this amount for further calculations:
41+
```
42+
remainingFees = 95% of totalRemainingFees
43+
```
44+
- We intentionally use only 95% of the unused fee to avoid any potential over-minting:
45+
- The 5% of tokens that are not minted back are retained by the TSS address
46+
- This creates a safety buffer since we burn the total amount of tokens when initiating the transaction, which creates a deficit
47+
48+
### Fee Distribution
49+
50+
The remaining fees are distributed as follows:
51+
52+
1. **Stability Pool Allocation**
53+
- For non-EVM chains: 100% of this amount goes to the stability pool
54+
- For EVM chains: A configurable percentage (defined in the chain parameters) goes to the stability pool
55+
56+
2. **User Refund**
57+
- For EVM chains: The remaining amount after stability pool allocation is refunded to the user
58+
- Refunds are provided as ZRC20 gas tokens of the connected chain
59+
60+
3. **Fallback**
61+
- If refunding fails for any reason, all remaining fees are allocated to the stability pool
62+
63+
## Implementation Details
64+
65+
- The refund is always in the form of gas tokens from the connected chain
66+
- The stability pool funded is always associated with the gas token of the outbound chain
67+
- The system requires chain parameters to be properly configured
68+
- The mechanism handles various edge cases (invalid addresses, zero amounts, etc.).
69+
- We refund the amount to the sender irrespective of whether it is a user or a contract. The contract should implement a mechanism to handle the refund.

e2e/e2etests/e2etests.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1877,7 +1877,7 @@ var AllE2ETests = []runner.E2ETest{
18771877
{Description: "amount in wei", DefaultValue: "100000"},
18781878
},
18791879
TestEtherWithdrawRestricted,
1880-
runner.WithMinimumVersion("v30.0.0"),
1880+
runner.WithMinimumVersion("v37.0.0"),
18811881
),
18821882
runner.NewE2ETest(
18831883
TestLegacyEtherDepositAndCallRefundName,

e2e/e2etests/test_eth_withdraw_restricted_address.go

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,17 @@ package e2etests
33
import (
44
"math/big"
55

6+
"cosmossdk.io/math"
67
"github.com/ethereum/go-ethereum/accounts/abi/bind"
78
ethcommon "github.com/ethereum/go-ethereum/common"
89
"github.com/stretchr/testify/require"
910
"github.com/zeta-chain/protocol-contracts/pkg/gatewayzevm.sol"
1011

1112
"github.com/zeta-chain/node/e2e/runner"
1213
"github.com/zeta-chain/node/e2e/utils"
14+
crosschainkeeper "github.com/zeta-chain/node/x/crosschain/keeper"
1315
crosschaintypes "github.com/zeta-chain/node/x/crosschain/types"
16+
observertypes "github.com/zeta-chain/node/x/observer/types"
1417
)
1518

1619
// TestEtherWithdrawRestricted tests the withdrawal to a restricted receiver address
@@ -71,5 +74,39 @@ func TestEtherWithdrawRestricted(r *runner.E2ERunner, args []string) {
7174
// revert address should receive the amount
7275
revertBalanceAfter, err := r.ETHZRC20.BalanceOf(&bind.CallOpts{}, revertAddress)
7376
require.NoError(r, err)
74-
require.EqualValues(r, new(big.Int).Add(revertBalanceBefore, amount), revertBalanceAfter)
77+
78+
userBalanceAfterUint := math.NewUintFromBigInt(revertBalanceAfter)
79+
userBalanceBeforeUint := math.NewUintFromBigInt(revertBalanceBefore)
80+
totalRevertAmount := getTotalRevertedAmount(r, cctx)
81+
82+
require.EqualValues(r, userBalanceAfterUint.Sub(totalRevertAmount), userBalanceBeforeUint)
83+
}
84+
85+
func getTotalRevertedAmount(r *runner.E2ERunner, cctx *crosschaintypes.CrossChainTx) math.Uint {
86+
outboundParams := cctx.OutboundParams[0]
87+
outboundTxGasUsed := math.NewUint(outboundParams.GasUsed)
88+
outboundTxFinalGasPrice := math.NewUintFromBigInt(outboundParams.EffectiveGasPrice.BigInt())
89+
outboundTxFeePaid := outboundTxGasUsed.Mul(outboundTxFinalGasPrice)
90+
userGasFeePaid := outboundParams.UserGasFeePaid
91+
totalRemainingFees := userGasFeePaid.Sub(outboundTxFeePaid)
92+
93+
remainingFees := crosschainkeeper.PercentOf(totalRemainingFees, crosschaintypes.UsableRemainingFeesPercentage)
94+
if remainingFees.LTE(math.ZeroUint()) {
95+
return math.ZeroUint()
96+
}
97+
98+
evmChainID, err := r.EVMClient.ChainID(r.Ctx)
99+
require.NoError(r, err)
100+
101+
chainParams, err := r.ObserverClient.GetChainParamsForChain(
102+
r.Ctx,
103+
&observertypes.QueryGetChainParamsForChainRequest{
104+
ChainId: evmChainID.Int64(),
105+
},
106+
)
107+
require.NoError(r, err)
108+
109+
stabilityPoolAmount := crosschainkeeper.PercentOf(remainingFees, chainParams.ChainParams.StabilityPoolPercentage)
110+
refundAmount := remainingFees.Sub(stabilityPoolAmount)
111+
return cctx.InboundParams.Amount.Add(refundAmount)
75112
}

e2e/runner/setup_zetacore_configuration.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ import (
1313
observertypes "github.com/zeta-chain/node/x/observer/types"
1414
)
1515

16-
// UpdateProtocolContractsInChainParams update the erc20 custody contract and gateway address in the chain params
16+
// UpdateEVMChainParams update the erc20 custody contract and gateway address in the chain params
1717
// TODO: should be used for all protocol contracts including the ZETA connector
1818
// https://github.com/zeta-chain/node/issues/3257
19-
func (r *E2ERunner) UpdateProtocolContractsInChainParams(testLegacy bool) {
19+
func (r *E2ERunner) UpdateEVMChainParams(testLegacy bool) {
2020
res, err := r.ObserverClient.GetChainParams(r.Ctx, &observertypes.QueryGetChainParamsRequest{})
2121
require.NoError(r, err)
2222

0 commit comments

Comments
 (0)