Root Cause
X100 Global's governance design allowed flash-loan-borrowed tokens to participate in Senate elections without any time lock, snapshot mechanism, or stake-age requirement. The three-seat Senate was the sole check on treasury withdrawals, and the governance contract used live token balances at vote time rather than historical snapshots.
// Vulnerable pattern (pseudocode)
function electSenateMember() external {
// Uses live balance — exploitable with a same-block flash loan
uint256 votes = token.balanceOf(msg.sender);
// No: require(block.number > depositBlock[msg.sender] + MIN_STAKE_AGE)
// No: snapshot-at-proposal-creation check
_recordVote(msg.sender, votes);
}
function executeProposal(uint256 proposalId) external {
// No timelock between approval and execution
// Proposal passes and executes in the same transaction
_execute(proposals[proposalId]);
}
Attack Steps
| Step | Action | Detail |
|---|---|---|
| 1 | Flash-loan X tokens | Borrows large quantity of X via Uniswap V3 flash loan |
| 2 | Seize 2 of 3 Senate seats | Votes with borrowed tokens atomically; needs only 2/3 for quorum |
| 3 | Submit malicious treasury proposal | Proposes: sell 400,000 X tokens with minOutput = 0 |
| 4 | Execute proposal in-band | Treasury sells 400,000 X into attacker-created PancakeSwap V3 pool; attacker controls the pool and receives all value |
| 5 | Liquidate remaining X | Dumps residual X on Uniswap V3 during execution |
| 6 | Repay flash loan | Returns flash-borrowed X; net proceeds ≈ $4.75M |
All six steps executed in a single atomic transaction, leaving no window for on-chain defence.
Impact
- ~$4.75M–$4.85M USDT drained from the X100 treasury (initial BlockSec estimate: $4.75M; updated Blockaid figure: $4.85M).
- X100 token price declined >96% within minutes of the exploit.
- Protocol operations suspended; no confirmed recovery timeline at time of writing.
Lessons for Auditors
- Snapshot-based voting is non-negotiable. Record token balances at proposal-creation block, not at vote or execution time. ERC-20 Votes / OpenZeppelin
ERC20Votesprovides this pattern out-of-the-box. - Mandatory timelocks. Require a minimum delay (e.g., 48 h) between proposal approval and execution. This allows the community to react and an emergency multisig to pause.
- Stake-age requirement. Tokens must have been held for at least N blocks before their votes count, neutralising same-block flash-loan acquisition.
- Council size and quorum. A 3-seat council where a 2/3 majority is sufficient is trivially attackable if two seats can be obtained atomically. Larger councils, higher quorum ratios, or external veto rights improve resilience.
minOutput = 0in governance-controlled swaps. Treasury swap proposals should enforce meaningful slippage bounds and route through audited price oracles, not attacker-owned pools.