BLOCKCHAIN AI.NEWS

Security · Analysis

$736,442 Left Chainflip Through Refunds for Swaps That Already Paid

An attacker got Chainflip's Tron USDT route to pay twice against the same money, six times over. Chainflip's public code shows that on Tron, unlike on Ethereum, it reads swap instructions from a transaction memo. Its own account of the attack left one step unexplained. Updated September 22: that step now has a name — the memo rode on one of Chainflip's own internal sweep transactions — and the network has restarted with Tron switched off and provider balances converted to claims.

Editorial illustration: a chrome coin in a frosted glass tray with a blank tag, and two chrome pipes leading away from it, one dripping gold and one glowing blue
✓ Disclosed by Chainflip (Sep 13) · Reported by crypto.news and The Crypto Times · Code read by this desk: Chainflip's Tron vault-swap witnesser and Ethereum Vault.sol · Transaction format from the TRON Developer Hub · Sep 22 update: restart and balance-reset detail from Unchained and CryptoSlate (Sep 17)

Early on Saturday, September 12, someone found a way to make Chainflip pay for the same Tron deposit twice. They did it eight times in about ninety minutes, starting small and roughly doubling the size of each round. By the time Chainflip noticed, because outgoing USDT payouts had started to fail, six unauthorized payouts had gone out, worth 736,442.17 USDT. That's according to the post-incident note the cross-chain swap protocol published at 10:11 UTC on Sunday. A seventh swap, worth 115,654.41 USDT, was caught while still pending, and those funds stayed in the vault.

Chainflip paused the whole network, not only the Tron route. It says it has a fix and expects to stay offline "until Monday at the earliest." It also says users who were affected will be made whole, though it hasn't said where the money will come from. crypto.news quoted the protocol's post on X: "All other funds are unaffected and secure."

What Chainflip says happened

Chainflip's explanation is short. On Tron, it reads a swap's instructions from the memo attached to the transaction. On its other chains, instructions arrive through dedicated contract functions. According to Chainflip, the attacker attached a new memo to a transaction its validators had already signed. The system read that memo as a separate swap, decided the swap had failed, and issued a refund. But the original deposit had already been paid out. So the refund was a second payment against the same money.

According to crypto.news, Chainflip hasn't published transaction hashes, attacker addresses or a per-round breakdown. As of Sunday evening, this desk had found no independent analysis from a security firm. So the details below come from Chainflip's own statement and its public source code. Neither has been checked against the chain.

Where the memo comes in

Chainflip's backend is open source, and the Tron witnessing code is a single file, vault_swaps_witnessing.rs. It was added in the Tron integration commit of May 7 and hasn't changed since. This desk read it. The code watches each Tron block for native TRX arriving at the vault address and for token Transfer events whose recipient is the vault. It ignores the sender. For every match, it fetches the full transaction by ID, takes the memo from the transaction's raw_data.data field, and decodes it into a swap instruction: output asset, destination address, optional cross-chain message, and a parameters blob. The file's own test shows that blob carrying refund settings, including a refund address.

Ethereum works differently. A vault swap there is a call to xSwapToken on Chainflip's Vault contract. In a single function, the contract pulls the tokens from the caller and emits a SwapToken event that carries the amount, the instruction and the sender together. The money and the instruction come from one call.

How a Chainflip vault swap tells the protocol what to do

Ethereum vaultTron vault
What the deposit isA call to the Vault contract's xSwapTokenAny transfer whose recipient is the vault address
Where the instruction livesArguments in that same call, echoed in a SwapToken eventThe memo field of whichever transaction carried the transfer
Sender recorded by the witnesserYes, msg.sender in the eventNo, the from field is ignored
Compiled by this desk from Vault.sol and vault_swaps_witnessing.rs on the public main branches as of Sep 13, 21:00 UTC. Deposit-channel swaps, which don't use memos, aren't shown. This is what the public code does. It isn't a confirmed description of the path used in the attack.

None of that proves how the attack worked. What it does show is why Tron was different. On Tron, the only thing tying a deposit to an instruction is that they share a transaction ID. Whoever wrote the transaction wrote the memo.

The step that needs explaining

That brings up a puzzle in Chainflip's own wording. TRON's developer documentation calls raw_data "the signed payload." It lists data as "an optional memo" inside it, and it says a signature is computed over a SHA-256 hash of the serialized raw_data. Read literally, a memo is part of what a Tron signature covers. You can't add one to an already-signed transaction without breaking the signature.

