Hello everyone,
I’m researching Polkadot’s transaction graph and investigating an unusual pattern of successful self-transfers involving astronomically large requested amounts.
I would appreciate some clarification from people familiar with the historical Balances pallet implementation and Polkadot runtime upgrades.
1. What I observed
I identified successful Balances.transfer extrinsics where the sender and recipient were the same account, but the requested transfer amount was far beyond any possible account balance.
Two examples from my dataset:
Example 1 — May 2, 2021
-
Block:
4880515 -
Extrinsic:
4880515-2 -
Runtime
spec_version:30 -
Transaction version:
7 -
Requested amount: approximately 12.65 trillion DOT
-
Sender and recipient: identical
-
Extrinsic result:
System.ExtrinsicSuccess -
No
Balances.Transferevent was emitted.
Example 2 — July 5, 2022
-
Block:
11026623 -
Runtime
spec_version:9230 -
Transaction version:
12 -
Requested amount: approximately 16.395 trillion DOT
-
Sender and recipient: identical
-
Extrinsic result: successful.
These are not isolated observations. My dataset contains 140 suspicious self-loop edges, including multiple astronomically large requested amounts.
2. What I found in the historical source code
In the older Balances implementation, the transfer function contained the following condition:
if value.is_zero() || transactor == dest {
return Ok(())
}
This check occurs before the subsequent balance validation.
Consequently, when the sender and destination are identical, the function returns successfully without validating whether the requested amount can actually be transferred.
I checked the source corresponding to both historical runtime versions:
Both implementations contain this early self-transfer return.
3. A potentially significant change in the later implementation
I also investigated the subsequent Balances pallet refactoring.
Substrate PR #12951, merged in March 2023, introduced substantial changes to the Currency and fungible interfaces.
In the newer fungible transfer implementation, the execution order is different:
-
Check withdrawal preconditions.
-
Check deposit preconditions.
-
Check whether source and destination are identical.
-
Return without performing the actual balance mutations if they are identical.
The newer implementation therefore appears to validate the requested amount before treating a self-transfer as a no-op.
Relevant source:
I have not yet established the exact first Polkadot runtime version in which this changed behaviour became active.
4. My experiment on Westend
I also tested this behaviour on a modern Westend runtime:
-
A small self-transfer succeeded without emitting a
Balances.Transferevent. -
An oversized self-transfer failed with an arithmetic error.
This is consistent with the newer implementation validating the requested amount before reaching the same-account no-op.
I will attach screenshots of both experiments.
5. Questions for the maintainers
I would particularly appreciate clarification on the following:
-
Was the old behaviour intentional, where
transactor == destreturned successfully before any balance validation, regardless of the requested amount? -
Was the change introduced through PR #12951, or was there an earlier or later change specifically responsible for moving balance validation before the self-transfer check?
-
What was the first Polkadot runtime
spec_versionto adopt this new behaviour? -
At which block was that runtime upgrade activated on Polkadot?
-
Is the arithmetic error observed in my oversized Westend self-transfer experiment the expected consequence of the newer validation order?
I am particularly interested in identifying the exact runtime upgrade that changed this behaviour, rather than relying only on Substrate release dates.
6. Why this matters
My research involves constructing and analysing a transaction graph containing millions of edges.
If successful self-transfer calls are interpreted as actual value transfers, these no-op calls can introduce false edges and distort transaction counts, transfer volumes, account activity, and graph metrics.
Identifying the historical implementation and its evolution is therefore important for correctly interpreting Polkadot’s transaction history and producing reliable research results.
Thank you for any clarification or pointers to the relevant commits, runtime upgrades, or historical implementation details.