0

Based on this simulation, it seems like the transaction was reverted because of gas limit.

This is the piece of code that was trying to execute:

const service = new SafeApiKit({ chainId: BigInt(chainId) });
const safeSdk = await Safe.create({ ethAdapter, safeAddress });
const safeTransaction: SafeMultisigTransactionResponse =
  await service.getTransaction(safeTxHash);
const isValidTx = await safeSdk
  .isValidTransaction(safeTransaction)
  .catch(() => false);
if (isValidTx) {
  const txResponse = await safeSdk.executeTransaction(safeTransaction);
  await txResponse.transactionResponse?.wait();
  return { hash: txResponse.hash, error: null };
}

return {
  hash: null,
  error: new Error('Invalid transaction. Please check your Safe wallet'),
};

Maybe the transaction would have been valid if I could set a gasLimit on the safe transaction?

Further context: Safe transaction hash: 0x84e066388c54661c56a5429d3b78d1f1a1820762ff1c3c5a020587178c4c0c00

https://app.safe.global/transactions/history?safe=eth:0x2a69F406384DCEee59480010037d4499136Dd1bf https://etherscan.io/tx/0x170eb706ce48570b036509007f66a8781bc84860d1f89e04be71f2ebc1ac86d2#internal

Notice the Safe Multisend 1.1.1 fail.

Tried to mimic the 1.1.1 Safe transactions in a newly created safe and same transactions type that failed above successfully passed: https://app.safe.global/transactions/history?safe=eth:0xC8B8eCD2255ff75707e69E1dB08DD01C4024b06B

Trying to understand why the transactions could have failed in the eth:0x2a69F406384DCEee59480010037d4499136Dd1bf Safe

Here's side by side of 2 transactions

enter image description here

Looking at the tests for isValidTransaction it doesn't look like there is anything about order: https://github.com/safe-global/safe-core-sdk/blob/d6e2fe955c75fad3a812d5537afbfe082aa840af/packages/protocol-kit/tests/e2e/execution.test.ts#L34

Code to create batch transaction

/ Processes a batch of transactions
  const batchTransaction = async (
    bills: Bill[],
    chainId: number,
    safeAddress: string
  ): Promise<TransactionResult> => {
    try {
      const ethAdapter = await createEthAdapter();
      if (!ethAdapter || !signer) return { hash: null, error: null };

      const service = new SafeApiKit({ chainId: BigInt(chainId) });
      const safeSdk = await Safe.create({ ethAdapter, safeAddress });
      const nextNonce = await service.getNextNonce(safeAddress);

      const transactions = await Promise.all(
        bills.map(async bill => {
          // Get token price data for the bill
          const [data] = await getTokenPriceAPI(bill.tokenId?.toString() || '');
          if (!data)
            return {
              to: getAddress(bill.paidToAddress),
              value: '0',
              data: '0x',
              operation: OperationType.Call,
            };

          // Calculate transaction amount based on token data
          const amountToParse = (Number(bill.amount) / data.price).toFixed(6);
          const amount = bill.networkTokenId
            ? parseUnits(amountToParse, data.decimals)
            : parseEther(amountToParse);

          // Create ERC20 transfer transaction
          if (bill.networkTokenId) {
            // Construct ERC20 transfer or default transaction
            const transactionData = bill.networkTokenId
              ? new Contract(
                  bill.networkTokenId,
                  erc20ABI,
                  signer
                ).interface.encodeFunctionData('transfer', [
                  bill.paidToAddress,
                  amount,
                ])
              : '0x';
            return {
              to: getAddress(bill.networkTokenId),
              value: '0',
              data: transactionData,
              operation: OperationType.Call,
            };
          }

          // Default transaction
          return {
            to: getAddress(bill.paidToAddress),
            value: amount.toString(),
            data: '0x',
            operation: OperationType.Call,
          };
        })
      );

      const safeTransaction = await safeSdk.createTransaction({
        transactions,
        options: { nonce: nextNonce },
      });
      const safeTxHash = await safeSdk.getTransactionHash(safeTransaction);
      const signature = await safeSdk.signTransactionHash(safeTxHash);

      // Propose the batch transaction
      await service.proposeTransaction({
        safeAddress: await safeSdk.getAddress(),
        safeTransactionData: safeTransaction.data,
        safeTxHash,
        senderAddress: await signer.getAddress(),
        senderSignature: signature.data,
      });

      return { hash: safeTxHash, error: null };
    } catch (err) {
      return {
        hash: null,
        error: err instanceof Error ? err : new Error('Unknown error'),
      };
    }
  };

