From 035905282096467249553a0fdaab2f42e6280d6c Mon Sep 17 00:00:00 2001
From: Rares <6453351+raress96@users.noreply.github.com>
Date: Mon, 18 Nov 2024 16:18:33 +0200
Subject: [PATCH] handle all todos and check available gas fees before sending
 transaction.

---
 .env.example                                  |   2 +
 .env.test                                     |   2 +
 .../approvals.processor.service.ts            |  13 +-
 .../approvals.processor.spec.ts               |  32 ++-
 .../processors/gateway.processor.spec.ts      |   1 +
 .../message-approved.processor.service.ts     |  60 ++++--
 .../message-approved.processor.e2e-spec.ts    | 143 +++++++++++--
 libs/common/src/config/api.config.service.ts  |   4 +
 libs/common/src/contracts/contracts.module.ts |   3 +
 .../src/contracts/entities/gas.error.ts       |   2 +
 libs/common/src/contracts/fee.helper.spec.ts  | 199 ++++++++++++++++++
 libs/common/src/contracts/fee.helper.ts       |  82 ++++++++
 .../repository/message-approved.repository.ts |  27 ++-
 .../migration.sql                             |   2 +
 prisma/schema.prisma                          |  27 +--
 15 files changed, 516 insertions(+), 83 deletions(-)
 create mode 100644 libs/common/src/contracts/fee.helper.spec.ts
 create mode 100644 libs/common/src/contracts/fee.helper.ts
 create mode 100644 prisma/migrations/20241118081638_add_available_gas_balance/migration.sql

diff --git a/.env.example b/.env.example
index 84b7e66..cbb9e5d 100644
--- a/.env.example
+++ b/.env.example
@@ -23,3 +23,5 @@ WALLET_MNEMONIC=
 
 CLIENT_CERT=
 CLIENT_KEY=
+
+ENABLE_GAS_CHECK=1
diff --git a/.env.test b/.env.test
index f9ad5fd..7e062a0 100644
--- a/.env.test
+++ b/.env.test
@@ -24,3 +24,5 @@ WALLET_MNEMONIC="fitness horror fluid six mutual ahead upon zone install stadium
 
 CLIENT_CERT=test
 CLIENT_KEY=test
