On 24 October 2026 at 14:11:47 UTC, a single Ethereum transaction turned a flash loan into a governance majority at X100, then used that majority to make the project’s own treasury sell 400,000 X for roughly 36 USDT. The attacker walked away with about 4.75 million USDT from that one transaction. Follow-on transactions using the same path pushed the total realized to roughly $4.85M, according to Blockaid.
Nothing here required a stolen key or a compiler bug. The governance system did exactly what it was written to do. It just measured voting power at the one moment an attacker could rent it.
X) whose “Senate” decides where the project’s liquidity sits.minOut = 0 into a pool the attacker had created earlier in the same transaction.X100 describes its Senate as the body that controls where protocol liquidity is held, with smart contracts carrying out its decisions. Holders lock (“freeze”) X in a contract called the Freezer, and the three largest frozen positions among Senate applicants hold the three seats. The Freezer also acts as a treasury, holding X and Uniswap V3 positions, and exposes a v3Swap function that the Senate can direct.
Before the attack, per Coindaro’s reading of the Senate’s on-chain ranking events, the seats were held by stakes of 20,500 X, 19,000 X and 1,000 X. In other words, the whole treasury was guarded by roughly 40,500 frozen tokens. At a price around $50 per X, that is about $2M of stake protecting many times that amount in treasury assets. That ratio alone should have been a red flag.
Three design choices combined into the exploit. Each one shows up regularly in audit findings on its own.
ExVul’s analysis notes that applyForSenate() let anyone become a candidate immediately for a 100 X fee. There was no waiting period between applying, freezing tokens and taking a seat.
Seats were assigned from current frozen balances. There was no checkpoint of voting power at an earlier block and no requirement that tokens had been frozen for any length of time. If you could hold enough X for one transaction, you could be a senator for that transaction.
Once two of three seats were captured, a proposal could be created, approved and executed immediately. ExVul reports that proposal #7 auto-counted the proposer’s vote, the second helper voted, and execute() ran straight away. A timelock of even a few hours would have broken the flash loan, since the loan must be repaid in the same transaction.
The simplified sketch below is our own illustration of this pattern, not X100’s verified source code:
// Simplified illustration of the vulnerable pattern (not verbatim X100 code)
function applyForSenate() external {
xToken.transferFrom(msg.sender, treasury, 100e18); // entry fee
isCandidate[msg.sender] = true; // effective immediately
}
function _rankSenate() internal {
// Top 3 candidates by CURRENT frozen balance, no snapshot
seats = topThreeBy(frozenBalance);
}
function propose(bytes calldata action) external onlySenator returns (uint256 id) {
id = ++proposalCount;
proposals[id].action = action;
proposals[id].votes = 1; // proposer votes automatically
}
function vote(uint256 id) external onlySenator {
proposals[id].votes++;
}
function execute(uint256 id) external {
require(proposals[id].votes >= 2, "No majority");
// No delay between approval and execution
(bool ok, ) = address(this).call(proposals[id].action); // e.g. v3Swap(...)
require(ok);
}
function v3Swap(address router, uint24 fee, uint256 amountIn, uint256 minOut)
external
onlySelf
{
// minOut chosen by the proposal, so it can be zero
_swapExactInput(router, address(xToken), USDT, fee, amountIn, minOut);
}
The following sequence is assembled from BlockSec Phalcon, ExVul and Coindaro’s on-chain reconstruction. We confirmed the transaction hash, sender, block (26,263,022), timestamp and the victim and coordinator contracts against a public Ethereum node.
A contract deployed by the attacker wallet in the attack transaction itself borrowed about 1.94M USDT in a flash loan and bought roughly 38,200 X from the main Uniswap V3 X/USDT pool.
The X was split between two helper contracts. Each paid the 100 X application fee and froze 19,000.000001 X, one millionth of a token more than the 19,000 X incumbent. The two helpers took seats two and three, pushing out the 19,000 X and 1,000 X senators.
The attacker created a new PancakeSwap V3 X/USDT pool with a 0.01% fee tier and seeded it with a tiny amount of liquidity. Coindaro reports about 100 USDT on the USDT side. This pool is where the treasury would be forced to sell.
One helper submitted a proposal calling the Freezer’s v3Swap with 400,000 X in, minimum output 0, routed through the new PancakeSwap pool. The other helper voted, and the proposal executed in the same call. ExVul measured the treasury’s proceeds at 35.675313 USDT for 400,000 X.
The attacker withdrew their liquidity from the PancakeSwap pool, which now held the treasury’s X, and sold part of it into the real Uniswap V3 pool. Coindaro puts that sale at 131,703 X for about 6.69M USDT. After repaying the flash loan, about 4,749,726 USDT went to the attacker wallet, with roughly 268,000 X left in the attack contract.
Blockaid flagged the exploit at 14:49 UTC and described it as ongoing. A second actor burned the Freezer’s Uniswap V3 positions, and a third used the same Senate route to pull a position tied to about 550,000 X. Chainalysis reported the stablecoins being swapped to ETH and split across four wallets holding 400 ETH each. Several outlets reported that roughly 12.2M X still sat in the Freezer at the time of Blockaid’s update.
Flash-loan governance capture is not new. The Beanstalk attack in 2022 used the same idea: borrow voting power, pass a proposal, execute, repay. What makes these incidents repeat is that governance code is often reviewed as “admin logic” rather than as a market. If voting power can be bought, then governance is a price, and the right question is how much it costs to control the treasury for one block.
At X100 that cost was a flash loan fee plus some slippage, against a treasury worth millions. The minOut = 0 swap made it worse, but it was not the root problem. Even with a slippage check, a majority that can call arbitrary treasury functions immediately will find another way to move funds. We covered a related failure mode, contracts trading at whatever price someone else sets, in our post on flash loan attack prevention.
Use checkpointed balances (for example ERC20Votes with getPastVotes) taken at a block before the proposal was created. Tokens bought in the same block, or the same transaction, should carry zero weight.
For seat-based systems like a Senate, require that the qualifying stake has been frozen for a minimum period before it counts, and make the seat change effective only after that delay.
A timelock is the single most effective control here. It makes flash loans useless, and it gives holders and monitoring services time to react to a malicious proposal.
Treasury functions reachable from governance should have hard limits: allowlisted pools and routers, per-period outflow caps, and an oracle-based minimum output rather than a caller-supplied one. A proposal that sells 400,000 X for 36 USDT should revert no matter who approved it.
During an audit, compare the cost of acquiring a governance majority with the value governance can move. If capturing the vote is cheaper than the treasury, the design is exploitable even when every line of code is correct.
For a broader pre-launch review list, see our smart contract security checklist.
0x38c60844...4d81d00x7ec2afdf3ec5964899894f8d58afa95cbb8ffd630x79f35a33fe2813414851c4ae8daeacd14d822a460x37541fc4a6f1ec86889ba506f51dc39f2bedd10aapplyForSenate() details, seat amounts, proposal #7, swap parameters and addressesX100’s Senate had three seats, and two of them were for sale for the length of one transaction. Governance that reads live balances and executes instantly is not governance; it is an open function with an entry fee. Snapshots, stake age and a timelock would each have stopped this attack on their own.
Launching a DAO, a treasury or any token with on-chain voting? Contact Panda Security for a governance and economic review before it controls real funds, or check our audit pricing.
A missing sign check on max_fee in WAX's eth.token withdraw action let an attacker bridge out ~$90K in USDC, USDT, DAI, WETH and WBTC for free.
A stale sell counter in NemTokenV2 let an attacker burn the PancakeSwap pair's own NEM 42 times and sell into a pumped price for $822K on BNB Chain.
A wrapped recovery deadline and a replayable signed payload cost an Argent-style smart wallet system about $59.8K on Ethereum. How it worked and how to fix it.
A vault that lets callers pass in their own contract addresses and then books whatever those contracts return is a drain waiting to happen. Here is how this smart contract vulnerability works, why decompiled bytecode still gives it away in minutes, and the checklist PandaSecurity uses when auditing accounting logic.