Exploit post-mortems
/
Transit Finance
TRON
Arbitrary call

Transit Finance's $1.88M Loss and How Olympix Would Have Prevented It

Loss
Loss
Exploit date
May 11, 2026

Transit Finance stopped using its early TRON swap bridge in 2022. Four years later, on May 11, 2026, that bridge was still on-chain, still wired into Transit's approval system, and still able to move USDT out of wallets that had approved it years earlier. An attacker found it and took about $1.88 million, according to BlockSec and PeckShield.

Nobody had to break a key, fool an oracle or manipulate a pool. The attacker wrote a short contract, pointed it at the old bridge, and asked the bridge to forward a call. The bridge forwarded it to the one contract in Transit's stack that users had trusted with unlimited USDT allowances, and that contract did what it was told.

An Aggregator Built Around One Approval

Transit Finance is a cross-chain swap and bridge aggregator. It routes swaps and bridge transfers across many blockchains, TRON among them, and finds the route for the user so they don't have to stitch it together themselves.

Aggregators like this usually separate two jobs. One set of contracts decides where a trade should go and executes the route. A separate approval contract holds the token allowances users grant, so a user approves once and every future route can pull funds through that single trusted contract. It's a clean design for user experience. It also means the approval contract carries the combined spending power of every user who ever approved it.

On TRON, that approval contract is TransitApproveGovernanceTron. On-chain data from TronScan shows the routing chain in front of it: a TransitProxyV3Tron entry contract, the TransitMixSwapBridge router behind it, and supporting contracts such as XswapFeeModelTronV2 and WTRX. According to BlockSec's analysis, TransitMixSwapBridge was part of Transit's TRON deployment and had been deprecated since 2022.

Deprecated, in this case, described Transit's product rather than the chain. The router was no longer in use by Transit's app, but it still existed at its address, it could still be reached through the proxy, and the approval contract still recognized it as a caller it would act for.

The Call That Nobody Checked

BlockSec's root cause is direct. TransitMixSwapBridge exposed arbitrary calldata forwarding. Calldata supplied by the caller passed through Transit's execution chain without sufficient validation, and the path placed no restrictions on the target or the calldata of the calls it forwarded.

That matters because of where the forwarded calls ended up. They reached TransitApproveGovernanceTron, which still held unlimited USDT allowances from users who'd approved it and never revoked. When the approval contract executed a forwarded USDT.transferFrom(), the token contract saw a legitimate spender with a legitimate allowance and moved the funds. From USDT's point of view, nothing unusual happened at all.

So the authority in the system sat in the approval contract, and the decision about what that authority should be used for sat in the router. The router never asked whether the call it was passing along was a swap step or a transfer out of a stranger's wallet. Once the router stopped checking, the approval contract's power was available to anyone who could reach the router.

Halborn's May review describes the same thing in shorter terms: the legacy contract was still active and callable, and it had weak input validation.

How the Drain Ran on May 11

TronScan shows the attacker's wallet, TF9xyL...6JoS, deploying a contract named Test at 01:49:00 UTC on May 11. It's unverified, and the method the attacker called on it is run(address[] tokens, address[] users), which takes a list of token addresses and a list of user addresses.

A minute and a half later, at 01:50:27 UTC, the attacker called run() in transaction 3a981b...8ac2. The internal call trace on TronScan lays out the path in order. Test called TransitProxyV3Tron. The proxy called TransitMixSwapBridge. The bridge made its usual supporting calls into XswapFeeModelTronV2 and WTRX, then called TransitApproveGovernanceTron. The approval contract called USDT. The trace runs that bridge-to-approval sequence twice, once for each user address passed to run(), and each pass ends in a USDT transfer from a victim's wallet. In that single transaction, two wallets lost funds: 1,090.065703 USDT from one and 127,151.356786 USDT from the other, a total of 128,241.422489 USDT, all sent straight to the attacker's wallet.

That was one batch. TronScan lists hundreds of transactions on the attacker's wallet, including a long run of calls into the Test contract that continues into the early hours of May 12. Because run() accepts a list of users on every call, the attacker could sweep wallet after wallet in batches, and the drain added up to roughly $1.88 million. BlockSec and PeckShield both put the total at about that figure, and the stolen funds were later converted to DAI and held on Ethereum, which is why some reports describe the loss in DAI.

The supporting calls in the trace are worth a second look. TransitMixSwapBridge still charged its routine pass through XswapFeeModelTronV2 and still wrapped TRX through WTRX, the same housekeeping it would perform for a real swap, before handing the forwarded call to the approval contract. From the inside, the router was behaving like a router. Two more details stand out from the trace. The attacker never needed to touch Transit's current contracts, and the call reached the approval contract through Transit's own proxy, the front door the protocol had built for legitimate routes. Everything the attacker used was Transit infrastructure that had been left in place.

The Money Trail and Transit's Response

PeckShield flagged the exploit on May 11 and linked the funds to an Ethereum address holding the stolen value as DAI. Transit Finance confirmed the incident in a public notice two days later. According to ChainCatcher's report of that notice, the team attributed it to a historical vulnerability in an old version of its smart contract deployed on TRON and deprecated in 2022, said it affected a small number of users, and committed to compensating affected users in full.

