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.
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 vault | Tron vault | |
|---|---|---|
| What the deposit is | A call to the Vault contract's xSwapToken | Any transfer whose recipient is the vault address |
| Where the instruction lives | Arguments in that same call, echoed in a SwapToken event | The memo field of whichever transaction carried the transfer |
| Sender recorded by the witnesser | Yes, msg.sender in the event | No, the from field is ignored |
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.