Diagnosing Token Transfer Failures on Neo N3

Joe Stewart
5 min readOct 31, 2021

--

As a Neo dApp developer, I’ve spent a lot of time diagnosing and debugging transaction failures on the Neo blockchain over the years. Armed with this special set of skills, I’ve provided countless hours of support to other dApp developers and end-users when they find their way to the Neo community Discord server. Some problems are harder than others to solve — blockchains are fairly opaque to most people and block explorers rarely provide a simple breakdown of anything other than ordinary token transfers. But armed with some basic knowledge, nearly anyone can learn to read a Neo transaction log and make sense of it.

A week ago, a user found his way into the Neo Discord #support channel asking for help with an exchange withdrawal. The user had made two withdrawals from gate.io, one for 1,266.85 GAS, and another for 1,347.25 GAS. The transfer for 1,266.85 reached the destination wallet, but the transfer for 1,347.25 GAS did not. The gate.io support staff told the user that the transaction was complete and the funds were sent, but the user could see that was clearly not the case, so he came to the Discord for help. I took a look at both transactions on the Dora block explorer. Both transactions were sent in the same block, with the same parameters except for the amount of GAS sent, but they appear very differently when viewed.

The first transaction in the block, 0x6bc9509d88556688089a2c49151fb31cbbbb64a889e827479bd07b6a013baf6b appears to have been sent successfully. Dora clearly shows a transfer in the amount of 1,266.85 GAS.

A successful N3 GAS transfer

However, the second transaction in the block, 0xed35282326ea961befcc5cb5828e1dddf4df708921c38af2295ef6766b8407c5, although persisted to the chain, shows no evidence at all that any transfer occurred.

A failed N3 GAS transfer

Inspecting the disassembled transaction script shows that a transfer was attempted in the requested amount, from gate.io’s wallet to the same recipient as the previous transaction. But for some reason the transfer did not actually occur.

An N3 transfer invocation script decoded

A token transfer is just a smart contract invocation — calling transfer(from, to, amount, data) in the token contract. There are a few possible outcomes when invoking a smart contract on the Neo N3 blockchain, and in particular for a NEP-17 token transfer, the outcomes can be:

  1. Successful transfer: a Transfer notification event is emitted by the chain and the contract execution ends in a HALT state with a stack value of True.
  2. Failed transfer: no Transfer notification event is emitted, execution ends in a HALT state with a stack value of False
  3. NeoVM exception: a Transfer notification event may or may not be emitted, but execution ends in a FAULT state

We can review the log of any transaction by making a JSON-RPC request to the getapplicationlog method of a public RPC node:

curl -X POST '<https://mainnet1.neo.coz.io/>' -H 'Content-Type: application/json' --data-raw '{
"jsonrpc": "2.0",
"method": "getapplicationlog",
"params": [
"0xed35282326ea961befcc5cb5828e1dddf4df708921c38af2295ef6766b8407c5"
],
"id": 1
}'

Here we see that the transaction in question returned no notifications and ended in a HALT state with a stack value of False - indicating a failed transfer:

Neo N3 transaction log

This alone should be enough evidence for the sending exchange to realize the transfer was not successful and they still have the user’s funds in their wallet. However, in this case the exchange support staff was still unconvinced that it was a problem with gate.io’s withdrawal process and that the error must lie with the user or the destination wallet/exchange.

So we know this transfer failed even though the transaction itself was persisted to the chain, but why did it fail? I had a suspicion, so I took a look at the balance history of the gate.io Neo N3 wallet in the Dora back-end database. Of course, end users don’t have access to the raw data in this way, but an exchange should provide their support staff with such tools for their internal wallets.

gate.io N3 wallet balance history for GAS

Here we can see that in the block in question, the gate.io wallet balance dropped from 2,307 GAS to 1,040 GAS, falling to 208 GAS over the next few minutes until they topped it up again to 12,353 GAS five minutes after the transfer failure in question.

At the start of processing of the block in question (456958) gate.io only had 2,307.36516389 GAS in their wallet — not enough to send both transfers, which would total 2,614.1 GAS. So the second transfer’s failure is purely due to gate.io’s attempt to send funds they simply didn’t have in the wallet at the time. This suggests that the gate.io withdrawal code has a bug in that it doesn’t consider the total amount of the pending withdrawals in a single block and whether it exceeds the current balance of the wallet.

Hopefully the gate.io team will read this article and fix their mistake and restore the user’s GAS balance, as well as fixing their withdrawal code so the same problem doesn’t occur again for additional users. The key takeaway here for users and exchange support staff alike is that that just because a transaction was persisted on the chain, it doesn’t necessarily mean it’s a successful transfer. All the information you need is on the chain, you just have to know how to read it.

Update - Nov 3, 2021: The user who originally reported the problem has confirmed that gate.io has fixed the issue today and restored his GAS balance.

--

--

Joe Stewart

Member of COZ community, a development organization focused on the Neo blockchain