TL;DR
At 14:11:47 UTC on October 24, 2026, one Ethereum transaction took control of X100's three-seat Senate and used it to sell 400,000 X from the project's Freezer treasury for 35.68 USDT. A 1.94M USDT Aave flash loan bought about 38,200 X. Two helper contracts each froze 19,000.000001 X, one raw unit more than the second-ranked senator, and took two of three seats. A proposal was created, voted and executed in the same call, and the treasury swapped into a pool the attacker had seeded with about 100 USDT. The first attacker kept 4,749,726 USDT. Two more actors reused the same path within 30 minutes. Blockaid counted about $4.85M USDT realized, and the third transaction pulled roughly 550,000 X of the treasury's remaining liquidity. X fell more than 96%. Every contract executed as written. The Senate's three-day voting period capped how late a proposal could run and never how early.
What is the X100 Senate, and what did it control?
X100 is a small ERC-20 token on Ethereum (ticker X). Its holders stake ("freeze") X in a contract called the Freezer, and the largest stakers sit in a Senate that decides where the project's liquidity goes. According to Coindaro's reading of the project's site and verified code, X has a fixed supply of 14,000,000, about 12M of it set aside for staking rewards and about 1M for liquidity. Seats go to the largest stakers, a candidate pays a 100 X fee, and a proposal passes with more than half of the seats.
The Senate had three seats. Before the attack they were held by stakes of 20,500 X, 19,000 X and 1,000 X, about 40,500 X in total. The first holder registered at 12:07 UTC on the day of the attack, so the Senate had been live for roughly two hours.
Per the verified DexTreasury source, that Senate could tell the Freezer, which was the treasury, to mint, change and swap DEX liquidity positions. Two seats out of three controlled all of it.
How did the attacker take two of three seats?
The attacker rented them. A flash loan must be borrowed and repaid inside a single transaction; if repayment fails, everything reverts. That puts very large, uncollateralized positions in anyone's hands for one block, for a fee.
ExVul's root-cause write-up pins the hole precisely: applyForSenate() lets anyone join immediately for the 100 X fee, and the three seats are chosen from current frozen balances, with no voting-power snapshot, no stake-age requirement and no execution timelock. Seat rank is a live sort over whatever is frozen at this instant.
So the attacker sized the bid to the unit. Two helper contracts each froze 19,000.000001 X, which beat the 19,000 X incumbent by one raw unit and pushed both the 19,000 X and 1,000 X senators out of seats two and three. The 20,500 X senator kept seat one, and was now outvoted two to one.
By our reading of the ranking code, a stake that ties an incumbent sorts below them, so matching 19,000 X exactly would have displaced no one. The attacker bid one raw unit above it.
How does a three-day vote execute in the same block?
The three days only set an expiry. The X100 code gives every proposal a votingPeriod of three days, but that number only defines when a proposal expires. execute() checks two things: the proposal is still open, and more than half of the seats voted for it. Nothing checks that any time has passed. Coindaro reads the verified contract the same way: a proposal can be executed as soon as it has enough votes, with no delay.
Proposing also counts as a vote. When helper one created proposal #7, it was recorded as voting for it. Helper two voted. That was two of three, and execute() ran immediately, inside the same transaction that had bought the seats.
The proposal's description field, visible in the transaction's event logs, read "testnet flash governance proof".
Why did the treasury sell 400,000 X for 35 dollars?
The proposal called the Freezer's own v3Swap function. Per ExVul, the parameters were PancakeSwap V3, X to USDT, fee tier 100 (0.01%), amountIn of 400,000 X and minOut of 0. As we read the verified Freezer source, the swap checks only that a pool exists in the factory for that pair and fee tier. It does not check that the pool has liquidity, that its price is anywhere near the market, or that the output is above zero.
The attacker had created exactly such a pool earlier in the same transaction and seeded it with 100 USDT. The treasury's 400,000 X went in. Out came 35.675313 USDT. Slippage protection is the minimum-output parameter on a swap; set it to zero and the swap accepts any price, including a price the counterparty set earlier in the same transaction.
The attacker then withdrew the pool's liquidity, recovering 399,986.8 X, and sold 131,702.92 X into the main Uniswap V3 X/USDT pool for 6,693,089.64 USDT. That pool still held the 1.94M USDT the attacker had paid for X a moment earlier, which is the loop that let a borrowed 1.94M USDT pay for itself. The rest of the 6.69M came out of the main pool's existing USDT reserves, which included the Freezer's own liquidity positions; Blockaid notes the second attacker later burned those positions.
The attack, step by step
There was no rehearsal on-chain. The attacker wallet's history starts with two incoming funding transfers at 14:05:59 and 14:06:59 UTC, and its first outgoing transaction is the exploit itself, a contract deployment. The coordinator contract, both helpers and the PancakeSwap pool were all created inside that one transaction.
| # | Step | On-chain result |
|---|---|---|
| 1 | Fund the deployer wallet | 0.86 ETH arrives from a wallet labeled FixedFloat, minutes before the attack (per Coindaro) |
| 2 | Flash-borrow from Aave | 1,942,457.64 USDT |
| 3 | Buy X in the main Uniswap V3 X/USDT pool | 38,200.000002 X for 1,942,356.64 USDT |
| 4 | Split into two helpers; each freezes and pays the fee | 19,000.000001 X frozen plus 100 X fee, twice |
| 5 | Helpers rank into seats two and three | Senate: 20,500 X incumbent plus two attacker helpers |
| 6 | Create and seed a PancakeSwap V3 X/USDT pool, 0.01% tier | 100 USDT of liquidity |
| 7 | Proposal #7 created (auto-vote), second vote, execute() | 2 of 3 seats, executed in the same call |
| 8 | Treasury v3Swap into the seeded pool, minOut 0 | 400,000 X out, 35.675313 USDT back |
| 9 | Remove the seeded pool's liquidity | 399,986.8 X recovered |
| 10 | Sell into the main pool | 131,702.92 X for 6,693,089.64 USDT |
| 11 | Repay Aave | 1,943,428.87 USDT, a 971.23 USDT fee |
| 12 | Forward profit | 4,749,726.10 USDT to the attacker wallet |
Every figure from step 2 onward is in the transaction's token transfers. The net movement out of the Freezer in this transaction was 361,799.999998 X: 400,000 sold, less the 38,200.000002 X the helpers paid in as stake and fees. Roughly 268,284 X were left sitting in the attack contract.
The out-of-pocket cost was a 971 USDT loan fee and gas, against a 4,749,726 USDT payout. Control of the Senate cost 1,942,356 USDT worth of X, held for the length of one transaction.
What happened after the first transaction?
The same route was used again, because nothing about it had changed. Coindaro's timeline shows the same route used twice more:
- 14:35:59 UTC. A second actor flash-borrowed about 1.95M USDT, took seats two and three with 19,000.000002 X per helper, passed six proposals, burned the Freezer's Uniswap V3 positions, and sent about 96,382 USDT and 9,895 X to another address.
- 14:40:47 UTC. A third transaction passed four more proposals and moved 549,916 X and about 1,909 USDT out through the PancakeSwap pool.
The second actor's stake shows how thin the margin was. The first attacker's helpers held seats two and three at 19,000.000001 X; the second actor froze 19,000.000002 X. Twice, a seat changed hands on one raw unit of stake, and both bids were paid for with flash loans.
The defenders had one move, and it did not fit the problem. Between 14:12:35 and 14:21:59 UTC, the address listed first among the senators in the exploit's execution event, the seat-one holder, sent six more Freeze transactions to the Freezer. Whatever the intent, more stake on seat one moves nobody out of seats two and three, and a majority needs two of them.
The first public alert, from Blockaid, went out at 14:49 UTC, eight minutes after the last of the three waves. Coindaro's count of on-chain sending wallets found three different senders and no established link between them, so whether this was one operator or three is open.
How much was actually lost: $4.75M or $4.85M?
Both numbers are correct, and they measure different things. BlockSec Phalcon and ExVul report $4.75M because they describe the first transaction, which delivered 4,749,726.10 USDT to the attacker wallet. Blockaid's $4.85M covers the first two transactions. Add the second actor's roughly 96,382 USDT and you get about 4,846,108 USDT, the total Coindaro computes. We treat $4.85M as the realized USDT loss of the incident, with the main attacker taking $4.75M of it.
That figure excludes the X itself. Blockaid's update adds roughly 550,000 X that the third attacker pulled from the Freezer's remaining Uniswap V3 liquidity (Coindaro counts 549,916 X leaving in that transaction), and Chainalysis puts unsold X at attacker addresses at about 560K. Bitcoin.com reported X falling from the $50 to $60 range to about $1.75, a drop of more than 96%, and Coindaro found about $341 of liquidity left in the main X/USDT Uniswap pool by 16:50 UTC. We do not put a dollar value on the extracted X.
Where did the money go?
Out of Tether's reach, fast. Per Coindaro, the 4,749,726 USDT was sold through UniswapX for about 2,022 ETH in two orders within minutes, which removed the option of a Tether freeze. The ETH was then split: four transfers of 400 ETH each to fresh addresses, a pattern Chainalysis and Finwire both report, with about 422.86 ETH left at the intermediate address.
An on-chain message to the attacker wallet from a self-described independent researcher in contact with the X100 team offers to relay a return-for-bounty deal, noting the FixedFloat funding and an Across bridge trail are documented. No return has been reported.
The team had made no statement as of 17:00 UTC on October 24, per Coindaro, and we found no post-mortem as of October 26. The Defiant's report carried Blockaid's warning that the attack was still ongoing.
What bounded the damage?
Roughly 12.17M X is still in the Freezer, per Coindaro, which also quotes the project's site: no more than 1,000,000 X can be on the market at once, and nobody, creators or senators, can withdraw tokens to a wallet. The attack ran entirely through trades. By our reading of the verified DEX module, it enforces a sell cap on X: a net counter that rises whenever the treasury puts X into the market through a swap or a liquidity position, falls when X comes back, and reverts any action that would push it past the cap. Our own call to the contract's dexSold() and dexSellCap() read functions on October 26 returned 999,999.999998 X against a cap of 1,000,000 X. The 399,986.8 X the first attacker pulled back out of its own pool went to the attacker contract, never to the treasury, so it did nothing to lower the counter.
The three waves ran the counter to within two millionths of a token of the ceiling, and there it stopped. The cap is set when the module is initialized, and as we read the code none of the functions a proposal may call can change it. A majority that could do anything else with the treasury could move about a million X into the market and no more. As far as we can tell from the code, that ceiling is why the other 12M X were not dumped into a dead pool as well.
Is this the same thing as BonkDAO or Beanstalk?
It is the same family with a sharper edge. Beanstalk lost about $77M in user assets on April 17, 2022, when an attacker used a flash loan to exploit its governance mechanism. BonkDAO lost about $20M in July 2026 to a voting majority that was bought on the open market over days. Each case turns on voting power that can be acquired faster than anyone can react to it.
X100 compresses the timeline to zero. There was no accumulation phase and no public proposal to watch: registration, vote, execution and swap were one transaction. The attacker only needed the treasury to agree to a bad trade. Governance that allowlists swaps and liquidity changes as safe actions still needs a price check on each one.
A day later, SlowMist reported Yapeswap losing about $21.9K after a zero-quorum vote handed over a factory admin role. For a related admin-capture case, see the Neutron governance admin takeover.
What should teams running on-chain governance do?
- Snapshot voting power before the proposal exists. Seats or votes must be computed from balances at a block before the proposal was created, so anything borrowed in the voting transaction counts for nothing. This single change defeats the flash-loan version of the attack outright.
- Make the voting period a minimum delay.
execute()should require that a fixed delay has elapsed since creation, then a timelock before the call runs. Search for every placevotingPeriodis read; if the only reader is an expiry check, execution is immediate. - Require stake age for seats. A seat should go to stake that has been frozen for longer than the voting delay. Candidacy by fee alone means candidacy by flash loan.
- Treat every treasury action as a value transfer. A swap with
minOutof zero into any factory pool is a transfer to whoever controls that pool. Allowlist specific pool addresses, bound slippage against a TWAP or a reference price, and cap size per proposal. - Keep hard invariants outside governance. X100's sell cap is the reason about 12M X survived. Identify what a hostile majority could do in one block, and put a ceiling on it that no proposal can lift.
- Model the copycat window. Once the first exploit lands, the path is public. With no pause and no guardian, the only response is outbidding the attacker inside the same governance system. Decide before launch who holds an emergency brake and what it can touch.
What would have caught it before launch?
A design review of the governance contracts. Every one of these failures is visible in a few hundred lines of Solidity: seats computed from live balances, a voting period that never gates execution, a candidacy fee as the only admission control, and a swap path that trusts any factory pool with zero slippage protection. None of it depends on an obscure compiler edge case or a cross-contract interaction that only shows up on mainnet. It also went live without time to be reviewed in public; the Senate was two hours old.
A first transaction like this one is atomic, and by our reading the verified Freezer source has no owner, pause or guardian role, so the defense had to exist in the code before launch. That is the exact job of a full smart-contract audit: model the governance system as an adversary with unlimited one-block capital, then check what a majority can reach. We test seat and vote accounting against flash-loaned balances, trace every proposable selector to the value it can move, and confirm each treasury action carries a price bound fixed in code, outside any proposer's reach.
Frequently Asked Questions
What happened to X100?
On October 24, 2026, an attacker used a 1.94M USDT Aave flash loan to buy 38,200 X, froze it across two helper contracts to take two of the three X100 Senate seats, and passed a proposal in the same transaction that sold 400,000 treasury X for 35.68 USDT into an attacker-seeded pool. The attacker netted 4,749,726 USDT. Two follow-on transactions by other actors brought realized losses to about $4.85M USDT.
Was the X100 exploit a smart contract bug?
Yes, in the design. The contracts encoded seats ranked by live balances with no snapshot, a voting period with an expiry but no minimum delay, and a treasury swap with no pool allowlist or slippage bound, and the attacker used those rules as written.
Could monitoring have stopped the X100 attack?
No, not the first transaction. It was atomic, and we found no owner, pause or guardian role in the verified source, so an alert had nothing to interrupt it with. The observable signals were all inside that same block: two new senators registering, a proposal created, voted and executed in one call, and a treasury swap returning under 36 USDT for 400,000 X. Those signals matter for the two copycat waves, which came 24 and 29 minutes later, but only if someone holds a lever to pull.
How was the $4.85M figure calculated?
It is the USDT realized in the first two attack transactions, about 4,749,726 plus 96,382, roughly 4,846,108 USDT. The commonly cited $4.75M covers the first transaction only. Neither includes the roughly 550,000 X pulled in the third transaction.
Have the stolen funds been recovered?
No. As of October 26 no return has been reported. The USDT became about 2,022 ETH, split into four 400 ETH wallets.
Sources / References
- Etherscan: X100 exploit transaction 0x38c60844...4d81d0
- Etherscan: X100 Freezer contract 0x79f35a33...d822a46, verified source
- Blockscout API: event logs of the exploit transaction
- Etherscan: on-chain bounty message to the attacker wallet
- Coindaro: X100 exploit: $4.85M in USDT drained through its Senate
- Blockaid on X: ongoing exploit on X100
- ExVul on X: X100 Freezer/DexTreasury root cause
- BlockSec Phalcon on X: X100 alert
- Chainalysis on X: How today's X100 attack went down
- Bitcoin.com News: X 100 Global Loses Millions as Three-Seat Senate Is Hijacked
- Finwire: X100 Exploit Proceeds Split Into Four 400-ETH Wallets
- The Defiant: X100 Exploit Nets 4.85 Million USDT After Governance Takeover
- Beanstalk: Beanstalk Governance Exploit
- SlowMist on X: Yapeswap loss alert



