QUATERNION Pair-Rebase Accounting Drift Enables Permissionless Drain
Exploit Transactions
0x352320bf89db8a117b87b28213bf936e58db848cde788571fe9164a1aac7b68b0xfde10ad92566f369b23ed5135289630b7a6453887c77088794552c2a3d1ce8b70x37cb8626e45f0749296ef080acb218e5ccc7efb2ae4d39c952566dc378ca1c4c0xd78380d1caaf494338d2c5d9093ebee7dcea2a2b804ceb7714dad899bae65be1Victim Addresses
0xc9fa8f4cfd11559b50c5c7f6672b9eea2757e1bdEthereum0xa8208da95869060cfd40a23eb11f2158639c829bEthereumLoss Breakdown
Similar Incidents
TomInu Reflection Pair Inflation Flashloan Exploit
32%NOON Pool Drain via Public transfer
32%UpSwing sell-pressure accounting can be inflated with transfer-plus-skim loops
32%NFTX Doodles collateral accounting flaw enables flash-loan ETH extraction
31%SorraV2 staking withdraw bug enables repeated SOR reward drain
31%Indexed Finance DEFI5 gulp/reindex bug enables SUSHI flash-swap drain
30%Root Cause Analysis
QUATERNION Pair-Rebase Accounting Drift Enables Permissionless Drain
1. Incident Overview TL;DR
QUATERNION (0xc9fa8f4cfd11559b50c5c7f6672b9eea2757e1bd) integrated a rebasing token with a Uniswap V2 pair (0xa8208da95869060cfd40a23eb11f2158639c829b) using two incompatible balance views. The pair-facing ERC-20 view returned a stale nominal counter, while the actual transfer path debited gon-backed balances derived from _gonBalances / _gonsPerFragment. At block 16430212, that mismatch let any unprivileged attacker compute that the pair could only transfer 10732339951003446152392961 QTN even though balanceOf(pair) still reported 25666645898426115635900466 QTN.
The attacker EOA 0x88a2386e7ec97ad1e7a72176a66b6d0711ae3527 used orchestrator contract 0xa33c965ca6d3bdc42bdb23a79081757090eb7700 to buy the pair's true transferable QTN balance with 2.1911450261043878 WETH in tx 0x352320bf89db8a117b87b28213bf936e58db848cde788571fe9164a1aac7b68b, then repeatedly cycled QTN through 25 helper contracts using permissionless skim so each pair-origin transfer triggered another rebase. The final realization tx 0xd78380d1caaf494338d2c5d9093ebee7dcea2a2b804ceb7714dad899bae65be1 sold 147189504344603535877565439 QTN back into the pair for 4736306819128998852 WETH. The pair lost 2545161793024611052 WETH, and the attacker realized a conservative post-gas profit of 1662656464355209396 wei.
2. Key Background
QUATERNION stores balances internally in gons and exposes both _gonBalances(address) and _gonsPerFragment() as public getters. That means every account's true transfer balance is publicly reconstructible as _gonBalances(account) / _gonsPerFragment().
The token also keeps a pair-specific counter, uniswapV2PairAmount, and uses it only when someone calls balanceOf(pair). Uniswap V2 reserve accounting and skim(address) rely on the token's ERC-20 balanceOf, not on QUATERNION's internal gon ledger. This difference matters because QUATERNION rebases by changing _gonsPerFragment, which changes every gon-backed fragment balance without automatically reconciling uniswapV2PairAmount.
QUATERNION also records _buyInfo[to] on pair-origin transfers and enforces a 5 minutes cooldown before a non-pair sender can transfer again. That is why the historical attacker alternated distribute and collect transactions across many blocks instead of executing the entire loop in a single transaction.
The exploit path remained permissionless at the relevant block. The owner had already renounced ownership, _live was true, _percentForTxLimit had been raised to 100, and _percentForRebase remained 5. The attack therefore required no private keys, no privileged roles, and no interaction with hidden state.
3. Vulnerability Analysis & Root Cause Summary
The core invariant for a rebasing token paired on Uniswap V2 is simple: the balance returned by balanceOf(pair) must remain equal to the amount the token can actually debit from the pair during transfers. QUATERNION violated that invariant by mixing a stale nominal pair counter with live gon accounting.
Origin: QUATERNION verified source.
function balanceOf(address account) public view override returns (uint256) {
if(account == uniswapV2Pair)
return uniswapV2PairAmount;
return _gonBalances[account].div(_gonsPerFragment);
}
function rebasePlus(uint256 _amount) private {
_totalSupply = _totalSupply.add(_amount.div(5));
_gonsPerFragment = TOTAL_GONS.div(_totalSupply);
}
function _tokenTransfer(address from, address to, uint256 amount, uint256 taxFee) internal {
if(to == uniswapV2Pair)
uniswapV2PairAmount = uniswapV2PairAmount.add(amount);
else if(from == uniswapV2Pair)
uniswapV2PairAmount = uniswapV2PairAmount.sub(amount);
uint256 burnAmount = amount.mul(taxFee).div(100);
uint256 transferAmount = amount.sub(burnAmount);
uint256 gonTotalValue = amount.mul(_gonsPerFragment);
uint256 gonValue = transferAmount.mul(_gonsPerFragment);
_gonBalances[from] = _gonBalances[from].sub(gonTotalValue);
_gonBalances[to] = _gonBalances[to].add(gonValue);
}
This design breaks as soon as a rebase changes _gonsPerFragment. The real transferable balance of the pair changes immediately because _gonBalances[pair] / _gonsPerFragment changes, but uniswapV2PairAmount does not. QUATERNION then adds a second issue: pair-origin transfers to small recipients call rebasePlus(amount), so an attacker can intentionally trigger rebases by making the pair transfer to many fresh helper addresses. Because the helper balances also participate in the rebase, the attacker compounds their holdings across repeated distribute and collect phases. The exploit is therefore a deterministic accounting bug in QUATERNION, not a pricing anomaly or privileged manipulation.
4. Detailed Root Cause Analysis
At block 16430212, the public pair state was already broken. balanceOf(pair) returned 25666645898426115635900466 QTN, but the public gon state implied only 10732339951003446152392961 transferable QTN. That gap existed because prior rebases had changed _gonsPerFragment while leaving uniswapV2PairAmount behind.
The rebase trigger sits in the pair-origin branch of _transfer.
Origin: QUATERNION verified source.
else {
if(!_live)
blacklist[to] = true;
require(balanceOf(to) <= txLimitAmount, "ERC20: current balance exceeds the max limit.");
_buyInfo[to] = now;
_tokenTransfer(from, to, amount, 0);
uint256 rebaseLimitAmount = _totalSupply.mul(_percentForRebase).div(100);
uint256 currentBalance = balanceOf(to);
uint256 newBalance = currentBalance.add(amount);
if(currentBalance < rebaseLimitAmount && newBalance < rebaseLimitAmount) {
rebasePlus(amount);
}
}
This means any pair-origin transfer to a sufficiently small recipient mints a rebase by increasing _totalSupply and decreasing _gonsPerFragment. Since _tokenTransfer debits and credits gons, all existing gon holders, including the attacker and the helper contracts, get larger fragment balances after each rebase. The pair's stale nominal balance view persists, so Uniswap's reserve accounting continues to misprice the pool while the attacker compounds inventory.
The initial attacker buy proves the pair-balance mismatch was directly monetizable.
Origin: initial attacker buy trace.
WETH9::transfer(UniswapV2Pair, 2191145026104387800)
UniswapV2Pair::swap(
0,
10732339951003446152392961,
0xA33c965Ca6D3bdc42BDb23a79081757090eb7700,
0x
)
emit Transfer(
from: UniswapV2Pair,
to: 0xA33c965Ca6D3bdc42BDb23a79081757090eb7700,
value: 10732339951003446152392961
)
The attacker paid only 2.1911450261043878 WETH for the pair's real gon-backed balance, not for the larger stale nominal balance shown to Uniswap. That bought the attacker enough QTN to start the compounding phase.
The distribute phase then turned skim(address) into a permissionless rebase primitive.
Origin: distribute-phase trace.
UniswapV2Pair::skim(0x89425431C2971BE618658Ef8c155E250b1B8b125)
emit Transfer(from: UniswapV2Pair, to: 0x89425431..., value: 5109705265130400336142756)
UniswapV2Pair::skim(0x672Fa16Fa926FBfF41cA2c80B931A3c28818ba99)
emit Transfer(from: UniswapV2Pair, to: 0x672Fa16F..., value: 5135253791456052337823470)
Each skim caused the pair to send QTN to a fresh helper, so QUATERNION executed the pair-origin branch above and called rebasePlus(amount). Because the helpers were all below the 5% threshold, the attacker could repeat this 25 times in one distribute tx. The collected transaction history shows that this distribute/collect alternation happened across many blocks and many attacker-crafted transactions, not just once.
The collect phase returned the rebased balances to the orchestrator.
Origin: collect-phase trace.
0x89425431C2971BE618658Ef8c155E250b1B8b125::transferBack()
QUATERNION::transfer(0xA33c965Ca6D3bdc42BDb23a79081757090eb7700, 5109705265130400336142757)
emit Transfer(from: 0x89425431..., to: 0xA33c965C..., value: 5109705265130400336142757)
That flow matters because it confirms the helpers were only temporary rebase receivers. The attacker did not need any privileged contract code or private state; the helper behavior was simply "receive pair-origin QTN, wait out the cooldown, transfer the larger balance back."
The final tx converted the inflated QTN position into WETH.
Origin: final swap trace.
0xA33c965Ca6D3bdc42BDb23a79081757090eb7700::swapOut()
emit Transfer(from: 0xA33c965C..., to: UniswapV2Pair, value: 147189504344603535877565439)
WETH9::transfer(0x88a2386E7ec97aD1E7a72176A66B6d0711aE3527, 4736306819128998852)
emit Sync(reserve0: 482148896801030497, reserve1: 162123810292026205361072944)
After the dump, the pair's WETH reserve fell from 3.027310689825641549 WETH at the start of the attack sequence to 0.482148896801030497 WETH. That is the concrete value-transfer breakpoint: stale pair accounting let the attacker acquire underpriced QTN, permissionless rebases multiplied that QTN, and the final swap removed real WETH from the pool.
5. Adversary Flow Analysis
The attacker flow was a multi-transaction loop, not a one-shot swap:
- Tx
0x352320bf89db8a117b87b28213bf936e58db848cde788571fe9164a1aac7b68bfunded the orchestrator with2.1911450261043878WETH, bought the pair's true transferable QTN balance, and deployed 25 helper contracts. - The attacker then alternated distribute and collect transactions across many blocks to respect the cooldown on helper transfers.
- A representative collect transaction,
0x37cb8626e45f0749296ef080acb218e5ccc7efb2ae4d39c952566dc378ca1c4c, called each helper'stransferBack()function to return rebased balances to the orchestrator. - A representative later distribute transaction,
0xfde10ad92566f369b23ed5135289630b7a6453887c77088794552c2a3d1ce8b7, transferred QTN into the pair and calledskim(helper)25 times so each pair-origin transfer hit the rebase path again. - Tx
0xd78380d1caaf494338d2c5d9093ebee7dcea2a2b804ceb7714dad899bae65be1transferred147189504344603535877565439QTN into the pair and swapped out4736306819128998852WETH.
The attacker transaction history shows 46 attacker-crafted EOA transactions in the wider sequence. The two middle hashes above are representative checkpoints from the repeated loop, and the repeated pattern is what compounded the position from the initial 10732339951003446152392961 QTN buy to the final 147189504344603535877565439 QTN dump.
6. Impact & Losses
The direct pool loss measured in WETH was 2545161793024611052, or 2.545161793024611052 WETH. The attacker's conservative lower-bound profit, after gas and counting only the initial seed/final realization path, was 1662656464355209396 wei. That lower bound intentionally excludes an additional router transaction at nonce 19, so it understates, rather than overstates, the realized gain.
The loss mechanism was structural. QUATERNION let Uniswap price against a nominal pair balance that no longer matched the amount the token could transfer. Once the attacker amplified their position through permissionless rebases, the final dump extracted almost all of the pair's WETH liquidity, leaving only 0.482148896801030497 WETH in reserve.
7. References
- Victim token: QUATERNION
0xc9fa8f4cfd11559b50c5c7f6672b9eea2757e1bd - Victim pool: QTN/WETH Uniswap V2 pair
0xa8208da95869060cfd40a23eb11f2158639c829b - Attacker EOA:
0x88a2386e7ec97ad1e7a72176a66b6d0711ae3527 - Attacker orchestrator:
0xa33c965ca6d3bdc42bdb23a79081757090eb7700 - Representative helper:
0x89425431c2971be618658ef8c155e250b1b8b125 - Initial buy and helper deployment tx:
0x352320bf89db8a117b87b28213bf936e58db848cde788571fe9164a1aac7b68b - Representative collect tx:
0x37cb8626e45f0749296ef080acb218e5ccc7efb2ae4d39c952566dc378ca1c4c - Representative distribute tx:
0xfde10ad92566f369b23ed5135289630b7a6453887c77088794552c2a3d1ce8b7 - Final realization tx:
0xd78380d1caaf494338d2c5d9093ebee7dcea2a2b804ceb7714dad899bae65be1