Code to execute transaction

  const executeTransaction = async (
    safeTxHash: string,
    safeAddress: string,
    chainId: string
  ): Promise<TransactionResult> => {
    try {
      const ethAdapter = await createEthAdapter();

      if (!ethAdapter || !signer)
        return {
          hash: null,
          error: new Error('Ethereum adapter or signer is missing'),
        };

      const service = new SafeApiKit({ chainId: BigInt(chainId) });
      const safeSdk = await Safe.create({ ethAdapter, safeAddress });
      const safeTransaction: SafeMultisigTransactionResponse =
        await service.getTransaction(safeTxHash);
      const isValidTx = await safeSdk
        .isValidTransaction(safeTransaction)
        .catch(() => false);
      if (isValidTx) {
        const txResponse = await safeSdk.executeTransaction(safeTransaction);
        await txResponse.transactionResponse?.wait();
        return { hash: txResponse.hash, error: null };
      }

      return {
        hash: null,
        error: new Error('Invalid transaction. Please check your Safe wallet'),
      };
    } catch (err) {
      return {
        hash: null,
        error: err instanceof Error ? err : new Error('Unknown error'),
      };
    }
  };

2 Answers 2

1

After some investigations we were able to go to the bottom of it. It seems that the gas limit was actually too low for all the transactions. Please retry this transaction by adding more gas, as gas refunds may have made the transaction more expensive, before returning some gas.

In the future, you can also make sure that all the transaction in your safe queue are executed in the right order, as getNextNonce will return a number for all the transactions in the queue. If that nonce is executed before other transactions in the queue, this will fail.

Hope it helps; Best, Louis

3
  • We execute our transactions in the right order. Can you provide some code as to how to determine the gasLimit that the transaction will be executed with and how to modify it. Can we do safeTxGasLimit * 1.5 or something? what are the best practices to set custom gas rules since the default one with Safe's gasLimit failed. Commented Mar 29 at 15:25
  • Some more findings: docs.safe.global/sdk/api-kit/reference#estimatesafetransaction - this returned "safeTxGas": "0" on Optimism for a transaction. - Is there limitations on estimating gas? How would I set the gasLimit if i dont even know what the estimate is? The estimate on Sepolia was 76667 but the transaction ended up being closer to ~100k. Commented Apr 4 at 15:54
  • This should not return 0. If you believe this is a bug, I'd suggest opening an issue in github.com/safe-global/safe-transaction-service. For estimating a gas limit, you can indeed apply a multiplier as you suggested, since the transaction won't consume more gas than it actually needs and refund the leftover gas to the user. Commented Apr 15 at 13:44
0

This transaction was malformed. The order of the transactions in the batch was different from other successful executions, so this could cause an invalid transaction. The gas was estimated correctly.

5
  • Thanks Tanay, can you provide more context as to what was wrong with the order since we haven’t done anything different for this transaction for our user and we wanna make sure this doesn’t happen again. Thanks! Commented Mar 14 at 12:46
  • 1
    Without knowing how the original transaction (from the safeTxHash) was created, it will be difficult for us to understand where the problem comes from. Could you please edit the question to give more context on that? Thanks 🙏 Commented Mar 14 at 15:46
  • @Louis-SafeCoreTeam updated, thank you! Commented Mar 14 at 16:36
  • Would you be able to add the code you used to create the original transaction? It will help us understand where the problem comes from. Thank you! Commented Mar 20 at 10:10
  • @Louis-SafeCoreTeam updated with the code to create a batch transaction and execute transaction Commented Mar 20 at 15:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.