+
+ENABLE_GAS_CHECK=1
diff --git a/apps/axelar-event-processor/src/approvals-processor/approvals.processor.service.ts b/apps/axelar-event-processor/src/approvals-processor/approvals.processor.service.ts
index 94769b4..78698de 100644
--- a/apps/axelar-event-processor/src/approvals-processor/approvals.processor.service.ts
+++ b/apps/axelar-event-processor/src/approvals-processor/approvals.processor.service.ts
@@ -209,8 +209,7 @@ export class ApprovalsProcessorService {
   }
 
   private async processExecuteTask(response: ExecuteTask, taskItemId: string) {
-    // TODO: Should we also save response.availableGasBalance and check if enough gas was payed before executing?
-    const messageApproved = await this.messageApprovedRepository.create({
+    await this.messageApprovedRepository.createOrUpdate({
       sourceChain: response.message.sourceChain,
       messageId: response.message.messageID,
       status: MessageApprovedStatus.PENDING,
@@ -220,15 +219,9 @@ export class ApprovalsProcessorService {
       payload: Buffer.from(response.payload, 'base64'),
       retry: 0,
       taskItemId,
+      // Only support native token for gas
+      availableGasBalance: !response.availableGasBalance.tokenID ? response.availableGasBalance.amount : '0',
     });
-
-    if (!messageApproved) {
-      this.logger.warn(
-        `Couldn't save message approved to database, duplicate exists for source chain ${response.message.sourceChain} and message id ${response.message.messageID}`,
-      );
-
-      return;
-    }
   }
 
   private async processRefundTask(response: RefundTask) {
diff --git a/apps/axelar-event-processor/src/approvals-processor/approvals.processor.spec.ts b/apps/axelar-event-processor/src/approvals-processor/approvals.processor.spec.ts
index 6d9cee5..2c0a75c 100644
--- a/apps/axelar-event-processor/src/approvals-processor/approvals.processor.spec.ts
+++ b/apps/axelar-event-processor/src/approvals-processor/approvals.processor.spec.ts
@@ -225,7 +225,7 @@ describe('ApprovalsProcessorService', () => {
                 task: {
                   payload: BinaryUtils.hexToBase64('0123'),
                   availableGasBalance: {
-                    amount: '0',
+                    amount: '100',
                   },
                   message: {
                     messageID: 'messageId',
@@ -246,8 +246,8 @@ describe('ApprovalsProcessorService', () => {
 
       await service.handleNewTasksRaw();
 
-      expect(messageApprovedRepository.create).toHaveBeenCalledTimes(1);
-      expect(messageApprovedRepository.create).toHaveBeenCalledWith({
+      expect(messageApprovedRepository.createOrUpdate).toHaveBeenCalledTimes(1);
+      expect(messageApprovedRepository.createOrUpdate).toHaveBeenCalledWith({
         sourceChain: 'ethereum',
         messageId: 'messageId',
         status: MessageApprovedStatus.PENDING,
@@ -257,11 +257,12 @@ describe('ApprovalsProcessorService', () => {
         payload: Buffer.from('0123', 'hex'),
         retry: 0,
         taskItemId: 'UUID',
+        availableGasBalance: '100',
       });
       expect(redisCacheService.set).toHaveBeenCalledTimes(1);
     });
 
-    it('Should handle execute task duplicate in database', async () => {
+    it('Should handle execute task invalid gas token', async () => {
       axelarGmpApi.getTasks.mockReturnValueOnce(
         // @ts-ignore
         Promise.resolve({
@@ -270,16 +271,17 @@ describe('ApprovalsProcessorService', () => {
               {
                 type: 'EXECUTE',
                 task: {
-                  payload: '0123',
+                  payload: BinaryUtils.hexToBase64('0123'),
                   availableGasBalance: {
-                    amount: '0',
+                    tokenID: 'other',
+                    amount: '100',
                   },
                   message: {
                     messageID: 'messageId',
                     destinationAddress: 'destinationAddress',
                     sourceAddress: 'sourceAddress',
                     sourceChain: 'ethereum',
-                    payloadHash: '0234',
+                    payloadHash: BinaryUtils.hexToBase64('0234'),
                   },
                 } as ExecuteTask,
                 id: 'UUID',
@@ -291,11 +293,21 @@ describe('ApprovalsProcessorService', () => {
         }),
       );
 
-      messageApprovedRepository.create.mockReturnValueOnce(Promise.resolve(null));
-
       await service.handleNewTasksRaw();
 
-      expect(messageApprovedRepository.create).toHaveBeenCalledTimes(1);
+      expect(messageApprovedRepository.createOrUpdate).toHaveBeenCalledTimes(1);
+      expect(messageApprovedRepository.createOrUpdate).toHaveBeenCalledWith({
+        sourceChain: 'ethereum',
+        messageId: 'messageId',
+        status: MessageApprovedStatus.PENDING,
+        sourceAddress: 'sourceAddress',
+        contractAddress: 'destinationAddress',
+        payloadHash: '0234',
+        payload: Buffer.from('0123', 'hex'),
+        retry: 0,
+        taskItemId: 'UUID',
+        availableGasBalance: '0',
+      });
       expect(redisCacheService.set).toHaveBeenCalledTimes(1);
     });
 
diff --git a/apps/mvx-event-processor/src/cross-chain-transaction-processor/processors/gateway.processor.spec.ts b/apps/mvx-event-processor/src/cross-chain-transaction-processor/processors/gateway.processor.spec.ts
index 9954efe..9431790 100644
--- a/apps/mvx-event-processor/src/cross-chain-transaction-processor/processors/gateway.processor.spec.ts
+++ b/apps/mvx-event-processor/src/cross-chain-transaction-processor/processors/gateway.processor.spec.ts
@@ -236,6 +236,7 @@ describe('GatewayProcessor', () => {
         createdAt: new Date(),
         successTimes: null,
         taskItemId: null,
+        availableGasBalance: '0',
       };
 
       messageApprovedRepository.findBySourceChainAndMessageId.mockReturnValueOnce(Promise.resolve(messageApproved));
diff --git a/apps/mvx-event-processor/src/message-approved-processor/message-approved.processor.service.ts b/apps/mvx-event-processor/src/message-approved-processor/message-approved.processor.service.ts
index 8df4526..5f01f63 100644
--- a/apps/mvx-event-processor/src/message-approved-processor/message-approved.processor.service.ts
+++ b/apps/mvx-event-processor/src/message-approved-processor/message-approved.processor.service.ts
@@ -17,15 +17,15 @@ import { TransactionsHelper } from '@mvx-monorepo/common/contracts/transactions.
 import { ApiConfigService, AxelarGmpApi } from '@mvx-monorepo/common';
 import { ItsContract } from '@mvx-monorepo/common/contracts/its.contract';
 import { Locker } from '@multiversx/sdk-nestjs-common';
-import { GasError } from '@mvx-monorepo/common/contracts/entities/gas.error';
+import { GasError, NotEnoughGasError } from '@mvx-monorepo/common/contracts/entities/gas.error';
 import {
   CannotExecuteMessageEvent,
-  CannotExecuteMessageEventV2,
+  CannotExecuteMessageReason,
   Event,
 } from '@mvx-monorepo/common/api/entities/axelar.gmp.api';
 import { AxiosError } from 'axios';
 import { DecodingUtils } from '@mvx-monorepo/common/utils/decoding.utils';
-import { CONSTANTS } from '@mvx-monorepo/common/utils/constants.enum';
+import { FeeHelper } from '@mvx-monorepo/common/contracts/fee.helper';
 
 // Support a max of 3 retries (mainly because some Interchain Token Service endpoints need to be called 2 times)
 const MAX_NUMBER_OF_RETRIES: number = 3;
@@ -43,6 +43,7 @@ export class MessageApprovedProcessorService {
     private readonly transactionsHelper: TransactionsHelper,
     private readonly itsContract: ItsContract,
     private readonly axelarGmpApi: AxelarGmpApi,
+    private readonly feeHelper: FeeHelper,
     apiConfigService: ApiConfigService,
   ) {
     this.logger = new Logger(MessageApprovedProcessorService.name);
@@ -70,7 +71,7 @@ export class MessageApprovedProcessorService {
         const entriesToUpdate: MessageApproved[] = [];
         const entriesWithTransactions: MessageApproved[] = [];
         for (const messageApproved of entries) {
-          if (messageApproved.retry === MAX_NUMBER_OF_RETRIES) {
+          if (messageApproved.retry >= MAX_NUMBER_OF_RETRIES) {
             await this.handleMessageApprovedFailed(messageApproved);
 
             entriesToUpdate.push(messageApproved);
@@ -106,16 +107,20 @@ export class MessageApprovedProcessorService {
 
             entriesWithTransactions.push(messageApproved);
           } catch (e) {
-            this.logger.error(
-              `Could not build and sign execute transaction for ${messageApproved.sourceChain} ${messageApproved.messageId}`,
-              e,
-            );
+            // In case of NotEnoughGasError, don't retry the transaction and mark it as failed instantly
+            if (e instanceof NotEnoughGasError) {
+              messageApproved.retry = MAX_NUMBER_OF_RETRIES;
+              messageApproved.status = MessageApprovedStatus.FAILED;
 
-            if (e instanceof GasError) {
-              messageApproved.retry += 1;
+              await this.handleMessageApprovedFailed(messageApproved, 'INSUFFICIENT_GAS');
 
               entriesToUpdate.push(messageApproved);
             } else {
+              this.logger.error(
+                `Could not build and sign execute transaction for ${messageApproved.sourceChain} ${messageApproved.messageId}`,
+                e,
+              );
+
               throw e;
             }
           }
@@ -160,8 +165,24 @@ export class MessageApprovedProcessorService {
       .withChainID(this.chainId)
       .buildTransaction();
 
-    const gas = await this.transactionsHelper.getTransactionGas(transaction, messageApproved.retry);
-    transaction.setGasLimit(gas);
+    try {
+      const gas = await this.transactionsHelper.getTransactionGas(transaction, messageApproved.retry);
+      transaction.setGasLimit(gas);
+
+      this.feeHelper.checkGasCost(gas, transaction.getValue(), transaction.getData(), messageApproved);
+    } catch (e) {
+      // In case the gas estimation fails, the transaction will fail on chain, but we will still send it
+      // for transparency with the full gas available, but don't try to retry it
+      if (e instanceof GasError) {
+        transaction.setGasLimit(
+          this.feeHelper.getGasLimitFromEgldFee(BigInt(messageApproved.availableGasBalance), transaction.getData()),
+        );
+
+        messageApproved.retry = MAX_NUMBER_OF_RETRIES - 1;
+      } else {
+        throw e;
+      }
+    }
 
     const signature = await this.walletSigner.sign(transaction.serializeForSigning());
     transaction.applySignature(signature);
@@ -201,24 +222,23 @@ export class MessageApprovedProcessorService {
     );
   }
 
-  private async handleMessageApprovedFailed(messageApproved: MessageApproved) {
+  private async handleMessageApprovedFailed(
+    messageApproved: MessageApproved,
+    reason: CannotExecuteMessageReason = 'ERROR',
+  ) {
     this.logger.error(
       `Could not execute MessageApproved from ${messageApproved.sourceChain} with message id ${messageApproved.messageId} after ${messageApproved.retry} retries`,
     );
 
     messageApproved.status = MessageApprovedStatus.FAILED;
 
-    const cannotExecuteEvent: CannotExecuteMessageEventV2 = {
+    const cannotExecuteEvent: CannotExecuteMessageEvent = {
       eventID: messageApproved.executeTxHash
         ? DecodingUtils.getEventId(messageApproved.executeTxHash, 0)
         : messageApproved.messageId,
-      messageID: messageApproved.messageId,
-      sourceChain: CONSTANTS.SOURCE_CHAIN_NAME,
-      reason: 'ERROR',
+      reason,
       details: '',
-      meta: {
-        taskItemID: messageApproved.taskItemId || '',
-      },
+      taskItemID: messageApproved.taskItemId || '',
     };
 
     try {
diff --git a/apps/mvx-event-processor/test/message-approved.processor.e2e-spec.ts b/apps/mvx-event-processor/test/message-approved.processor.e2e-spec.ts
index edb42ae..7402b34 100644
--- a/apps/mvx-event-processor/test/message-approved.processor.e2e-spec.ts
+++ b/apps/mvx-event-processor/test/message-approved.processor.e2e-spec.ts
@@ -63,6 +63,15 @@ describe('MessageApprovedProcessorService', () => {
       return Promise.resolve(null);
     });
 
+    proxy.getNetworkConfig.mockImplementation((): Promise<any> => {
+      return Promise.resolve({
+        MinGasPrice: 1000000000,
+        MinGasLimit: 50000,
+        GasPerDataByte: 1500,
+        GasPriceModifier: 0.01,
+      });
+    });
+
     // Reset database & cache
     await prisma.messageApproved.deleteMany();
 
@@ -77,7 +86,7 @@ describe('MessageApprovedProcessorService', () => {
   });
 
   const createMessageApproved = async (extraData: Partial<MessageApproved> = {}): Promise<MessageApproved> => {
-    const result = await messageApprovedRepository.create({
+    await messageApprovedRepository.createOrUpdate({
       sourceAddress: 'sourceAddress',
       messageId: 'messageId',
       status: MessageApprovedStatus.PENDING,
@@ -89,14 +98,19 @@ describe('MessageApprovedProcessorService', () => {
       executeTxHash: null,
       updatedAt: new Date(),
       createdAt: new Date(),
+      availableGasBalance: '0',
       ...extraData,
     });
 
-    if (!result) {
-      throw new Error('Can not create database entries');
-    }
-
-    return result;
+    // @ts-ignore
+    return await prisma.messageApproved.findUnique({
+      where: {
+        sourceChain_messageId: {
+          sourceChain: extraData.sourceChain || 'ethereum',
+          messageId: extraData.messageId || 'messageId',
+        },
+      },
+    });
   };
 
   const assertArgs = (transaction: Transaction, entry: MessageApproved) => {
@@ -110,12 +124,15 @@ describe('MessageApprovedProcessorService', () => {
   };
 
   it('Should send execute transaction two initial', async () => {
-    const originalFirstEntry = await createMessageApproved();
+    const originalFirstEntry = await createMessageApproved({
+      availableGasBalance: '1200000000000000',
+    });
     const originalSecondEntry = await createMessageApproved({
       sourceChain: 'polygon',
       messageId: 'messageId2',
       sourceAddress: 'otherSourceAddress',
       payload: Buffer.from('otherPayload'),
+      availableGasBalance: '1200000000000000',
     });
 
     proxy.sendTransactions.mockImplementation((transactions): Promise<string[]> => {
@@ -176,6 +193,7 @@ describe('MessageApprovedProcessorService', () => {
     const originalFirstEntry = await createMessageApproved({
       retry: 1,
       updatedAt: new Date(new Date().getTime() - 60_500),
+      availableGasBalance: '1200000000000000',
     });
     const originalSecondEntry = await createMessageApproved({
       sourceChain: 'polygon',
@@ -185,11 +203,13 @@ describe('MessageApprovedProcessorService', () => {
       retry: 3,
       updatedAt: new Date(new Date().getTime() - 60_500),
       taskItemId: '0191ead2-2234-7310-b405-76e787415031',
+      availableGasBalance: '1200000000000000',
     });
     // Entry will not be processed (updated too early)
     const originalThirdEntry = await createMessageApproved({
       messageId: 'messageId3',
       retry: 1,
+      availableGasBalance: '1200000000000000',
     });
 
     proxy.sendTransactions.mockImplementation((transactions): Promise<string[]> => {
@@ -246,13 +266,9 @@ describe('MessageApprovedProcessorService', () => {
     expect(axelarGmpApi.postEvents.mock.lastCall[0][0]).toEqual({
       type: 'CANNOT_EXECUTE_MESSAGE',
       eventID: originalSecondEntry.messageId,
-      messageID: originalSecondEntry.messageId,
-      sourceChain: 'multiversx',
       reason: 'ERROR',
       details: '',
-      meta: {
-        taskItemID: originalSecondEntry.taskItemId,
-      },
+      taskItemID: originalSecondEntry.taskItemId,
     });
 
     // Was not updated
@@ -266,7 +282,9 @@ describe('MessageApprovedProcessorService', () => {
   });
 
   it('Should send execute transaction not successfully sent', async () => {
-    const originalFirstEntry = await createMessageApproved();
+    const originalFirstEntry = await createMessageApproved({
+      availableGasBalance: '1200000000000000',
+    });
     const originalSecondEntry = await createMessageApproved({
       sourceChain: 'polygon',
       messageId: 'messageId2',
@@ -274,6 +292,7 @@ describe('MessageApprovedProcessorService', () => {
       payload: Buffer.from('otherPayload'),
       retry: 2,
       updatedAt: new Date(new Date().getTime() - 60_500),
+      availableGasBalance: '1200000000000000',
     });
 
     proxy.sendTransactions.mockImplementation((): Promise<string[]> => {
@@ -324,10 +343,11 @@ describe('MessageApprovedProcessorService', () => {
     });
   }
 
-  it('Should send execute transaction retry on gas failure', async () => {
+  it('Should send execute transaction do not retry on gas failure', async () => {
     const originalFirstEntry = await createMessageApproved({
       retry: 1,
       updatedAt: new Date(new Date().getTime() - 60_500),
+      availableGasBalance: '1200000000000000',
     });
 
     proxy.sendTransactions.mockImplementation((transactions): Promise<string[]> => {
@@ -340,6 +360,36 @@ describe('MessageApprovedProcessorService', () => {
 
     await service.processPendingMessageApproved();
 
+    expect(proxy.getAccount).toHaveBeenCalledTimes(1);
+    expect(proxy.doPostGeneric).toHaveBeenCalledTimes(1);
+    // Transaction is sent even though it will fail
+    expect(proxy.sendTransactions).toHaveBeenCalledTimes(1);
+
+    // No contract call approved pending remained for now
+    expect(await messageApprovedRepository.findPending()).toEqual([]);
+
+    // Expect entries in database updated
+    const firstEntry = await messageApprovedRepository.findBySourceChainAndMessageId(
+      originalFirstEntry.sourceChain,
+      originalFirstEntry.messageId,
+    );
+    expect(firstEntry).toEqual({
+      ...originalFirstEntry,
+      executeTxHash: '90d4f525856840a5c9c8115a30e87d823ac8261b298ca4ecb42f1b806fec363c',
+      retry: 3,
+      updatedAt: expect.any(Date),
+    });
+  });
+
+  it('Should not send execute transaction if not enough gas', async () => {
+    const originalFirstEntry = await createMessageApproved({
+      retry: 1,
+      updatedAt: new Date(new Date().getTime() - 60_500),
+      availableGasBalance: '300000000000000', // Not enough gas
+    });
+
+    await service.processPendingMessageApproved();
+
     expect(proxy.getAccount).toHaveBeenCalledTimes(1);
     expect(proxy.doPostGeneric).toHaveBeenCalledTimes(1);
     expect(proxy.sendTransactions).toHaveBeenCalledTimes(0);
@@ -354,7 +404,37 @@ describe('MessageApprovedProcessorService', () => {
     );
     expect(firstEntry).toEqual({
       ...originalFirstEntry,
-      retry: 2,
+      status: 'FAILED',
+      retry: 3,
+      updatedAt: expect.any(Date),
+    });
+  });
+
+  it('Should not send execute transaction if not enough gas negative', async () => {
+    const originalFirstEntry = await createMessageApproved({
+      retry: 1,
+      updatedAt: new Date(new Date().getTime() - 60_500),
+      availableGasBalance: '-300000000000000', // Not enough gas negative
+    });
+
+    await service.processPendingMessageApproved();
+
+    expect(proxy.getAccount).toHaveBeenCalledTimes(1);
+    expect(proxy.doPostGeneric).toHaveBeenCalledTimes(1);
+    expect(proxy.sendTransactions).toHaveBeenCalledTimes(0);
+
+    // No contract call approved pending remained for now
+    expect(await messageApprovedRepository.findPending()).toEqual([]);
+
+    // Expect entries in database updated
+    const firstEntry = await messageApprovedRepository.findBySourceChainAndMessageId(
+      originalFirstEntry.sourceChain,
+      originalFirstEntry.messageId,
+    );
+    expect(firstEntry).toEqual({
+      ...originalFirstEntry,
+      status: 'FAILED',
+      retry: 3,
       updatedAt: expect.any(Date),
     });
   });
@@ -366,12 +446,14 @@ describe('MessageApprovedProcessorService', () => {
       const originalItsExecuteOther = await createMessageApproved({
         contractAddress,
         payload: Buffer.from(AbiCoder.defaultAbiCoder().encode(['uint256'], [0]).substring(2), 'hex'),
+        availableGasBalance: '1200000000000000',
       });
       const originalItsExecute = await createMessageApproved({
         contractAddress,
         sourceChain: 'polygon',
         sourceAddress: 'otherSourceAddress',
         payload: Buffer.from(AbiCoder.defaultAbiCoder().encode(['uint256'], [1]).substring(2), 'hex'),
+        availableGasBalance: '1200000000000000',
       });
 
       mockProxySendTransactionsSuccess();
@@ -435,6 +517,7 @@ describe('MessageApprovedProcessorService', () => {
         sourceChain: 'polygon',
         sourceAddress: 'otherSourceAddress',
         payload: Buffer.from(AbiCoder.defaultAbiCoder().encode(['uint256'], [1]).substring(2), 'hex'),
+        availableGasBalance: '51200000000000000', // also contains 0.05 EGLD for ESDT issue
       });
 
       mockProxySendTransactionsSuccess();
@@ -578,5 +661,35 @@ describe('MessageApprovedProcessorService', () => {
         successTimes: 1,
       });
     });
+
+    it('Should send execute transaction deploy interchain token only deploy esdt not enough fee', async () => {
+      const originalItsExecute = await createMessageApproved({
+        contractAddress,
+        sourceChain: 'polygon',
+        sourceAddress: 'otherSourceAddress',
+        payload: Buffer.from(AbiCoder.defaultAbiCoder().encode(['uint256'], [1]).substring(2), 'hex'),
+        retry: 1,
+        executeTxHash: '67b2b814e2ec9bdd08f57073f575ec95d160c76ec9ccd4d14395e7824b6b77cc',
+        successTimes: 1,
+        availableGasBalance: '1200000000000000', // not enough fee for paying 0.05 EGLD for ESDT issue
+        updatedAt: new Date(new Date().getTime() - 60_500),
+      });
+
+      // Process transaction for ESDT issue only
+      await service.processPendingMessageApproved();
+
+      expect(proxy.sendTransactions).toHaveBeenCalledTimes(0);
+
+      const itsExecute = (await messageApprovedRepository.findBySourceChainAndMessageId(
+        originalItsExecute.sourceChain,
+        originalItsExecute.messageId,
+      )) as MessageApproved;
+      expect(itsExecute).toEqual({
+        ...originalItsExecute,
+        retry: 3,
+        status: 'FAILED',
+        updatedAt: expect.any(Date),
+      });
+    });
   });
 });
diff --git a/libs/common/src/config/api.config.service.ts b/libs/common/src/config/api.config.service.ts
index 3233278..8c7ad69 100644
--- a/libs/common/src/config/api.config.service.ts
+++ b/libs/common/src/config/api.config.service.ts
@@ -157,4 +157,8 @@ export class ApiConfigService {
   getGatewayTimeout(): number {
     return this.configService.get<number>('GATEWAY_TIMEOUT') ?? 30_000; // 30 seconds default
   }
+
+  isEnabledGasCheck(): boolean {
+    return this.configService.get<boolean>('ENABLE_GAS_CHECK') ?? false;
+  }
 }
diff --git a/libs/common/src/contracts/contracts.module.ts b/libs/common/src/contracts/contracts.module.ts
index 0ca4316..a7003bd 100644
--- a/libs/common/src/contracts/contracts.module.ts
+++ b/libs/common/src/contracts/contracts.module.ts
@@ -12,6 +12,7 @@ import { WegldSwapContract } from '@mvx-monorepo/common/contracts/wegld-swap.con
 import { ApiConfigService } from '@mvx-monorepo/common/config';
 import { DynamicModuleUtils } from '@mvx-monorepo/common/utils';
 import { ItsContract } from '@mvx-monorepo/common/contracts/its.contract';
+import { FeeHelper } from '@mvx-monorepo/common/contracts/fee.helper';
 
 @Module({
   imports: [DynamicModuleUtils.getCacheModule()],
@@ -107,6 +108,7 @@ import { ItsContract } from '@mvx-monorepo/common/contracts/its.contract';
       inject: [ApiConfigService, ResultsParser],
     },
     TransactionsHelper,
+    FeeHelper,
   ],
   exports: [
     GatewayContract,
@@ -117,6 +119,7 @@ import { ItsContract } from '@mvx-monorepo/common/contracts/its.contract';
     ProxyNetworkProvider,
     ApiNetworkProvider,
     TransactionsHelper,
+    FeeHelper,
   ],
 })
 export class ContractsModule {}
diff --git a/libs/common/src/contracts/entities/gas.error.ts b/libs/common/src/contracts/entities/gas.error.ts
index 7552593..8d1678c 100644
--- a/libs/common/src/contracts/entities/gas.error.ts
+++ b/libs/common/src/contracts/entities/gas.error.ts
@@ -1 +1,3 @@
 export class GasError extends Error {}
+
+export class NotEnoughGasError extends Error {}
diff --git a/libs/common/src/contracts/fee.helper.spec.ts b/libs/common/src/contracts/fee.helper.spec.ts
new file mode 100644
index 0000000..b61757d
--- /dev/null
+++ b/libs/common/src/contracts/fee.helper.spec.ts
@@ -0,0 +1,199 @@
+import { createMock, DeepMocked } from '@golevelup/ts-jest';
+import { Test } from '@nestjs/testing';
+import { TransactionPayload } from '@multiversx/sdk-core/out';
+import { ProxyNetworkProvider } from '@multiversx/sdk-network-providers/out';
+import { ApiConfigService } from '@mvx-monorepo/common';
+import { FeeHelper } from '@mvx-monorepo/common/contracts/fee.helper';
+import { NotEnoughGasError } from '@mvx-monorepo/common/contracts/entities/gas.error';
+
+describe('FeeHelper', () => {
+  let proxy: DeepMocked<ProxyNetworkProvider>;
+  let apiConfigService: DeepMocked<ApiConfigService>;
+
+  let feeHelper: FeeHelper;
+
+  beforeEach(async () => {
+    proxy = createMock();
+    apiConfigService = createMock();
+
+    const moduleRef = await Test.createTestingModule({
+      providers: [FeeHelper],
+    })
+      .useMocker((token) => {
+        if (token === ProxyNetworkProvider) {
+          return proxy;
+        }
+
+        if (token === ApiConfigService) {
+          return apiConfigService;
+        }
+
+        return null;
+      })
+      .compile();
+
+    proxy.getNetworkConfig.mockImplementation((): Promise<any> => {
+      return Promise.resolve({
+        MinGasPrice: 1000000000,
+        MinGasLimit: 50000,
+        GasPerDataByte: 1500,
+        GasPriceModifier: 0.01,
+      });
+    });
+    apiConfigService.isEnabledGasCheck.mockReturnValueOnce(true);
+
+    feeHelper = moduleRef.get(FeeHelper);
+  });
+
+  describe('checkGasCost', () => {
+    it('Enough gas fee', () => {
+      try {
+        // @ts-ignore
+        feeHelper.checkGasCost(10_000_000, 0, TransactionPayload.fromEncoded('test'), {
+          availableGasBalance: '300000000000000', // 0.00003 EGLD
+          sourceChain: 'ethereum',
+          messageId: 'messageId',
+        });
+
+        expect(true).toEqual(true);
+      } catch (e) {
+        expect(false).toEqual(true);
+      }
+    });
+
+    it('Not enough gas fee', () => {
+      try {
+        // @ts-ignore
+        feeHelper.checkGasCost(10_000_000, 0, TransactionPayload.fromEncoded('test'), {
+          availableGasBalance: '100000000000000', // 0.00001 EGLD
+          sourceChain: 'ethereum',
+          messageId: 'messageId',
+        });
+
+        expect(false).toEqual(true);
+      } catch (e) {
+        expect(e).toEqual(new NotEnoughGasError());
+      }
+
+      try {
+        // @ts-ignore
+        feeHelper.checkGasCost(10_000_000, 0, TransactionPayload.fromEncoded('test'), {
+          availableGasBalance: '-100000000000000', // negative value
+          sourceChain: 'ethereum',
+          messageId: 'messageId',
+        });
+
+        expect(false).toEqual(true);
+      } catch (e) {
+        expect(e).toEqual(new NotEnoughGasError());
+      }
+    });
+  });
+
+  it('getGasLimitFromEgldFee', () => {
+    expect(
+      feeHelper.getGasLimitFromEgldFee(
+        BigInt(608621610000000),
+        TransactionPayload.fromEncoded(
+          'c2V0UmVtb3RlVmFsdWVANjE3NjYxNmM2MTZlNjM2ODY1MmQ2Njc1NmE2OUAzMDc4NjM0NTM0MzEzMDMzMzgzNjM3NDM0MzM0NDI2NjYyMzIzMzM4MzI0NTM2NDQzMDQyMzc0NjM4Mzg2NTM2NDUzMzQ2Mzg0NDM1MzYzMzQ0MzZAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAyMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMTI0ODY1NmM2YzZmMjA3NzZmNzI2YzY0MjA2MTY3NjE2OTZlMjEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw',
+        ),
+      ),
+    ).toBe(BigInt(7712161));
+
+    expect(
+      feeHelper.getGasLimitFromEgldFee(
+        BigInt(726185000000000),
+        TransactionPayload.fromEncoded(
+          'c2V0UmVtb3RlVmFsdWVANjE3NjYxNmM2MTZlNjM2ODY1MmQ2Njc1NmE2OUAzMDc4NjM0NTM0MzEzMDMzMzgzNjM3NDM0MzM0NDI2NjYyMzIzMzM4MzI0NTM2NDQzMDQyMzc0NjM4Mzg2NTM2NDUzMzQ2Mzg0NDM1MzYzMzQ0MzZAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAyMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMTI0ODY1NmM2YzZmMjA3NzZmNzI2YzY0MjA2MTY3NjE2OTZlMjEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw',
+        ),
+      ),
+    ).toBe(BigInt(19468500));
+
+    expect(
+      feeHelper.getGasLimitFromEgldFee(
+        BigInt(508621610000000),
+        TransactionPayload.fromEncoded(
+          'c2V0UmVtb3RlVmFsdWVANjE3NjYxNmM2MTZlNjM2ODY1MmQ2Njc1NmE2OUAzMDc4NjM0NTM0MzEzMDMzMzgzNjM3NDM0MzM0NDI2NjYyMzIzMzM4MzI0NTM2NDQzMDQyMzc0NjM4Mzg2NTM2NDUzMzQ2Mzg0NDM1MzYzMzQ0MzZAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAyMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMTI0ODY1NmM2YzZmMjA3NzZmNzI2YzY0MjA2MTY3NjE2OTZlMjEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw',
+        ),
+      ),
+    ).toBe(BigInt(531500));
+
+    expect(
+      feeHelper.getGasLimitFromEgldFee(
+        BigInt(588621610000000),
+        TransactionPayload.fromEncoded(
+          'c2V0UmVtb3RlVmFsdWVANjE3NjYxNmM2MTZlNjM2ODY1MmQ2Njc1NmE2OUAzMDc4NjM0NTM0MzEzMDMzMzgzNjM3NDM0MzM0NDI2NjYyMzIzMzM4MzI0NTM2NDQzMDQyMzc0NjM4Mzg2NTM2NDUzMzQ2Mzg0NDM1MzYzMzQ0MzZAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAyMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMTI0ODY1NmM2YzZmMjA3NzZmNzI2YzY0MjA2MTY3NjE2OTZlMjEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw',
+        ),
+      ),
+    ).toBe(BigInt(5712161));
+
+    // Larger data
+    expect(
+      feeHelper.getGasLimitFromEgldFee(
+        BigInt(588621610000000),
+        TransactionPayload.fromEncoded(
+          'c2V0UmVtb3RlVmFsdWVANjE3NjYxNmM2MTZlNjM2ODY1MmQ2Njc1NmE2OUAzMDc4NjM0NTM0MzEzMDMzMzgzNjM3NDM0MzM0NDI2NjYyMzIzMzM4MzI0NTM2NDQzMDQyMzc0NjM4Mzg2NTM2NDUzMzQ2Mzg0NDM1MzYzMzQ0MzZAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAyMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMTI0ODY1NmM2YzZmMjA3NzZmNzI2YzY0MjA2MTY3NjE2OTZlMjEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA',
+        ),
+      ),
+    ).toBe(BigInt(4512161));
+
+    // Smaller data
+    expect(
+      feeHelper.getGasLimitFromEgldFee(
+        BigInt(588621610000000),
+        TransactionPayload.fromEncoded(
+          'c2V0UmVtb3RlVmFsdWU=',
+        ),
+      ),
+    ).toBe(BigInt(51762161));
+
+    expect(
+      feeHelper.getGasLimitFromEgldFee(
+        BigInt(-1000),
+        TransactionPayload.fromEncoded(
+          'c2V0UmVtb3RlVmFsdWU=',
+        ),
+      ),
+    ).toBe(BigInt(71000));
+  });
+
+  it('getEgldFeeFromGasLimit', () => {
+    expect(
+      feeHelper.getEgldFeeFromGasLimit(
+        BigInt(8243661),
+        BigInt(321)
+      ),
+    ).toBe(BigInt(613936610000000));
+
+    expect(
+      feeHelper.getEgldFeeFromGasLimit(
+        BigInt(6000000),
+        BigInt(321)
+      ),
+    ).toBe(BigInt(591500000000000));
+
+
+    expect(
+      feeHelper.getEgldFeeFromGasLimit(
+        BigInt(20000000),
+        BigInt(321)
+      ),
+    ).toBe(BigInt(731500000000000));
+
+    // Larger data
+    expect(
+      feeHelper.getEgldFeeFromGasLimit(
+        BigInt(20000000),
+        BigInt(350)
+      ),
+    ).toBe(BigInt(775000000000000));
+
+    // Smaller data
+    expect(
+      feeHelper.getEgldFeeFromGasLimit(
+        BigInt(20000000),
+        BigInt(300)
+      ),
+    ).toBe(BigInt(700000000000000));
+  });
+});
diff --git a/libs/common/src/contracts/fee.helper.ts b/libs/common/src/contracts/fee.helper.ts
new file mode 100644
index 0000000..4b54864
--- /dev/null
+++ b/libs/common/src/contracts/fee.helper.ts
@@ -0,0 +1,82 @@
+import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
+import { ProxyNetworkProvider } from '@multiversx/sdk-network-providers/out';
+import { ApiConfigService } from '@mvx-monorepo/common';
+import { ITransactionPayload, ITransactionValue } from '@multiversx/sdk-core/out';
+import { MessageApproved } from '@prisma/client';
+import { NotEnoughGasError } from '@mvx-monorepo/common/contracts/entities/gas.error';
+
+@Injectable()
+export class FeeHelper implements OnModuleInit {
+  private readonly logger: Logger;
+
+  private readonly isEnabledGasCheck: boolean;
+  private minGasPrice: bigint = BigInt(1000000000);
+  private minGasLimit: bigint = BigInt(50000);
+  private gasPerDataByte: bigint = BigInt(1500);
+  private gasPriceModifierInverted: bigint = BigInt(100);
+
+  constructor(
+    private readonly proxy: ProxyNetworkProvider,
+    apiConfigService: ApiConfigService,
+  ) {
+    this.logger = new Logger(FeeHelper.name);
+
+    this.isEnabledGasCheck = apiConfigService.isEnabledGasCheck();
+  }
+
+  async onModuleInit() {
+    const config = await this.proxy.getNetworkConfig();
+
+    this.minGasPrice = BigInt(config.MinGasPrice);
+    this.minGasLimit = BigInt(config.MinGasLimit);
+    this.gasPerDataByte = BigInt(config.GasPerDataByte);
+    this.gasPriceModifierInverted = BigInt(10_000 * config.GasPriceModifier);
+  }
+
+  public checkGasCost(
+    gas: number,
+    value: ITransactionValue,
+    data: ITransactionPayload,
+    messageApproved: MessageApproved,
+  ) {
+    const gasFee = this.getEgldFeeFromGasLimit(BigInt(gas), BigInt(data.length()));
+    const egldValue = BigInt(value.toString());
+    const total = gasFee + egldValue;
+
+    // Also take into account value in case of ITS
+    if (total <= BigInt(messageApproved.availableGasBalance)) {
+      return;
+    }
+
+    if (!this.isEnabledGasCheck) {
+      this.logger.warn(
+        `[GAS CHECK NOT ENABLED] Not enough gas to execute transaction ${messageApproved.sourceChain} ${messageApproved.messageId} BUT it will be executed anyway. Needed ${total} EGLD but only have ${messageApproved.availableGasBalance} EGLD`,
+      );
+
+      return;
+    }
+
+    this.logger.warn(
+      `[GAS CHECK ENABLED] Not enough gas to execute transaction ${messageApproved.sourceChain} ${messageApproved.messageId}. Needed ${total} EGLD but only have ${messageApproved.availableGasBalance} EGLD`,
+    );
+
+    throw new NotEnoughGasError();
+  }
+
+  public getGasLimitFromEgldFee(availableGasBalance: bigint, data: ITransactionPayload): bigint {
+    const gasLimit1 = this.minGasLimit + this.gasPerDataByte * BigInt(data.length());
+
+    // Use data gas limit in this case
+    if (availableGasBalance < gasLimit1 * this.minGasPrice) {
+      return gasLimit1;
+    }
+
+    return ((availableGasBalance - gasLimit1 * this.minGasPrice) * this.gasPriceModifierInverted) / this.minGasPrice;
+  }
+
+  public getEgldFeeFromGasLimit(gasLimit2: bigint, dataLength: bigint): bigint {
+    const gasLimit1 = this.minGasLimit + this.gasPerDataByte * dataLength;
+
+    return gasLimit1 * this.minGasPrice + (gasLimit2 * this.minGasPrice) / this.gasPriceModifierInverted;
+  }
+}
diff --git a/libs/common/src/database/repository/message-approved.repository.ts b/libs/common/src/database/repository/message-approved.repository.ts
index 60d8396..53111a9 100644
--- a/libs/common/src/database/repository/message-approved.repository.ts
+++ b/libs/common/src/database/repository/message-approved.repository.ts
@@ -6,21 +6,18 @@ import { MessageApproved, MessageApprovedStatus, Prisma } from '@prisma/client';
 export class MessageApprovedRepository {
   constructor(private readonly prisma: PrismaService) {}
 
-  async create(data: Prisma.MessageApprovedCreateInput): Promise<MessageApproved | null> {
-    try {
-      return await this.prisma.messageApproved.create({
-        data,
-      });
-    } catch (e) {
-      if (e instanceof Prisma.PrismaClientKnownRequestError) {
-        // Unique constraint fails
-        if (e.code === 'P2002') {
-          return null;
-        }
-      }
-
-      throw e;
-    }
+  async createOrUpdate(data: Prisma.MessageApprovedCreateInput) {
+    await this.prisma.messageApproved.upsert({
+      where: {
+        sourceChain_messageId: {
+          sourceChain: data.sourceChain,
+          messageId: data.messageId,
+        },
+      },
+      update: data,
+      create: data,
+      select: null,
+    });
   }
 
   findPending(page: number = 0, take: number = 10): Promise<MessageApproved[] | null> {
diff --git a/prisma/migrations/20241118081638_add_available_gas_balance/migration.sql b/prisma/migrations/20241118081638_add_available_gas_balance/migration.sql
new file mode 100644
index 0000000..5a830cd
--- /dev/null
+++ b/prisma/migrations/20241118081638_add_available_gas_balance/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "MessageApproved" ADD COLUMN     "availableGasBalance" VARCHAR(255) NOT NULL DEFAULT '0';
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 4f55510..fb2e42b 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -11,19 +11,20 @@ datasource db {
 }
 
 model MessageApproved {
-  sourceChain     String                @db.VarChar(255)
-  messageId       String                @db.VarChar(255)
-  status          MessageApprovedStatus
-  sourceAddress   String                @db.VarChar(255)
-  contractAddress String                @db.VarChar(62)
-  payloadHash     String                @db.VarChar(64)
-  payload         Bytes
-  executeTxHash   String?               @db.VarChar(64)
-  retry           Int                   @db.SmallInt
-  createdAt       DateTime              @default(now()) @db.Timestamp(6)
-  updatedAt       DateTime              @default(now()) @updatedAt @db.Timestamp(6)
-  successTimes    Int?                  @db.SmallInt
-  taskItemId      String?               @db.Uuid
+  sourceChain         String                @db.VarChar(255)
+  messageId           String                @db.VarChar(255)
+  status              MessageApprovedStatus
+  sourceAddress       String                @db.VarChar(255)
+  contractAddress     String                @db.VarChar(62)
+  payloadHash         String                @db.VarChar(64)
+  payload             Bytes
+  executeTxHash       String?               @db.VarChar(64)
+  retry               Int                   @db.SmallInt
+  createdAt           DateTime              @default(now()) @db.Timestamp(6)
+  updatedAt           DateTime              @default(now()) @updatedAt @db.Timestamp(6)
+  successTimes        Int?                  @db.SmallInt
+  taskItemId          String?               @db.Uuid
+  availableGasBalance String                @default("0") @db.VarChar(255)
 
   @@id([sourceChain, messageId])
 }