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
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)
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?
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.
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.
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.