A full-stack deep dive into how fee logic is implemented, abused, hidden, detected, and modeled with machine learning in modern ERC-20 style tokens.
- Introduction
- Background: How Token Fees Work
- Execution Flow of Fee Logic
- Tax Abuse: Formal Definition
- Attack Taxonomy: Types of Tax Abuse
- Solidity Deep Dive: How Attackers Implement Tax Abuse
- Attack Timelines: From Launch to Rug
- Mathematics of Tax Abuse
- Defensive Engineering: Manual Audit Checklist
- Machine Learning for Tax Abuse Detection
- Behavioral & On-Chain Signals
- Putting It All Together
- Conclusion
Token “taxes”, transaction fees embedded into ERC-20 style smart contracts, started as a simple economic mechanism:
- Charge a small fee on each transfer
- Use it to reward holders, fund development, or add liquidity
But in the hands of malicious developers, these fees turn into a powerful, stealthy weapon.
They can be used to:
- silently siphon value from traders over time
- trap users in honeypots where selling is suicidal
- disguise rugpulls as “tokenomics”
- hide behind complex, obfuscated fee logic
This article takes a scientific, engineering, and ML-driven look at token tax abuse:
- how it works at the Solidity level
- how attackers design their logic
- how to model the damage mathematically
- and how to detect it using machine learning and static analysis.
Many projects implement fees for legitimate reasons:
- Liquidity fees: a portion of each trade is used to add liquidity to the pool.
- Reflection/redistribution: fees are redistributed to holders.
- Marketing & development: a portion funds core operations or marketing.
- Buyback & burn: fees are used to burn supply and support price.
So fees themselves are not evil. The danger comes from:
- how fee parameters can be changed
- who controls them
- and how they interact with DEX trading behavior.
In most ERC-20 tokens with taxes, fee logic lives in or around the _transfer function.
Typical pattern:
function _transfer(
address from,
address to,
uint256 amount
) internal virtual {
uint256 feeAmount = calculateFee(from, to, amount);
uint256 amountAfterFee = amount - feeAmount;
_balances[from] -= amount;
_balances[to] += amountAfterFee;
// route the fee to some wallet or logic
_takeFee(feeAmount);
emit Transfer(from, to, amountAfterFee);
}The core “hook” for tax abuse is:
calculateFee(...)_takeFee(...)- and any owner-controlled hooks around them.
A basic, non-taxed ERC-20 _transfer is simple:
function _transfer(address from, address to, uint256 amount) internal virtual {
require(from != address(0) && to != address(0), "zero address");
require(_balances[from] >= amount, "insufficient balance");
_balances[from] -= amount;
_balances[to] += amount;
emit Transfer(from, to, amount);
}With taxes, it becomes:
function _transfer(address from, address to, uint256 amount) internal virtual {
require(from != address(0) && to != address(0), "zero address");
require(_balances[from] >= amount, "insufficient balance");
uint256 feeAmount = _calculateFee(from, to, amount);
uint256 netAmount = amount - feeAmount;
_balances[from] -= amount;
_balances[to] += netAmount;
if (feeAmount > 0) {
_takeFee(from, feeAmount);
}
emit Transfer(from, to, netAmount);
}This flexible structure is exactly what attackers exploit.
On Uniswap-like DEXes, swaps typically go:
- user ↔ router contract
- router ↔ pair contract (liquidity pool)
- token ↔ WETH (or other base pair)
When a user buys or sells a token:
- User calls
swapExactETHForTokensor similar on the router - Router sends tokens to the user / LP pair
- The token’s
_transferexecutes between router/pair and user
The token contract controls:
- how much is actually received or sent after fees
- who receives the fees
- whether the router or pair is treated specially
A malicious token can:
- treat buys differently from sells
- apply different taxes based on whether
fromortois the pair address - completely devastate sellers by taxing sell routes heavily.
flowchart LR
A[User Wallet] -->|swap on DEX| B[DEX Router]
B --> C[Liquidity Pair]
C -->|Token transfer| D[Token Contract _transfer]
D -->|Apply tax logic| E[Fee Destination Wallet]
D -->|Net amount| A2[User / Buyer / Seller]
Key observation:
Even though users interact with the DEX router, all the power is inside the token contract’s _transfer logic.
We’ll define token tax abuse as:
Any design of transaction fee logic in a token contract that is intentionally structured to mislead, trap, or extract value from users beyond what is transparently communicated, especially via hidden, dynamic, owner-controlled, or context-sensitive fee mechanisms.
Characteristics:
- Hidden or non-obvious high taxes
- Owner-controlled tax manipulation after launch
- Conditional “trigger events” (time, block, price, volume)
- Whitelists or blacklists that change fees based on address
- Unreasonably high caps (
sellTaxup to 100%)
Here’s a practical taxonomy you can use while auditing.
Start with:
- low buy/sell tax at launch
- after enough buyers enter → increase sell tax to extreme levels
Example pattern:
uint256 public sellFee = 2;
function setSellFee(uint256 fee) external onlyOwner {
require(fee <= 100, "too high"); // still allows absurd values
sellFee = fee;
}Attackers call setSellFee(80) or setSellFee(99) after launch.
2. Hidden Sell-Only Taxes
Code appears to show “2% tax”, but real logic has a special path for sells.
function _calculateFee(address from, address to, uint256 amount) internal view returns (uint256) {
uint256 fee = buyFee;
if (isSell(from, to)) {
fee = sellFee * multiplier; // multiplier can be 5x, 10x, etc.
}
return (amount * fee) / 100;
}
function isSell(address from, address to) internal view returns (bool) {
return to == uniswapV2Pair; // treat sends to LP as "sell"
}Attackers set multiplier high after investors are in.
Owner or certain privileged wallets can bypass the fee logic entirely.
mapping(address => bool) public isWhitelisted;
function _transfer(address from, address to, uint256 amount) internal override {
if (isWhitelisted[from] || isWhitelisted[to]) {
super._transfer(from, to, amount);
return;
}
uint256 taxed = _taxAmount(amount);
...
}Attackers:
- whitelist themselves
- tax everyone else brutally.
Combining max transaction size with high tax makes selling nearly impossible.
uint256 public maxTxAmount;
function _transfer(address from, address to, uint256 amount) internal override {
require(amount <= maxTxAmount, "Too big");
uint256 taxAmount = amount * sellFee / 100;
...
}If:
maxTxAmount = 1% of your balancesellFee = 25%
→ Users must sell in many small chunks, losing heavily to fees each time.
Fee logic changes after some time/block conditions:
uint256 public launchBlock;
function _calculateFee(...) internal view returns (uint256) {
if (block.number < launchBlock + 5) {
return amount * 20 / 100; // anti-bot
} else if (block.number > launchBlock + 10000) {
return amount * 90 / 100; // rugpull mode
}
return amount * 2 / 100;
}This looks like anti-bot but becomes a time-delayed rugpull.
Fees routed directly or indirectly to dev-controlled wallets or LP removal addresses.
address public devWallet;
function _takeFee(uint256 amount) internal {
_balances[devWallet] += amount;
}Or even more sneaky, routing through intermediate contracts that eventually send funds to devs.
Reflection tokens use complex internal accounting. Scammers hide abusive fee logic inside this complexity:
- reflection supply vs total supply
- weird mapping naming
- fee logic split across 5+ functions
Unless the auditor understands reflection math, it’s easy to hide tax abuse here.
Red flags:
uint256 public buyTax;
uint256 public sellTax;
uint256 public marketingTax;
uint256 public devFee;
uint256 public extraSellTax; // suspicious extras
function setBuyTax(uint256 _tax) external onlyOwner { buyTax = _tax; }
function setSellTax(uint256 _tax) external onlyOwner { sellTax = _tax; }
function setFees(...) external onlyOwner { ... } // big multi-param setterLarge numbers of fee variables often indicate:
- multiple hidden tax channels
- complex fee routing to mask dev extraction.
Modifiers can contain fee-related conditions:
modifier tradingOpen() {
require(_tradingEnabled, "Trading not enabled");
_;
}Or worse:
modifier taxed(address from, address to, uint256 amount) {
uint256 tax = _calculateFee(from, to, amount);
_;
_applyTax(from, tax);
}Connected with:
function _transfer(address from, address to, uint256 amount)
internal
tradingOpen
taxed(from, to, amount)
{
...
}Fee logic hides inside modifiers, not direct _transfer.
Common obfuscation patterns:
_transfercalls_tokenTransfer, which calls_executeTransfer, which calls_takeFeeonly in certain branches.- Using meaningless names:
function fireRocket(...) internal { ... } // actually does fee routing- Splitting fee math across several functions:
function _preTransferCheck(...) internal { ... }
function _postTransferHook(...) internal { ... }One of them might sneak in tax logic.
Most tax-abuse scams follow a time-based playbook.
-
Launch Phase
- Owner sets low fee: 1–5%
- Hype, Telegram, influencers
- Contract looks “normal” on quick scan
-
Accumulation Phase
- Investors buy in
- Liquidity grows
- Early trades look OK → confidence rises
-
Trigger Phase
- Owner calls
setSellFee(80) - Or time-based logic automatically increases taxes
- Or trading conditions change (thresholds hit)
- Owner calls
-
Harvest Phase
- Sellers get destroyed by high taxes
- Tax flows to dev wallets
- Dev slowly dumps or pulls tax funds
-
Exit Phase
- Dev removes LP / dumps collected tokens
- Token price collapses
- Social channels go silent
flowchart TD
A([Launch<br/>Contract deployed<br/>low tax, marketing push])
B([Accumulation<br/>Investors buy<br/>normal trading, low fees])
C([Trigger<br/>Owner flips fee switch<br/>sellTax = 80–99%])
D([Harvest<br/>Users try to sell<br/>heavy tax loss])
E([Exit<br/>Dev exits<br/>project dies])
A --> B --> C --> D --> E
Let’s model the damage.
Assume:
Then:
If tax is applied only on sells, a user who:
If a max-tx system forces a user to sell in ( n ) chunks, each with tax ( t ):
So a flat tax is linear.
But: if t increases with each sell or based on conditions, effective loss can be super-linear in practice.
Example: First sells: 10% tax. Later sells: 40% tax due to dynamic fee escalators.
If a token routes all fees to a dev wallet which then sells those tokens for base currency, then:
- User sells pay tax → dev accumulates tokens
- Dev’s sells further push price down → amplifies user loss
High tax + many traders = huge economic drain.
Here’s a practical checklist you can use when auditing token fee logic.
- Search for
fee,tax,buyFee,sellFee,marketingFee,devFee. - Confirm all fee variables are visible and not misleadingly named.
- Check maximum allowed tax values (
require(fee <= X)). - Inspect
setFee,setTax,setBuyFee,setSellFeefunctions. - Verify who can call these functions (onlyOwner? multi-sig?).
- Ensure there is no function that sets fees near 100%.
- Find all implementations of
_transfer. - Track calls to
_calculateFee,_taxAmount,_takeFee, etc. - Confirm that every token movement goes through the same logic (no “backdoor transfers”).
- Check for
ifconditions specific to DEX pair addresses. - Verify buys and sells don’t get wildly different treatments.
- Search for
whitelist,blacklist,isWhitelisted,canTrade,blocked. - If whitelists exist, check if they bypass fee logic.
- Confirm there is no centrally controlled blacklist that can block sellers selectively.
- Look for
require(!blacklisted[from])in_transfer.
- Search for
block.number,block.timestamp,launchBlock,launchTime. - Check for conditions that dramatically change tax after certain blocks.
- Verify that “anti-bot” measures cannot be reactivated later.
- Watch out for thresholds based on
totalSupply,balanceOf(pair), or price.
- Inspect
owner(),onlyOwner,renounceOwnership. - Confirm whether ownership is truly renounced (or just appears so).
- Look for proxy patterns that allow reintroducing owner control.
- Check if fee wallets can be changed by the owner.
- Trace where fee tokens go: marketing wallet, dev wallet, burn address, LP, etc.
- Verify that “burn” addresses are actually
0x000...0or dead, not dev-controlled. - If fees go to a contract, inspect that contract too.
- Verify no “recoverTokens” or “sweep” functions drain those wallets.
- Look for unnecessary layers:
_transfer → _tokenTransfer → _execute → _afterTransfer. - Check modifiers used on
_transferor public transfer functions. - Be suspicious of unusual naming like
fireRocket,blessHolders,rebasePowertied into_transfer. - Count how many different fee types are combined (reflection, LP, dev, marketing, etc.) — more complexity, more hiding potential.
- Simulate a buy and sell with different fee settings.
- Verify behavior when owner changes fee variables.
- Check what happens near launch conditions (early blocks).
- Simulate sells from different accounts (whitelisted vs normal).
- Confirm max-tx + fee combinations don’t trap sellers.
- Compare whitepaper/website claims vs actual code.
- Ensure claimed fee ranges match contract possibilities.
- Check if “renounced ownership” is truly implemented.
- Be wary when documentation is vague about fees and control.
This checklist alone can drastically reduce exposure to tax-abuse contracts.
Beyond manual audits, ML can scale detection over thousands of contracts.
At token level, you can build a feature vector like:
n_lines, total lines of coden_public, count ofpublicn_external, count ofexternalhas_set_fee, binary (1 if any fee setter exists)has_dynamic_fee, any time-conditional fee logichas_whitelist, presence of whitelist logichas_blacklisthas_trading_lock, presence of flags liketradingOpenhas_max_tx, presence ofmaxTxAmountfee_to_owner_flow, heuristics about fee routing dest
At deployer level, you can aggregate:
n_contractsn_high_risk_tokensn_safe_tokensfrac_rugpullavg_tax_complexity
Typical choice: RandomForestClassifier or Gradient Boosting.
- Handles mixed binary + numeric features
- Robust to noisy labels
- Provides feature importance
The target label might be:
1= high tax abuse risk0= low tax abuse risk
You can bootstrap labels via:
- manual auditing of a subset
- rough heuristics (“sellTax > 50% → 1”)
- public scam databases (if available)
Model outputs:
Convert to human-friendly score:
Then bucket:
0–20→ Low21–60→ Medium61–100→ High
You can expose:
risk_scorerisk_levellabel(safe,suspicious,tax_abuse_candidate)
To avoid “black box” fears, you can use SHAP values:
-
Show how each feature contributed to a contract’s risk score
-
bar charts showing:
has_set_fee+0.18has_trading_lock+0.12has_blacklist+0.09n_lines+0.02
This helps analysts and users understand why a token scored as high-risk.
flowchart LR
A[Solidity Contract Source] --> B[Static Feature Extractor]
B --> C[Feature Vector]
C --> D[Trained ML Model]
D --> E[Risk Score & Label]
E --> F[Human Analyst / UI / API]
Optional extension:
- add a “Deployer Aggregator” node between C and D
- combine token-level and deployer-level features.
In addition to code-based features, you can use on-chain behavior:
- sudden spike in gas usage on sells
- rapid increase in dev wallet balances
- repeatedly rising sell tax after each wave of buys
- frequent deployments from same deployer with similar patterns
- large number of victims exiting with heavy slippage
Combining code signals + behavioral signals + ML produces the strongest detection system.
We can now view token tax abuse detection as a layered system:
-
Static code analysis
- Extract patterns and features
- Immediate detection of extreme or structural abuse
-
Machine learning inference
- Learn subtle combinations of features
- Predict high-risk tokens even if patterns are partially obfuscated
-
Deployer reputation modeling
- Capture repeated offenders
- Penalize addresses that consistently produce abusive tokens
-
On-chain behavioral analysis
- Watch live transaction behavior
- Confirm suspicious patterns in practice
-
On-chain registries & transparency
- Publish risk scores so dApps, wallets, and explorers can show warnings
- Build a decentralized trust layer
Token tax abuse is not just a code-level trick, it’s an economic weapon crafted through:
- carefully designed fee logic
- dynamic and conditional tax changes
- whitelists, blacklists, and trading locks
- and ultimately, user deception.
However, it’s not unstoppable.
By combining:
- deep Solidity understanding
- systematic audit checklists
- rigorous mathematical modeling
- and machine learning over on-chain data
we can:
- flag abusive tokens early
- identify malicious deployers
- and build smarter tools that protect users.
If you’re building Web3 security tools, this domain token tax abuse detection, is one of the most impactful areas you can work in.