ton-org / sandbox

Local TON emulator
MIT License
187 stars 33 forks source link

Making sense of total fees #55

Open 9oelM opened 3 weeks ago

9oelM commented 3 weeks ago

I have a question about tx.totalFees.coins. I genuinely cannot understand what this value is supposed to represent. In my case, I want to write a test on how much gas is spent for a series of transactions.

I've provided an example at https://github.com/9oelM/ton-sandbox-totalfees-bug.git. The test setup is simple:

  1. User wallet sends tx to ContractA (msg mode is 0)
  2. ContractA sends tx to ContractB (msg mode is 64)

In code, it is as simple as:

it('fees test', async () => {
        const increaser = await blockchain.treasury('increaser');
        const contractBTONBalanceBefore = await contractB.getBalance();

        const increaseResult = await contractA.sendIncrease(increaser.getSender(), {
            increaseBy: 1,
            value: toNano('0.05'),
        });

        expect(increaseResult.transactions).toHaveTransaction({
            from: increaser.address,
            to: contractA.address,
            success: true,
        });
        expect(increaseResult.transactions).toHaveTransaction({
            from: contractA.address,
            to: contractB.address,
            success: true,
        });

        printTransactionFees(increaseResult.transactions);

        const allFees = increaseResult.transactions.reduce((acc, tx) => {
            return acc + tx.totalFees.coins;
        }, 0n)

        const contractBTONBalanceAfter = await contractB.getBalance();
        console.log('contractB TON inflow', contractBTONBalanceAfter - contractBTONBalanceBefore);

        const lastTx = flattenTransaction(increaseResult.transactions[increaseResult.transactions.length - 1]);
        expect(lastTx.value! - lastTx.totalFees!).toBe(
            toNano('0.05') - allFees
        );
    });

printTransactionFees(increaseResult.transactions); prints: Screenshot 2024-10-03 at 10 08 58

But I couldn't wrap my head around these numbers. I thought totalFees is supposed to be the sum of all total fees for a tx. In other words, $valueIn - valueOut = totalFees$. But this simply isn't the case:

  1. For index = 0, valueIn = 0.05 TON (we started with 0.05 TON) and valueOut = 0.0496 TON. valueIn - valueOut = 0.0004. But totalFees = 0.001613???
  2. Same for index = 1. valueIn = 0.0496 TON and valueOut = 0.046978 TON. valueIn - valueOut = 0.002622. But totalFees = 0.002296???

If totalFees were correctly calculated as total gas for the tx, toNano('0.05') - allFees would simply match the TON balance transferred to a contract, which is contractBTONBalanceAfter - contractBTONBalanceBefore. But this is not the case. In fact, lastTx.value - lastTx.totalFees == contractBTONBalanceAfter - contractBTONBalanceBefore != toNano('0.05') - allFees:

cc @krigga

Trinketer22 commented 3 weeks ago

Transaction has multiple phases (storage, compute, action). Gas is compute phase only. totalFees is sum of fees of all phases in the specified transaction. (not series of transactions).

9oelM commented 3 weeks ago

@Trinketer22 thanks for the reply. Then could you break the fees in the first transaction down for me?: image How are they supposed to add up or make sense mathematically?

Trinketer22 commented 3 weeks ago

Well, the totalFees should be compute+action_fees+storage+external import(in case of transaction triggered by external message). If we're talking tx with index 0, then the import fee is there, because it's external triggered. As you can see, storage+import are not part of the table and action fees is in fact lower.

In order to get to the bottom of what is happening, you can print the blockchainLogs of this transaction. Because for whatever reason, the import fee is not part of transaction description.

In case of transactions, triggered by external message, the fees are deducted from contract balance, so (valueIn - valueOut) is not valid logic here.

There is also a gotcha in the fact that totalFees represent the amount of fees collected at this point. But the tricky part is that the forward fee is collected in two parts in/out. One part for the sending shard and the other for the receiving shard. the fwdOut goes to the action fees and accounted in totalFees on the sender part, while inFwd part is accounted for totalFees on receiver. I think it would be better to display action fees separately.

What printTransaction displays as outForwardFee is in fact the totalForwardFee, and that's likely a bug.

9oelM commented 3 weeks ago

@Trinketer22 wow thank you so much for the explanation. Now I kinda get the hang of it. Appreciate the help.

Trinketer22 commented 3 weeks ago

@9oelM, you can also go through this attempt to explain fees in detail, if you haven't already.

Bare in mind that there is slight naming confusion when reading about fwd_fees. fwd_in/fwd_out vs fwd_mine/fwd_remain. What printTransaction atempts to show as fwd_in/out is mine/remain and fwd_in supposed to be used for the external import fee.