TomInu Reflection Pair Inflation Flashloan Exploit
Exploit Transactions
0x6200bf5c43c214caa1177c3676293442059b4f39eb5dbae6cfd4e6ad16305668Victim Addresses
0x2d0e64b6bf13660a4c0de42a0b88144a7c10991fEthereum0xb835752feb00c278484c464b697e03b03c53e11bEthereumLoss Breakdown
Similar Incidents
SilicaPools decimal-manipulation bug drains WBTC flashloan collateral
33%NOON Pool Drain via Public transfer
33%SchnoodleV9 reflection allowance bug drains SNOOD/WETH liquidity
33%QUATERNION Pair-Rebase Accounting Drift Enables Permissionless Drain
32%SBR reserve desynchronization exploit drains WETH from UniswapV2 pair
31%FIREDRAKE Reflection Drain on PancakeSwap
31%Root Cause Analysis
TomInu Reflection Pair Inflation Flashloan Exploit
1. Incident Overview TL;DR
On Ethereum mainnet block 16489409, transaction 0x6200bf5c43c214caa1177c3676293442059b4f39eb5dbae6cfd4e6ad16305668 turned a reflection-accounting bug in TomInu into a permissionless reserve drain. EOA 0x14d8ada7a0ba91f59dc0cb97c8f44f1d177c2195 called helper contract 0xdb2d869ac23715af204093e933f5eb57f2dc12a9, which borrowed WETH from the Balancer Vault, bought TomInu from the TomInu/WETH Uniswap V2 pair at 0xb835752feb00c278484c464b697e03b03c53e11b, called deliver() twice, used skim() to extract the reflection-created surplus, and then used swap() to pull nearly all WETH from the pair.
The root cause is a deterministic accounting flaw in TomInu. TomInu leaves its Uniswap pair inside the reflection set while exposing public deliver(uint256). Because deliver() reduces _rTotal, any non-excluded holder can lower the global reflection rate; since the pair is not excluded, balanceOf(pair) rises without any real token transfer into the pair. The attacker turned that artificial balance increase into transferable tokens via skim() and then into WETH via swap().
2. Key Background
TomInu is a reflection token. For non-excluded accounts, the public balanceOf() value is not stored directly; it is derived from reflected ownership divided by a global rate. That matters because changing the global rate changes the apparent token balances of every non-excluded holder, including AMM pairs.
The relevant TomInu source collected for 0x2d0e64b6bf13660a4c0de42a0b88144a7c10991f shows the core mechanism:
constructor (address payable teamWalletAddress, address payable marketingWalletAddress) public {
_rOwned[_msgSender()] = _rTotal;
IUniswapV2Router02 _uniswapV2Router = IUniswapV2Router02(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D);
uniswapV2Pair = IUniswapV2Factory(_uniswapV2Router.factory())
.createPair(address(this), _uniswapV2Router.WETH());
uniswapV2Router = _uniswapV2Router;
}
function balanceOf(address account) public view override returns (uint256) {
if (_isExcluded[account]) return _tOwned[account];
return tokenFromReflection(_rOwned[account]);
}
function deliver(uint256 tAmount) public {
address sender = _msgSender();
require(!_isExcluded[sender], "Excluded addresses cannot call this function");
(uint256 rAmount,,,,,) = _getValues(tAmount);
_rOwned[sender] = _rOwned[sender].sub(rAmount);
_rTotal = _rTotal.sub(rAmount);
_tFeeTotal = _tFeeTotal.add(tAmount);
}
function tokenFromReflection(uint256 rAmount) public view returns(uint256) {
uint256 currentRate = _getRate();
return rAmount.div(currentRate);
}
function _getRate() private view returns(uint256) {
(uint256 rSupply, uint256 tSupply) = _getCurrentSupply();
return rSupply.div(tSupply);
}
On the AMM side, Uniswap V2 pairs keep internal stored reserves and separately hold live ERC-20 balances. skim(address) transfers any live token balance above stored reserves, and swap() infers token input from the post-transfer live balances relative to stored reserves. That pair behavior is not a bug by itself; the exploit works because TomInu lets public callers distort the pair's live balanceOf() reading.
3. Vulnerability Analysis & Root Cause Summary
This incident is an ATTACK case, not an MEV-only opportunity. The exploited flaw sits in TomInu's reflection design, specifically the combination of a public deliver(uint256) entrypoint and a Uniswap pair that was never excluded from reflections. The violated invariant is simple: a third party must not be able to increase an AMM pair's effective token balance relative to stored reserves unless the pair actually receives tokens. TomInu breaks that invariant because deliver() shrinks _rTotal, which lowers the global reflection rate and increases balanceOf(pair) while the pair's reflected ownership stays constant. Once that artificial surplus appears, standard Uniswap V2 pair functions do the rest: skim() converts the fake surplus into attacker-owned tokens, and a later swap() treats the inflated pair balance as genuine token input and releases WETH. The flash loan only supplied temporary WETH for the first buy; it was not the root cause.
4. Detailed Root Cause Analysis
4.1 Pre-state and exploit conditions
The ACT pre-state was Ethereum mainnet immediately before the seed transaction in block 16489409. The necessary public conditions were:
- TomInu pair
0xb835752feb00c278484c464b697e03b03c53e11bremained a non-excluded reflection holder. - Trading was already enabled, so anyone could buy TomInu through the public Uniswap V2 router.
- The pair still held meaningful WETH liquidity that could be drained after balance inflation.
- Balancer flash loans were permissionlessly available.
The report in root_cause.json states these same exploit conditions, and the collected trace confirms they were all true during the seed transaction.
4.2 Code-level breakpoint
TomInu exposes an owner-only excludeAccount(address) function, but the constructor never excludes the newly created pair. The result is that the pair remains governed by reflected accounting:
function excludeAccount(address account) external onlyOwner() {
require(!_isExcluded[account], "Account is already excluded");
if(_rOwned[account] > 0) {
_tOwned[account] = tokenFromReflection(_rOwned[account]);
}
_isExcluded[account] = true;
_excluded.push(account);
}
Because the pair was left out of _excluded, any public caller with TomInu balance could call deliver(), reduce _rTotal, and thereby reduce currentRate. The pair's _rOwned value stayed unchanged, so tokenFromReflection(_rOwned[pair]) jumped upward without a token transfer. That is the exact code-level breakpoint identified by the auditor and independently validated here.
4.3 On-chain execution evidence
The seed trace shows the exploit sequence directly:
0x7a250d...::swapExactTokensForTokensSupportingFeeOnTransferTokens(
104850000000000000000,
0,
[WETH, TomInu],
0xDb2d869ac23715af204093e933f5EB57F2DC12a9,
1674717047
)
0xb835752f...::getReserves() -> (316871513264115731249, 126994561461014981232, ...)
0x2d0E64B6...::deliver(1465904852700232013011)
0xb835752f...::skim(0xDb2d869ac23715af204093e933f5EB57F2DC12a9)
0x2d0E64B6...::balanceOf(0xb835752f...) -> 2050642424158542203032
0x2d0E64B6...::transfer(..., 1733770910894426471783)
0x2d0E64B6...::deliver(1733769909104888397946)
0xb835752f...::swap(0, 126990751624171150782, 0xDb2d869ac23715af204093e933f5EB57F2DC12a9, 0x)
0x2d0E64B6...::balanceOf(0xb835752f...) -> 11191855315120216048899805
WETH9::balanceOf(0xb835752f...) -> 3809836843830450
Those values show the invariant failure quantitatively:
- After the initial buy, stored reserves were
316871513264115731249TINU and126994561461014981232wei WETH. - After the first
deliver(),TomInu.balanceOf(pair)jumped to2050642424158542203032TINU with no real TomInu transfer into the pair. skim()transferred the resulting fake surplus of1733770910894426471783TINU out of the pair.- After the second
deliver(),swap()released126990751624171150782wei WETH and left the pair with only3809836843830450wei WETH and an impossible11191855315120216048899805TINU balance, far above the total supply.
4.4 Why this is ACT
This opportunity was realizable by any unprivileged actor using only public information and public contract entrypoints:
BalancerVault.flashLoan(...)is public.UniswapV2Router.swapExactTokensForTokensSupportingFeeOnTransferTokens(...)is public.TomInu.deliver(uint256)is public for any non-excluded holder.UniswapV2Pair.skim(address)andUniswapV2Pair.swap(...)are public.
No private key compromise, privileged role, hidden calldata, or attacker-side proprietary artifact was required. The adversary model therefore fits the ACT definition exactly.
4.5 Safety principles violated
The incident breaks two protocol-design principles that are explicit in the auditor output and supported by the code and trace evidence:
- Reflection-accounting changes must not alter AMM pair balances unless the pair is deliberately isolated from the reflection set.
- Externally callable supply-affecting functions must preserve conservation of balances relative to external integrations such as AMMs.
5. Adversary Flow Analysis
- EOA
0x14d8ada7a0ba91f59dc0cb97c8f44f1d177c2195called helper contract0xdb2d869ac23715af204093e933f5eb57f2dc12a9in transaction0x6200bf5c43c214caa1177c3676293442059b4f39eb5dbae6cfd4e6ad16305668. - The helper borrowed WETH from Balancer Vault
0xba12222222228d8ba445958a75a0704d566bf2c8and swapped104.85WETH into TomInu through Uniswap V2 router0x7a250d5630b4cf539739df2c5dacb4c659f2488d, acquiring1465904852700232013011TINU. - The helper called
TomInu.deliver(1465904852700232013011). Because the pair was still in the reflection set, the pair's apparent TINU balance rose from the stored reserve of316871513264115731249to2050642424158542203032. - The helper called
skim()on the pair and received the artificial surplus:1733770910894426471783TINU. It then calleddeliver()again with the skimmed balance, inflating the pair's apparent TINU balance even further. - The helper called
swap()on the pair and received126990751624171150782wei WETH, repaid the flash loan, withdrew22.140751624171150782WETH to ETH, made a builder-side payment observed in the native balance diff for0xdafea492d9c6733ae3d56b7ed1adb60692c98bc5, and selfdestructed the remaining ETH back to the sender EOA.
This lifecycle matches the adversary stages recorded in root_cause.json: flash-loan funding and seed buy, reflection inflation and skim, and final reserve drain plus settlement.
6. Impact & Losses
The measurable victim-side loss was 22.140751624171150782 WETH from the TomInu/WETH pair. The pair's actual WETH balance fell from 22.144561461014981232 WETH before the exploit path to 0.003809836843830450 WETH after the exploit transaction, and its TomInu balance ended at 11191855315120216048899805, far above TomInu's total supply of 1733820000000000000000.
The attacker-side outcome is also deterministic:
balance_diff.jsonshows the adversary EOA's native balance increasing by22103097125490228832wei net of gas.- The incident report in
root_cause.jsonrecords the net ETH profit as22.103097125490228832. - The explicit builder-facing transfer in the trace is
0.03ETH, while the balance diff for0xdafea492d9c6733ae3d56b7ed1adb60692c98bc5shows a31280409000000000wei native increase during the transaction.
The affected public components were TomInu token 0x2d0e64b6bf13660a4c0de42a0b88144a7c10991f and the TomInu/WETH Uniswap V2 pair 0xb835752feb00c278484c464b697e03b03c53e11b.
7. References
- Seed exploit transaction:
0x6200bf5c43c214caa1177c3676293442059b4f39eb5dbae6cfd4e6ad16305668on Ethereum mainnet block16489409. - Verified TomInu source collected for
0x2d0e64b6bf13660a4c0de42a0b88144a7c10991f, especially the constructor,balanceOf(),deliver(),tokenFromReflection(),_getRate(), andexcludeAccount(). - Seed execution trace collected for the transaction above, showing the public sequence
flashLoan -> router swap -> deliver -> skim -> deliver -> swap -> repay -> withdraw. - Seed balance diff artifact for the same transaction, showing the adversary EOA's net ETH gain and the builder-side native balance increase.
- Validator challenge result confirming that the causal chain, exploit classification, and evidence quality pass independent review.