Transit said affected users didn't need to take any action and that the compensation would be handled by the company. It also said it had completed additional security reviews and recovery measures by May 12, and that only users who had interacted with the outdated contract were exposed. Transit also sent an on-chain message to the attacker, according to reporting at the time.

It isn't Transit's first incident. The protocol suffered a major exploit in 2022 as well, which PeckShield also flagged when it happened. As of November 10, 2026, Transit hadn't published a technical post-mortem for the May incident.

Why a Retired Contract Kept Its Power

Retiring a contract is mostly an off-chain act. A team removes it from the front end, stops routing new trades to it, and moves on to the next version. On-chain, the contract keeps every permission it had the day it was deployed unless someone takes those permissions away. TransitMixSwapBridge kept its place in the call chain, and the approval contract kept honoring it.

Security reviews follow the same off-chain logic. Each review is scoped to the code that's changing, so the work on a new version of a router naturally examines the new router. The interaction that broke here lived between two scopes: a router everyone considered finished and an approval contract that was still very much alive. It's the kind of combination that doesn't belong to any single change, which is exactly why it can sit untouched for four years.

BlockSec's own lessons point at the same seam. Deprecated contracts should be fully decommissioned, including revoking any approval authority they hold, and any path that forwards arbitrary calldata should validate both the target address and the function selector. Either one would have closed this path.

Users can shrink the blast radius too by revoking approvals to protocols they no longer use, but the people who lost USDT on May 11 had done nothing wrong. They approved a contract that Transit asked them to approve, and that approval outlived the product it was granted for.

How Olympix Would Have Closed the Forwarding Path

Olympix reads a protocol as states, transitions and entry points, and it reads them across contracts, which is where Transit's problem lived. The state that matters is the set of allowances users granted to TransitApproveGovernanceTron. The transitions are the calls that can spend those allowances. The entry points are every external function that can reach them, including the ones behind TransitProxyV3Tron and TransitMixSwapBridge that the protocol no longer advertised.

From that structure, the engine infers the invariant the system depends on. Tokens should only leave a user's wallet through the approval contract as part of a route that same user started. That property spans three contracts, and it's the one the old router quietly broke.

Static analysis would have surfaced the signal here. A function that forwards caller-supplied calldata to a caller-supplied target, with no allowlist on either, is a well-known marker for an arbitrary call. On its own, that signal might look harmless inside a router. It becomes critical when the engine follows the path one hop further and sees that the forwarded call lands in a contract holding user allowances. Signals like that are what let the engine narrow from every possible path to the feasible ones, and from there to the few that touch the invariant.

The engine attacks those paths two ways. BugScout runs Bounded Adversarial Verification, starting from a signal and working inward with known attack strategies until one lands, which is how known classes like an open forwarding path get tested quickly. BugPoCer runs Counterexample-Guided Invariant Refutation from the system outward. It takes an invariant and searches for a sequence of state changes that breaks it, which is how it reaches bugs that span several contracts and don't match any single pattern. Had Transit run it on the TRON deployment, the search would have found a sequence that looks almost exactly like the May 11 trace: a call into the proxy, routed to TransitMixSwapBridge, carrying calldata that makes TransitApproveGovernanceTron execute USDT.transferFrom() from a wallet that never initiated anything. BugPoCer would have returned that sequence as a counterexample, with the minimal conditions that trigger it and a generated test that reproduces the transfer, at the merge where the forwarding logic was written. The counterexample wouldn't need a real victim or a real allowance to make the point. It would show the state before the call, the single call into the proxy, and the state after it, with a user's USDT balance lower and the caller's higher, which is the invariant broken in the most literal way possible.

Olympix runs at every merge and before release, which matters for a story like this one. A contract doesn't stop being part of a system when it stops appearing in the app. Had the router's forwarding path and the approval contract been analyzed together, that counterexample would have pointed at two fixes: restrict what the router can forward and to where, and revoke the approval contract's trust in any router that's being retired. Both are a few lines of code or a single governance action, set against $1.88 million in user funds.

Retire the Permissions With the Code

A deprecated contract is only retired when it can no longer act. For aggregators and anything else built around a shared approval contract, the inventory that matters isn't the list of contracts the app still calls. It's the list of contracts the approval system still trusts. To see how Olympix maps every path into your approval and custody contracts and returns a working counterexample for each one that breaks, book a technical walkthrough.

On-Chain References

Exploit tx (May 11, 2026, 01:50:27 UTC): 3a981b...8ac2

Attacker wallet: TF9xyL...6JoS

Attacker contract (Test): TUfPjK...kbc4

TransitMixSwapBridge (deprecated router): TUY2wr...7NPz

TransitApproveGovernanceTron (approval contract): TTLaNd...bqew

TransitProxyV3Tron: TFHc9q...i15N

See what Olympix finds on your own code

Send us a repo that's already been audited, and we'll run Olympix at the same commit and show you what comes back.