So "attach a new memo to a transaction that Chainflip validators had already signed" is probably shorthand for something more specific. It could mean a signed payload used somewhere Chainflip didn't expect, or a validator-signed transfer landing in a transaction someone else wrote. This desk isn't going to guess. Chainflip has promised a fuller technical report, and this is the sentence it most needs to spell out.

The fix isn't public yet either. The latest push to the public repository's main branch was on September 11 at 15:41 UTC, the day before the attack, and as of Sunday evening no commit touching the Tron witnesser had appeared. That's normal. Projects routinely ship security fixes privately and publish the code after the patched version is running.

A young route with real volume

Tron is a recent addition. Its integration code only merged in May. In a September 10 post announcing a USDT-TRC20 lending market, the team said Tron swaps had processed $25.63 million across 956 swaps in their first thirty days and called it "one of the strongest launches Chainflip has run." By this desk's arithmetic, the six payouts equal about 2.9% of that first month's volume. Chainflip hasn't linked the new lending market to the attack, and nothing in its account suggests a connection.

Chainflip says it has flagged the taken funds with "relevant parties" so they can be traced as they move. It hasn't said whether any of the USDT has been frozen, or how much it expects to get back.

Update, September 22: the missing step, named

Nine days on, most of the sentence this desk flagged has been filled in — not by the promised final report, which has still not appeared, but by Chainflip's own restart notices as read by Unchained and CryptoSlate on September 17.

The transaction the validators had signed was an internal one. Chainflip's vaults periodically sweep deposits with what the protocol calls a fetch transaction, signed by the validator set as ordinary housekeeping. Per Unchained's account, the attacker attached the memo to one of those internal fetches. The witnessing layer read the memo, concluded a swap had failed, and issued a refund — which landed on top of a liquidity-provider withdrawal the attacker had also initiated. That is the double payment. It answers the question this desk raised above: "already signed" did not mean a user's deposit re-signed, it meant the protocol's own sweep being made to carry someone else's instruction.

Both outlets narrow the window to 01:44–03:10 UTC on September 12, and both describe the loss as six liquidity-provider withdrawals paid twice rather than six deposits. That is a meaningful correction to the shape of the attack: the victims were the liquidity providers on the Tron route, not swap users.

The fix shipped as protocol version 2.2.13 on September 15. Per Unchained, it restricts Tron vault swaps to direct transfers only — that is, a memo can no longer carry a swap instruction on a transaction the protocol itself created. The same release carries the settlement machinery for what came next.

Chainflip restarted without the route that broke. By September 16, per CryptoSlate, swaps and quoting had resumed across the network with Tron excluded. At restart every open order, strategy and lending position tied to Tron USDT was closed. Each provider's balance was copied to a separate on-chain record of what they are owed and their live balance set to nothing; Chainflip's notice to them, quoted by Unchained, is that the account "will now read 0 for trxUSDT."

That is an unusually tidy piece of bookkeeping for a shortfall, and it is worth being clear about what it does and does not do. It converts a balance into a claim. The claim is recorded on chain, which makes it auditable and hard to quietly revise. It is not payment. Chainflip has said it has plans to deal with the shortfall and still intends to make providers whole, and as of this update it has named no funding source, no payout schedule, and no recovered amount. The lending market announced on September 10 was among the positions closed.

Two things remain outstanding. The Tron route has not been announced as reopened, and this desk found no notice of its return. And the full technical report Chainflip promised "in the coming days" on September 13 has not been published, which means the question of whether any other chain Chainflip supports reads instructions the same loose way is still unanswered by the protocol itself.

The Take

A refund is a payout. Any code path that sends money back has to be keyed to the deposit it's refunding, and has to know whether that deposit has already been spent. It can't be keyed to whatever instruction happened to arrive with it. Chainflip's Ethereum design gets this almost for free, because one contract call ties the money, the sender and the instruction together. Its Tron design has to rebuild that link by hand from a transfer log and a memo. By Chainflip's own account, that's the route where one deposit got counted twice. Chainflip's response so far has been fast and plain: a same-day note, a whole-network pause, and a promise to repay users. The test now is the report. It should explain exactly what "already signed" meant, show the transactions, and say whether any other chain it supports reads instructions the same way.

More on the subject