Develop mechanism to refund unused tokens in connected chain call
Describe the Issue In connected chain calls, the gateway give approval of the asset to the target contract, for unused funds the amount is sent back to the custody.
This amount should be refunded to the sender instead.
Considered solution
Add a remaining value in the CCTX outbound data. This value is refunded back to the sender when the outbound is processed.
Listing down the changes needed to to avoid changes during implementation.
Using a withdraw and call as example (v2)
Smart contract changes
The ERC20Custody , calls the executeWithERC20
IERC20(token).safeTransfer(address(gateway), amount);
// Forward the call to the Gateway contract
gateway.executeWithERC20(token, to, amount, data);
emit WithdrawAndCall(token, to, amount, data);
}
// Transfer the tokens to the Gateway contract
//The executeWithERC20 returns the balance amount to the AssetHandler
// Transfer any remaining tokens back to the custody/connector contract
uint256 remainingBalance = IERC20(token).balanceOf(address(this));
if (remainingBalance > 0) {
transferToAssetHandler(token, remainingBalance);
}
To refund this amount to the original sender, we would need to add this amount to the WithdrawAndCall event or the ExecutedWithERC20 event
We are already using the WithdrawAndCall on zetaclient , so might make sense to add it there.
The advantage of adding it to ExecutedWithERC20 would be lesser modifications to smart contracts.
Zetaclient changes
- The parseOutboundReceivedValue already uses this event, we would be able to parse the new value and broadcast it back to zetacore when posting outbound Vote
Zetacore Changes
-
The VoteOutbound message should include the new field
-
We should send these tokens back to the caller (Handle if the transfer fails)
-
When initiating the original transfer, the ZRC20 equivalent for the ERC20 20 tokens are burned, and the ZRC20 tokens are
unlockedfrom the custody contract. [ We can ignore the fee calculations for the purpose of this document ] -
The unused tokens get locked back into the custody contract after the call is complete
-
These should be minted back to the user's address. This user can be an EOA initiating the withdrawal or a user initiating a withdrawal from a contract. (By calling a deposit)
Handle the refund
- If the address is an EOA, we can directly send the tokens back to the user on zEVM in the form of zrc20 tokens.
- If the address is a contract,
Option 1 : we could fetch the original
senderandsender chainusing the inbound hash, and create an outbound which can be processed on the sender chain. The outbound fee and protocol flat fee should be deducted from this amount. Option 2: However, since the amount in this case is nominal, I am not sure if it makes sense to send it back to the original user via an outbound cctx, As an alternative we could send it to a zevm address specified by the user (Similar to how we refund aborted cctx). Option 3: We could also let the contract handle these funds and add it to the Omni Channel contract interface. imo ,option 2 seems to be the best, but I am assuming this amount to be nominal.
This looks generally good to me, however I think at first we can focus on refunded the unused gas, not the unused tokens of the cctx value itself. I thought this was for this issue but realize it wasn't specific enough.
Refunding unused erc20 is also an issue to fix but it is less urgent for now, generally the contract will be designed to handle all the tokens.
For the gas it's a issue in priority because users will send high gas limit for complex ocntract and it might end up in lot of fund being unnecessarily lost.
We should report in the outbound vote the gas used, so the remaining gas * effective gas price is sent back to the user. We need to take into account the current gas price stability pool mechanism, there might be changes here to do to avoid artificial mint of new zrc02.
Option 2: However, since the amount in this case is nominal, I am not sure if it makes sense to send it back to the original user via an outbound cctx, As an alternative we could send it to a zevm address specified by the user (Similar to how we refund aborted cctx).
Let's keep it simple and always send tokens to sender, contract or not. In general we should not have in protocol logic that depends on contract, it should be up the the dev to decide how the funds are handled.
okay , that sounds good Wanted some clarity on what do you mean by funds lost . The funds currently end up getting minted back to the stability pool (95 percent of it ) , would you rather mint all these tokens back to the user / contract that created the withdraw ?
It might also make sense to refund a portion of it to the user and contribute the rest to the stability pool.
Wanted some clarity on what do you mean by funds lost . The funds currently end up getting minted back to the stability pool (95 percent of it ) , would you rather mint all these tokens back to the user / contract that created the withdraw ?
The recovered fund sent to the gas stability are based on the difference in the gas price, not the gas limit. If I'm not mistaken we still consider that all the gas limit has been consumed. It's been long time that I worked on this so might forget some details.
But the change here would be to calculate all funds that can be recovered based on (usedGasPriceForFeegasLimit)-(effectiveGasPriceusedGas) and give a portion back to the user, another portion to the gas stability pool (should depend on the chain)
I think the current implemtation does use the actual gasUsed , however there might be some cases where the remainingFees calculation might not be correct .
- When calculating the
remainingFeeswe use the following
-
GasPrice: we are using the final used gas Price,transaction.GasPrice(), which might be more than what the user paid for, this is fine as these additional tokens are withdrawn from the stability pool. -
RemainingGas: This is calculated bygasLimit - gasUsed, equivalent totransaction.Gas() - receipt.GasUsed, however, in cases where the limit is too high, zetaclient sets the gas limit to be maxGasLimit.
limit = params.CallOptions.GasLimit
if limit > maxGasLimit {
limit = maxGasLimit
}
Which is lower than what the user paid for.
We can calculate this unused gas value using callOptions.GasLimit - gasLimit(transaction.Gas()) and refund this to the user/contract
This would mean that the total gas is broken down into three parts 1 . Used by the TX 2 . Unused by the TX, upto the Max gas limit: We can fund this to the stability pool as the user signed the tx for the gasLimit 3 . Unused by the TX, above the Max gas limit: This is the amount we can refund to the user
Alternatively, can we move maxGasLimit check to the zEVM gateway contract, and not charge the user the extra gas in the first place?
We shoudl in fact prevent the user setting a gas limit higher than the limit at the smart contract level, this shouldn't be complex, I will create an issue in protocol-contracts