Security · Analysis
$736,442 Left Chainflip Through Refunds for Swaps That Already Paid
An attacker got Chainflip's Tron USDT route to pay against the same deposit twice, 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 leaves one step unexplained.
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.
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.