Successful transaction marked as failed

Here’s a transfer ETH transaction, which has a failed status, but resulted in actual asset movements: Fuel Explorer.

  1. Balance ETH before: more than 0.007
  2. Failed transaction (trying to transfer out 0.0062 ETH)
  3. Balance ETH after: 0.000883689
  4. Receiver’s balance increased by 0.0062 ETH

Is it a bug or am I missing something?

Hey @mpoplavkov let me have a look at this and get back to you, was this a normal wallet to wallet transfer?

Hey, yes, kind of. I was playing with ts sdk and it was a normal transfer but without gasLimit specified

Hello @mpoplavkov, how are you?

It seems this transaction failed due OutOfGas. Have you set the gasLimit to 0 before submitting the transaction?

When a transaction begins processing and reverts, its funded inputs will still be spent to pay for the gas used up to the point of failure. As a result, the transaction will still generate new UTXOs for the CoinOutput and CoinChange entries within its outputs.

This means that even if UTXOs are spent from the sender and generated for the receiver (i.e., the sender’s balance decreases, and the receiver’s balance increases), it doesn’t necessarily mean that the transaction succeeded.

I haven’t set any gas limit. Probably, it’s set to 0 as default in ts sdk.

Hm, that’s interesting, didn’t think about these utxo specifics.

But

  1. The receiver shouldn’t have had a CoinChange pointing to his address (the transaction is a normal transfer, so the receiver should have just a CoinOutput and the sender should have the CoinChange)
  2. Even if the CoinChange would’ve pointed to the receiver’s address, this behavior is unexpected I’d say. Doesn’t it make sense in case of failures to charge the CoinInput owner for the fees, but return the remaining funds to his account?

What if an address has just one UTXO with a large amount of ETH and this situation happens? Will he lose all the ETH even if he was trying to send a small fraction of it?

1 Like

Hey @mpoplavkov, could you share the initial code that triggered this error?

The TS SDK automatically estimates and sets an optimized gasLimit when submitting a transfer using account.transfer. However, when creating a transaction request manually, it is 0:

// `gasLimit` is not set and defaults to `0`
const request = new ScriptTransactionRequest();

1 - The receiver does not have a ChangeOutput pointing to their address. Instead, there is a CoinOutput with a specified asset ID and amount directed to their address. This creates a new UTXO for the receiver with the desired amount. The ChangeOutput points to the resource owner’s address (the sender), and its amount is determined during transaction processing as follows:

${amount available in inputs} - ( ${total amount in `CoinOutput`(s)} + ${total amount to pay in fee})

2 - Since the ChangeOutput is pointing to the sender’s address, the sender will not lose their resources as a new UTXO will be created for the sender’s address even if the transaction reverts during processing time, with the correct change amount. The TS SDK adds a ChangeOutput for the sender on the fly when adding resources to the transaction request:

const request = new ScriptTransactionRequest();

// will add `ChangeOutput`(s) pointing to the resources owner address, for every different asset ID present within their resources/UTXOs.
request.addResouces(fetchedResources);

The decision to charge and spend inputs for transactions that revert after processing begins was made to prevent users from spamming the network with invalid transactions without incurring any fees.

It is also important to note that these inputs are only spent if the transaction reverts after processing has started. If the transaction is not accepted by the network after submission, its inputs will remain unspent.

1 Like

Hey, here’s the code:

    let request = new ScriptTransactionRequest();
    request = wallet.addTransfer(request, {destination: destinationB256Address, amount: fractionalAmount, assetId});
    const txCost = await wallet.getTransactionCost(request);
    request = await wallet.fund(request, txCost);

I get all what you’re saying and I understand why you charge for gas even if transactions fail. But nothing of that explains how the funds got transferred to the destination account in this example

@mpoplavkov I am deeply sorry if my explanations weren’t clear enough

About your code:

To avoid the transaction reverting, we need to specifically set the transaction’s gasLimit and the maxFee:

  let request = new ScriptTransactionRequest();

    // Adds a `CoinOutput` in favor of the receiver to the TX outputs 
    request = wallet.addTransfer(request, {destination: destinationB256Address, amount: fractionalAmount, assetId});

    const txCost = await wallet.getTransactionCost(request);
    request.gasLimit = txCost.gasUsed;
    request.maxFee = txCost.maxFee;

    // Fetches and add resources (ChageOutput is added for the resources owner)
    request = await wallet.fund(request, txCost);

