@@ -38,6 +38,11 @@ contract GatewayEVM is
3838 /// @notice The address of the Zeta token contract.
3939 address public zetaToken;
4040
41+ /// @notice Fee charged for additional cross-chain actions within the same transaction.
42+ /// @dev The first action in a transaction is free, subsequent actions incur this fee.
43+ /// @dev This is configurable by the admin role to allow for fee adjustments.
44+ uint256 public additionalActionFeeWei;
45+
4146 /// @notice New role identifier for tss role.
4247 bytes32 public constant TSS_ROLE = keccak256 ("TSS_ROLE " );
4348 /// @notice New role identifier for asset handler role.
@@ -47,6 +52,11 @@ contract GatewayEVM is
4752 /// @notice Max size of payload + revertOptions revert message.
4853 uint256 public constant MAX_PAYLOAD_SIZE = 2880 ;
4954
55+ /// @notice Storage slot key for tracking transaction action count.
56+ /// @dev Uses transient storage (tload/tstore) for gas efficiency.
57+ /// @dev Value 0x01 is used as a unique identifier for this storage slot.
58+ uint256 private constant _TRANSACTION_ACTION_COUNT_KEY = 0x01 ;
59+
5060 /// @custom:oz-upgrades-unsafe-allow constructor
5161 constructor () {
5262 _disableInitializers ();
@@ -99,6 +109,17 @@ contract GatewayEVM is
99109 _unpause ();
100110 }
101111
112+ /// @notice Update the additional action fee.
113+ /// @dev Only callable by admin role. This allows for fee adjustments based on network conditions.
114+ /// @dev Setting fee to 0 disables additional action fees entirely.
115+ /// @param newFeeWei The new fee amount in wei for additional actions in the same transaction.
116+ /// @dev Fee should be adjusted based on the chain's native token decimals.
117+ function updateAdditionalActionFee (uint256 newFeeWei ) external onlyRole (DEFAULT_ADMIN_ROLE) {
118+ uint256 oldFee = additionalActionFeeWei;
119+ additionalActionFeeWei = newFeeWei;
120+ emit UpdatedAdditionalActionFee (oldFee, newFeeWei);
121+ }
122+
102123 /// @notice Transfers msg.value to destination contract and executes it's onRevert function.
103124 /// @dev This function can only be called by the TSS address and it is payable.
104125 /// @param destination Address to call.
@@ -234,18 +255,55 @@ contract GatewayEVM is
234255 /// @notice Deposits ETH to the TSS address.
235256 /// @param receiver Address of the receiver.
236257 /// @param revertOptions Revert options.
258+ /// @dev This function only works for the first action in a transaction (backward compatibility).
259+ /// @dev For subsequent actions, use the overloaded version with amount parameter.
237260 function deposit (address receiver , RevertOptions calldata revertOptions ) external payable whenNotPaused {
238261 if (msg .value == 0 ) revert InsufficientETHAmount ();
239262 if (receiver == address (0 )) revert ZeroAddress ();
240263 if (revertOptions.revertMessage.length > MAX_PAYLOAD_SIZE) revert PayloadSizeExceeded ();
241264
265+ // Check if this is a subsequent action (action index > 0)
266+ uint256 currentIndex = _getNextActionIndex ();
267+ if (currentIndex > 0 ) {
268+ revert AdditionalActionDisabled ();
269+ }
270+
271+ // Legacy behavior: transfer entire msg.value to TSS (no fee processing)
242272 (bool deposited ,) = tssAddress.call { value: msg .value }("" );
243273
244274 if (! deposited) revert DepositFailed ();
245275
246276 emit Deposited (msg .sender , receiver, msg .value , address (0 ), "" , revertOptions);
247277 }
248278
279+ /// @notice Deposits ETH to the TSS address with specified amount.
280+ /// @param receiver Address of the receiver.
281+ /// @param amount Amount of ETH to deposit (excluding fees).
282+ /// @param revertOptions Revert options.
283+ /// @dev msg.value must equal amount + required fee for the action.
284+ function deposit (
285+ address receiver ,
286+ uint256 amount ,
287+ RevertOptions calldata revertOptions
288+ )
289+ external
290+ payable
291+ whenNotPaused
292+ {
293+ if (amount == 0 ) revert InsufficientETHAmount ();
294+ if (receiver == address (0 )) revert ZeroAddress ();
295+ if (revertOptions.revertMessage.length > MAX_PAYLOAD_SIZE) revert PayloadSizeExceeded ();
296+
297+ uint256 feeCharged = _processFee ();
298+ _validateChargedFeeForETHWithAmount (amount, feeCharged);
299+
300+ (bool deposited ,) = tssAddress.call { value: amount }("" );
301+
302+ if (! deposited) revert DepositFailed ();
303+
304+ emit Deposited (msg .sender , receiver, amount, address (0 ), "" , revertOptions);
305+ }
306+
249307 /// @notice Deposits ERC20 tokens to the custody or connector contract.
250308 /// @param receiver Address of the receiver.
251309 /// @param amount Amount of tokens to deposit.
@@ -258,12 +316,16 @@ contract GatewayEVM is
258316 RevertOptions calldata revertOptions
259317 )
260318 external
319+ payable
261320 whenNotPaused
262321 {
263322 if (amount == 0 ) revert InsufficientERC20Amount ();
264323 if (receiver == address (0 )) revert ZeroAddress ();
265324 if (revertOptions.revertMessage.length > MAX_PAYLOAD_SIZE) revert PayloadSizeExceeded ();
266325
326+ uint256 feeCharged = _processFee ();
327+ _validateChargedFeeForERC20 (feeCharged);
328+
267329 _transferFromToAssetHandler (msg .sender , asset, amount);
268330
269331 emit Deposited (msg .sender , receiver, amount, asset, "" , revertOptions);
@@ -273,6 +335,8 @@ contract GatewayEVM is
273335 /// @param receiver Address of the receiver.
274336 /// @param payload Calldata to pass to the call.
275337 /// @param revertOptions Revert options.
338+ /// @dev This function only works for the first action in a transaction (backward compatibility).
339+ /// @dev For subsequent actions, use the overloaded version with amount parameter.
276340 function depositAndCall (
277341 address receiver ,
278342 bytes calldata payload ,
@@ -286,13 +350,50 @@ contract GatewayEVM is
286350 if (receiver == address (0 )) revert ZeroAddress ();
287351 if (payload.length + revertOptions.revertMessage.length > MAX_PAYLOAD_SIZE) revert PayloadSizeExceeded ();
288352
353+ // Check if this is a subsequent action (action index > 0)
354+ uint256 currentIndex = _getNextActionIndex ();
355+ if (currentIndex > 0 ) {
356+ revert AdditionalActionDisabled ();
357+ }
358+
359+ // Legacy behavior: transfer entire msg.value to TSS (no fee processing)
289360 (bool deposited ,) = tssAddress.call { value: msg .value }("" );
290361
291362 if (! deposited) revert DepositFailed ();
292363
293364 emit DepositedAndCalled (msg .sender , receiver, msg .value , address (0 ), payload, revertOptions);
294365 }
295366
367+ /// @notice Deposits ETH to the TSS address and calls an omnichain smart contract with specified amount.
368+ /// @param receiver Address of the receiver.
369+ /// @param amount Amount of ETH to deposit (excluding fees).
370+ /// @param payload Calldata to pass to the call.
371+ /// @param revertOptions Revert options.
372+ /// @dev msg.value must equal amount + required fee for the action.
373+ function depositAndCall (
374+ address receiver ,
375+ uint256 amount ,
376+ bytes calldata payload ,
377+ RevertOptions calldata revertOptions
378+ )
379+ external
380+ payable
381+ whenNotPaused
382+ {
383+ if (amount == 0 ) revert InsufficientETHAmount ();
384+ if (receiver == address (0 )) revert ZeroAddress ();
385+ if (payload.length + revertOptions.revertMessage.length > MAX_PAYLOAD_SIZE) revert PayloadSizeExceeded ();
386+
387+ uint256 feeCharged = _processFee ();
388+ _validateChargedFeeForETHWithAmount (amount, feeCharged);
389+
390+ (bool deposited ,) = tssAddress.call { value: amount }("" );
391+
392+ if (! deposited) revert DepositFailed ();
393+
394+ emit DepositedAndCalled (msg .sender , receiver, amount, address (0 ), payload, revertOptions);
395+ }
396+
296397 /// @notice Deposits ERC20 tokens to the custody or connector contract and calls an omnichain smart contract.
297398 /// @param receiver Address of the receiver.
298399 /// @param amount Amount of tokens to deposit.
@@ -307,12 +408,16 @@ contract GatewayEVM is
307408 RevertOptions calldata revertOptions
308409 )
309410 external
411+ payable
310412 whenNotPaused
311413 {
312414 if (amount == 0 ) revert InsufficientERC20Amount ();
313415 if (receiver == address (0 )) revert ZeroAddress ();
314416 if (payload.length + revertOptions.revertMessage.length > MAX_PAYLOAD_SIZE) revert PayloadSizeExceeded ();
315417
418+ uint256 feeCharged = _processFee ();
419+ _validateChargedFeeForERC20 (feeCharged);
420+
316421 _transferFromToAssetHandler (msg .sender , asset, amount);
317422
318423 emit DepositedAndCalled (msg .sender , receiver, amount, asset, payload, revertOptions);
@@ -328,12 +433,16 @@ contract GatewayEVM is
328433 RevertOptions calldata revertOptions
329434 )
330435 external
436+ payable
331437 whenNotPaused
332438 {
333439 if (revertOptions.callOnRevert) revert CallOnRevertNotSupported ();
334440 if (receiver == address (0 )) revert ZeroAddress ();
335441 if (payload.length + revertOptions.revertMessage.length > MAX_PAYLOAD_SIZE) revert PayloadSizeExceeded ();
336442
443+ uint256 feeCharged = _processFee ();
444+ _validateChargedFeeForERC20 (feeCharged);
445+
337446 emit Called (msg .sender , receiver, payload, revertOptions);
338447 }
339448
@@ -390,7 +499,7 @@ contract GatewayEVM is
390499 function _transferFromToAssetHandler (address from , address token , uint256 amount ) private {
391500 if (token == zetaToken) {
392501 // TODO: remove error and comment out code once ZETA supported back
393- // https://github.com/zeta-chain/protocol-contracts/issues/394
502+ // https://github.com/zeta-chain/protocol-contracts-evm /issues/394
394503 // ZETA token is currently not supported for deposit
395504 revert ZETANotSupported ();
396505
@@ -472,4 +581,70 @@ contract GatewayEVM is
472581 }
473582 }
474583 }
584+
585+ /// @notice Processes fee collection for cross-chain actions within a transaction.
586+ /// @dev The first action in a transaction is free, subsequent actions incur ADDITIONAL_ACTION_FEE_WEI.
587+ /// @dev If fee is 0, the entire functionality is disabled and will revert.
588+ /// @return The fee amount actually charged (0 for first action, ADDITIONAL_ACTION_FEE_WEI for
589+ /// subsequent actions).
590+ function _processFee () internal returns (uint256 ) {
591+ uint256 actionIndex = _getNextActionIndex ();
592+
593+ // First action is free
594+ if (actionIndex == 0 ) {
595+ return 0 ;
596+ }
597+
598+ // If fee is 0, functionality is disabled
599+ if (additionalActionFeeWei == 0 ) {
600+ revert AdditionalActionDisabled ();
601+ }
602+
603+ // Subsequent actions require fee payment
604+ if (msg .value < additionalActionFeeWei) {
605+ revert InsufficientFee (additionalActionFeeWei, msg .value );
606+ }
607+
608+ // Transfer fee to TSS address
609+ (bool success ,) = tssAddress.call { value: additionalActionFeeWei }("" );
610+ if (! success) {
611+ revert FeeTransferFailed ();
612+ }
613+
614+ return additionalActionFeeWei;
615+ }
616+
617+ /// @notice Validates fee payment for ERC20 operations (deposit, depositAndCall, call).
618+ /// @dev Validates that msg.value equals the required fee (no excess ETH allowed).
619+ /// @param feeCharged The fee amount that was charged.
620+ function _validateChargedFeeForERC20 (uint256 feeCharged ) internal view {
621+ // For ERC20 operations, msg.value must equal the required fee
622+ if (msg .value > feeCharged) {
623+ revert ExcessETHProvided (feeCharged, msg .value );
624+ }
625+ }
626+
627+ /// @notice Validates fee payment for ETH operations with specified amount.
628+ /// @dev Validates that msg.value equals amount + feeCharged.
629+ /// @param amount The amount to deposit (excluding fees).
630+ /// @param feeCharged The fee amount that was charged.
631+ function _validateChargedFeeForETHWithAmount (uint256 amount , uint256 feeCharged ) internal view {
632+ uint256 expectedValue = amount + feeCharged;
633+ if (msg .value != expectedValue) {
634+ revert IncorrectValueProvided (expectedValue, msg .value );
635+ }
636+ }
637+
638+ /// @notice Gets and increments the transaction action counter using transient storage.
639+ /// @dev Uses assembly for gas efficiency with tload/tstore operations.
640+ /// @dev Transient storage is transaction-scoped and automatically cleared after each transaction.
641+ /// @return currentIndex The current action index within the transaction (0-based).
642+ function _getNextActionIndex () internal returns (uint256 currentIndex ) {
643+ assembly {
644+ // Load current count from transient storage
645+ currentIndex := tload (_TRANSACTION_ACTION_COUNT_KEY)
646+ // Increment and store back to transient storage
647+ tstore (_TRANSACTION_ACTION_COUNT_KEY, add (currentIndex, 1 ))
648+ }
649+ }
475650}
0 commit comments