Calculated from recorded token losses using historical USD prices at the incident time.
0x6200bf5c43c214caa1177c3676293442059b4f39eb5dbae6cfd4e6ad163056680x2d0e64b6bf13660a4c0de42a0b88144a7c10991fEthereum0xb835752feb00c278484c464b697e03b03c53e11bEthereumOn 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().
TomInu is a reflection token. For non-excluded accounts, the public 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.
balanceOf()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.
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.
The ACT pre-state was Ethereum mainnet immediately before the seed transaction in block 16489409. The necessary public conditions were:
0xb835752feb00c278484c464b697e03b03c53e11b remained a non-excluded reflection holder.The report in root_cause.json states these same exploit conditions, and the collected trace confirms they were all true during the seed transaction.
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.
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:
316871513264115731249 TINU and 126994561461014981232 wei WETH.deliver(), TomInu.balanceOf(pair) jumped to 2050642424158542203032 TINU with no real TomInu transfer into the pair.skim() transferred the resulting fake surplus of 1733770910894426471783 TINU out of the pair.deliver(), swap() released 126990751624171150782 wei WETH and left the pair with only 3809836843830450 wei WETH and an impossible 11191855315120216048899805 TINU balance, far above the total supply.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) and UniswapV2Pair.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.
The incident breaks two protocol-design principles that are explicit in the auditor output and supported by the code and trace evidence:
0x14d8ada7a0ba91f59dc0cb97c8f44f1d177c2195 called helper contract 0xdb2d869ac23715af204093e933f5eb57f2dc12a9 in transaction 0x6200bf5c43c214caa1177c3676293442059b4f39eb5dbae6cfd4e6ad16305668.0xba12222222228d8ba445958a75a0704d566bf2c8 and swapped 104.85 WETH into TomInu through Uniswap V2 router 0x7a250d5630b4cf539739df2c5dacb4c659f2488d, acquiring 1465904852700232013011 TINU.TomInu.deliver(1465904852700232013011). Because the pair was still in the reflection set, the pair's apparent TINU balance rose from the stored reserve of 316871513264115731249 to 2050642424158542203032.skim() on the pair and received the artificial surplus: 1733770910894426471783 TINU. It then called deliver() again with the skimmed balance, inflating the pair's apparent TINU balance even further.swap() on the pair and received 126990751624171150782 wei WETH, repaid the flash loan, withdrew 22.140751624171150782 WETH to ETH, made a builder-side payment observed in the native balance diff for 0xdafea492d9c6733ae3d56b7ed1adb60692c98bc5, 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.
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.json shows the adversary EOA's native balance increasing by 22103097125490228832 wei net of gas.root_cause.json records the net ETH profit as 22.103097125490228832.0.03 ETH, while the balance diff for 0xdafea492d9c6733ae3d56b7ed1adb60692c98bc5 shows a 31280409000000000 wei native increase during the transaction.The affected public components were TomInu token 0x2d0e64b6bf13660a4c0de42a0b88144a7c10991f and the TomInu/WETH Uniswap V2 pair 0xb835752feb00c278484c464b697e03b03c53e11b.
0x6200bf5c43c214caa1177c3676293442059b4f39eb5dbae6cfd4e6ad16305668 on Ethereum mainnet block 16489409.0x2d0e64b6bf13660a4c0de42a0b88144a7c10991f, especially the constructor, balanceOf(), deliver(), tokenFromReflection(), _getRate(), and excludeAccount().flashLoan -> router swap -> deliver -> skim -> deliver -> swap -> repay -> withdraw.