Why did the funds get transferred?

  • Gas Limit Not Set: The transaction’s gasLimit was not set, so it defaulted to 0. The network accepts the transaction but it reverts due to an OutOfGas error during execution.
  • Spent Inputs: After the transaction reverts, the Fuel VM still spends the transaction inputs for the reason of preventing users from spamming the network with invalid transactions without incurring any fees.
  • Fee Deduction: The VM deducts the fee for its use up to the point of failure from the total amount of the spent inputs.
  • UTXO Generation for CoinOutput: The VM generates new UTXOs for every CoinOutput specified in the transaction. This is why the recipient received their funds despite the transaction reverting. So, since resources were spent, the VM will also generate new ones specified by every CoinOutput.
  • Change UTXO Creation: After generating UTXOs for each CoinOutput, the VM creates a new UTXO with the remaining amount (change) for the address specified in the ChangeOutput of that asset ID. This step ensures that the entity funding the transaction doesn’t lose all their resources. This is why the amount of the ChangeOutput is determined only during execution time.

Please let me know if this clarifies your question.

I don’t understand why in the step UTXO Generation for CoinOutput the funds get transferred to the recepient given that the transaction reverted. That’s not how it should work in my opinion. Who cares if there was a recepient in the transaction if the transaction failed? What should have happened: the sender should have been charged for transaction fees and that’s it

Reverts don’t stop CoinOutputs, those still go through. You would need to use a CoinVariable output where the variable output value is set to zero under revert. I believe if you don’t want things to be sent with reverting.

but please dually confirm to ensure that may/may not be it here.

Should we use CoinVariable for normal asset transfers in Fuelet then? It’s not intuitive for assets to get transferred in case of reverts

Yes @mpoplavkov , only OutputVariable and OutputChange are affected by runtime reverts.

OutputCoin would only be affected by predicate conditions, which would ensure the transaction is not included in a block.

Use the OutputVariable and ensure the amount is set correctly in execution, then it will be reverted if a revert occurs.

CoinOutputs are deterministic, and a script has no control over them.

This is because Fuel supports many kinds of transactions and use-cases that require completely deterministic and infallible behaviors. Some examples include: simple primitive native asset transfers, meta-transactions, or contract deployments.

Here is an example sponsored meta-transaction to illustrate why this medley of both deterministic and malleable behavior is needed and useful:

Alice: has USDC but no ethereum to pay for gas and wants to make a transaction
Bob: willing to sponsor gas fees in exchange for USDC

Bob and Alice cooperate on a coinjoin-style transaction that looks like this:

  • Inputs
    • InputCoin - Alice: $20 USDC
    • InputCoin - Bob: 1 ETH
  • Outputs
    • CoinOutput - Bob: $0.20 USDC (this is the amount of USDC Alice pays to Bob)
    • CoinOuptut - Bob: 0.99992 ETH (this is Bobs change of 1 ETH - $0.20 USDC)
    • CoinOutput - Alice: $19.80 USDC (this is the amount of USDC Alice has leftover)
    • ChangeOutput - Alice: Eth (change to capture any leftover gas from script execution)

In the above transaction, Alice gives Bob 20 cents in USDC to acquire enough ETH to pay for her transaction fees. This part of the transaction is completely deterministic, and it becomes obvious why it must be regardless of the outcome of her script which could revert.

Let’s say that Alice’s script in this transaction consumes a large amount of gas and then performs a coin-flip at the end to decide whether it will revert. The block builder still needs to process all that gas to determine whether the transaction reverts or not. If the coin outputs were reverted:

  • All of Bob’s ETH would go to Alice’s ETH change output, causing Bob to not get paid and lose all his ETH
  • Alice would burn her $19.80 since the $20 input must be consumed

If we reverted the whole transaction, including the input spending, then the block builder would get spammed by failing transactions and not collect any fees for processing a lot of gas.

It sounds like the confusion here is due to how UTXOs and gas are handled in the case of a failed transaction. Even though the transaction fails due to an OutOfGas error, the UTXOs are still spent to cover the gas fees, which can lead to the receiver receiving the funds. However, since the transaction fails, the funds should ideally be returned to the sender’s address as change. It would be helpful to clarify how these edge cases are handled in the SDK and ensure that the change is correctly applied when transactions fail.