Fee mechanisms exist in virtually every DeFi protocol. Swap fees, protocol fees, performance fees, withdrawal fees — they fund treasuries, reward liquidity providers, and sustain governance. But the same properties that make fee systems economically powerful — they touch every value flow in the protocol — make them devastating attack surfaces when they misbehave.
This article is a comprehensive technical breakdown of eight fee-related vulnerability classes. Each section includes the root-cause logic, a vulnerable Solidity pattern, the exploit path, and the hardened alternative. The piece closes with a fee system audit checklist you can apply to any protocol.
1. Fee-on-Transfer Tokens and the Assumed-Amount Gap
A canonical real-world example of this class is the SafeDollar exploit, where the attack happened because the protocol incentivized a token that has a fee on transfer — a design assumption mismatch that ultimately triggered an infinite mint.
Standard ERC-20 protocols assume that when a transferFrom(user, protocol, amount) call succeeds,
the protocol receives exactly amount tokens. Fee-on-transfer (FoT) tokens break this assumption: the
token contract silently deducts a percentage during transfer, so the receiver actually receives
amount - fee. When a protocol records amount in its internal ledger but only holds
amount - fee in its balance, a solvency gap is born.
The Vulnerable Pattern
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
interface IERC20 {
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}
contract VulnerableVault {
mapping(address => mapping(address => uint256)) public deposits; // token => user => amount
/// @notice Vulnerable: records `amount` but may receive less if FoT token
function deposit(address token, uint256 amount) external {
IERC20(token).transferFrom(msg.sender, address(this), amount);
// BUG: We record the requested amount, not the received amount.
// For a 2% FoT token and amount=1000, we record 1000 but hold 980.
deposits[token][msg.sender] += amount;
}
function withdraw(address token, uint256 amount) external {
require(deposits[token][msg.sender] >= amount, "Insufficient balance");
deposits[token][msg.sender] -= amount;
// This transfer will fail or drain other users' funds if the vault
// doesn't actually hold enough tokens.
IERC20(token).transfer(msg.sender, amount);
}
}
The Exploit
- The attacker calls
deposit(feeToken, 1000). - The protocol records
deposits[feeToken][attacker] = 1000. - Due to a 5% transfer tax, the vault only receives
950tokens. - The attacker calls
withdraw(feeToken, 1000). - The vault attempts to transfer
1000tokens but only holds950— either reverting or, if the vault has other depositors, draining their funds to cover the shortfall.
The Fix: Balance-Delta Accounting
It is recommended to find the balance of the contract before and after the transferFrom to see
how much tokens were received, and only credit what was actually received.
contract SafeVault {
mapping(address => mapping(address => uint256)) public deposits;
function deposit(address token, uint256 amount) external {
uint256 balanceBefore = IERC20(token).balanceOf(address(this));
IERC20(token).transferFrom(msg.sender, address(this), amount);
uint256 received = IERC20(token).balanceOf(address(this)) - balanceBefore;
// Credit only what was actually received, not what was requested.
deposits[token][msg.sender] += received;
}
function withdraw(address token, uint256 amount) external {
require(deposits[token][msg.sender] >= amount, "Insufficient balance");
deposits[token][msg.sender] -= amount;
uint256 balanceBefore = IERC20(token).balanceOf(address(this));
IERC20(token).transfer(msg.sender, amount);
// Optionally verify the actual sent amount to detect FoT on outbound transfers.
uint256 actualSent = balanceBefore - IERC20(token).balanceOf(address(this));
require(actualSent == amount, "FoT on withdrawal detected");
}
}
2. Protocol Fee Accounting Errors
Uranium Finance, an AMM and yield farming platform on Binance Smart Chain, was exploited for over $50 million due to a critical vulnerability in its swap logic. The root cause was a calculation error involving fee deductions within the token exchange function. While the math appeared correct at first glance, it created a flaw where a small input could return a disproportionately large output — effectively allowing an attacker to siphon funds from the liquidity pool.
The vulnerability stemmed from a miscalculation involving the constants 10000 and 1000², intended
as fee percentages.
This class of vulnerability is subtle because fee arithmetic errors are invisible in unit tests that only cover the “happy path.” The off-by-one or wrong-denominator error only manifests at boundary values or specific input sizes.
Vulnerable AMM Swap Fee Logic
contract VulnerableAMM {
uint256 public reserveA;
uint256 public reserveB;
uint256 public constant FEE_NUMERATOR = 997; // 0.3% fee
uint256 public constant FEE_DENOMINATOR = 1000;
/// @dev Simplified constant-product swap — intentionally buggy
function swapAforB(uint256 amountIn) external returns (uint256 amountOut) {
// BUG: denominator should be `reserveA * FEE_DENOMINATOR + amountIn * FEE_NUMERATOR`
// Using the wrong denominator can collapse the output check entirely.
uint256 amountInWithFee = amountIn * FEE_NUMERATOR;
uint256 numerator = amountInWithFee * reserveB;
// Missing the fee term in the denominator — miscalculates the invariant.
uint256 denominator = reserveA * FEE_DENOMINATOR; // BUG: omits amountInWithFee
amountOut = numerator / denominator;
reserveA += amountIn;
reserveB -= amountOut;
}
}
In this example, denominator is constant relative to amountIn, meaning amountOut grows
linearly instead of following the constant-product curve. An attacker can drain the pool with a
tiny input.
Correct AMM Swap Fee Logic
contract CorrectAMM {
uint256 public reserveA;
uint256 public reserveB;
function swapAforB(uint256 amountIn) external returns (uint256 amountOut) {
uint256 amountInWithFee = amountIn * 997;
uint256 numerator = amountInWithFee * reserveB;
// Correct: denominator includes the fee-adjusted input term.
uint256 denominator = (reserveA * 1000) + amountInWithFee;
amountOut = numerator / denominator;
reserveA += amountIn;
reserveB -= amountOut;
// Invariant check: k should not decrease
require(reserveA * reserveB >= (reserveA - amountIn) * (reserveB + amountOut), "Invariant violated");
}
}
Always enforce the constant-product invariant as a post-condition assertion after every swap. This turns silent accounting errors into immediate reverts.
3. Flashloan Fee Bypasses
A flash loan is an uncollateralized loan that must be borrowed and repaid within a single, atomic blockchain transaction. If the loan isn’t repaid by the transaction’s end, the entire operation reverts as if it never happened. This atomicity is legitimate — but it can also be weaponized to interact with fee systems in ways the protocol never anticipated.
In the Impermax V3 exploit, the reason for the attack was the protocol’s own logic. The protocol had certain mechanisms in place to avoid flashloan attacks, but had an edge case where an attacker could manipulate the fee parameter.
The classic flashloan fee bypass occurs when a protocol charges fees based on a snapshot of state before a flashloan, but the attacker can force the fee accounting to reference a manipulated value.
Vulnerable Flash Loan with Fee Bypass
contract VulnerableFlashLender {
mapping(address => uint256) public poolBalances;
uint256 public constant FLASH_FEE_BPS = 9; // 0.09%
/// @notice Fee is computed from the CURRENT balance — which the flashloan callback can deplete
function flashLoan(address token, uint256 amount, bytes calldata data) external {
uint256 balanceBefore = poolBalances[token];
// Transfer out
IERC20(token).transfer(msg.sender, amount);
// Callback — attacker can call back into the protocol here
IFlashBorrower(msg.sender).onFlashLoan(token, amount, data);
// BUG: Fee is calculated on `balanceBefore`, which the attacker may have already
// credited via a re-entrant deposit, inflating `balanceBefore` and zeroing the fee.
uint256 fee = (balanceBefore * FLASH_FEE_BPS) / 10000;
uint256 balanceAfter = poolBalances[token];
require(balanceAfter >= balanceBefore + fee, "Fee not paid");
}
}
Exploit Path
- Attacker calls
flashLoan(token, largeAmount). - Inside the callback, attacker calls
deposit(token, largeAmount)— inflatingpoolBalancesto a massive value. - The fee check
balanceAfter >= balanceBefore + feepasses becausebalanceBeforeis now the attacker-manipulated large value, and the actual fee token requirement is proportionally enormous — but the attacker already has the tokens from the transfer out. - Attacker withdraws on the next transaction.
The Fix: Pre-commit Fee in Calldata
contract SafeFlashLender {
mapping(address => uint256) public poolBalances;
uint256 public constant FLASH_FEE_BPS = 9;
function flashLoan(address token, uint256 amount, bytes calldata data) external {
// Lock fee amount BEFORE callback — immutable to re-entrancy
uint256 fee = (amount * FLASH_FEE_BPS) / 10000;
uint256 expectedRepayment = amount + fee;
uint256 balanceBefore = IERC20(token).balanceOf(address(this));
IERC20(token).transfer(msg.sender, amount);
// Reentrancy guard should also be applied here
IFlashBorrower(msg.sender).onFlashLoan(token, amount, fee, data);
uint256 balanceAfter = IERC20(token).balanceOf(address(this));
// Fee based on real token balance, not mutable ledger entry
require(balanceAfter >= balanceBefore + fee, "Flash loan fee not repaid");
}
}
Key mitigations: compute fees from amount (immutable within the call), verify against
balanceOf (not a manipulable mapping), and apply a non-reentrant guard on the entire function.
4. Fee Precision and Rounding Exploits
The unique challenges of DeFi stem significantly from Solidity’s lack of native support for floating-point arithmetic, leading to reliance on integer division. This limitation is critical because smart contracts demand precise execution to maintain integrity — immutability, transparency, and security.
Rounding errors have become the second most exploited vulnerability in DeFi, trailing only stolen private keys. They are also among the hardest to catch: rounding errors are notoriously difficult to detect by smart contract auditors and are routinely missed during audits, leading to instances where heavily-audited protocols still fall victim to hackers exploiting this vulnerability.
The Balancer V2 exploit in November 2025 is a sobering demonstration. A precision error buried deep in Balancer’s swap logic spiraled into a multi-chain crisis — a minor rounding error hidden deep within Balancer’s smart contracts led to one of the largest DeFi exploits of 2025, draining more than $128 million from its Composable Stable Pools across multiple blockchains.
The attack leveraged a rounding error vulnerability in the _upscaleArray function that, when
combined with carefully crafted batchSwap operations, allowed the attacker to artificially
suppress BPT prices and extract value through repeated arbitrage cycles. The exploitation occurred
primarily during attacker smart contract deployment, with the constructor executing 65+ micro-swaps
that compounded precision loss to devastating effect.
The zkLend hack illustrates the same class at a lending protocol level. In February 2025, zkLend was the victim of a $9.57 million hack where the attacker exploited a decimal precision vulnerability in the Starknet-based protocol’s smart contract to drain value from the project’s contracts. The attacker took advantage of the fact that the safeMath library used by the smart contract performs direct division, which rounds down when performing division.
The Rounding Direction Vulnerability
contract VulnerableFeeCollector {
uint256 public constant FEE_BPS = 30; // 0.3%
uint256 public accumulatedFees;
/// @notice Rounds DOWN — always favors the user, under-collects protocol fees
function collectFee(uint256 amount) external returns (uint256 userAmount) {
// Integer division truncates: 999 * 30 / 10000 = 2 (not 2.997)
uint256 fee = (amount * FEE_BPS) / 10000;
accumulatedFees += fee;
userAmount = amount - fee;
}
}
For an attacker splitting 10,000 transfers of 333 wei each:
- Each call:
fee = (333 * 30) / 10000 = 0(rounds to zero) - Total transferred:
333 * 10000 = 3,330,000 weifee-free
The Fix: Round in the Protocol’s Favor
library FeeMath {
/// @notice Rounds fee UP — always favors the protocol
function feeCeil(uint256 amount, uint256 feeBps) internal pure returns (uint256 fee) {
// Ceiling division: (a * b + c - 1) / c
fee = (amount * feeBps + 9999) / 10000;
}
/// @notice Minimum fee floor: never collect zero for non-zero amount
function feeWithFloor(uint256 amount, uint256 feeBps, uint256 minFee)
internal
pure
returns (uint256 fee)
{
fee = feeCeil(amount, feeBps);
if (amount > 0 && fee < minFee) fee = minFee;
}
}
contract SafeFeeCollector {
using FeeMath for uint256;
uint256 public constant FEE_BPS = 30;
uint256 public constant MIN_FEE = 1; // At least 1 wei for any non-zero trade
uint256 public accumulatedFees;
function collectFee(uint256 amount) external returns (uint256 userAmount) {
uint256 fee = amount.feeWithFloor(FEE_BPS, MIN_FEE);
accumulatedFees += fee;
userAmount = amount - fee;
}
}
Explicitly beware of integer division rounding: use multipliers for precision, or store numerator and denominator separately to defer division until the last possible moment.
5. Dynamic Fee Manipulation in Concentrated Liquidity AMMs
Uniswap V3 represents a monumental leap in AMM design, achieving unprecedented capital efficiency at the cost of significantly increased complexity. Its core innovation — concentrated liquidity — fundamentally transformed liquidity providers from passive participants into active, strategic market makers. This paradigm shift introduced a new class of security considerations and novel MEV strategies, demonstrating the inherent trade-off between optimization and attack surface.
Uniswap V3 forks and custom CLMMs carry unique risks: tick accounting bugs, TWAP oracle manipulation, fee-growth overflow, and JIT liquidity exploits.
In a concentrated liquidity AMM (CLMM), fees are tracked per tick range via fee-growth-inside
accumulators (e.g., feeGrowthInside0LastX128). A protocol that exposes writable fee parameters —
particularly the fee tier per pool — to insufficiently guarded governance creates a window for
dynamic fee manipulation.
JIT Liquidity for Fee Capture
Uniswap V3’s concentrated liquidity design introduces a new type of MEV source known as the “JIT liquidity attack,” where the adversary mints and burns a position immediately before and after a sizable swap transaction. The adversary monitors the mempool via a spy node. Once observing a sizable pending swap, the adversary simulates the JIT liquidity attack locally with chosen parameters and launches the attack if profitable.
But the deeper vulnerability class is fee-growth accounting manipulation, where an attacker can exploit a CLMM’s per-tick fee state:
// Simplified CLMM fee tracking — illustrating the feeGrowthInside vulnerability
contract VulnerableCLMM {
struct TickInfo {
uint256 feeGrowthOutside0X128;
uint256 feeGrowthOutside1X128;
int128 liquidityNet;
}
mapping(int24 => TickInfo) public ticks;
uint256 public feeGrowthGlobal0X128;
int24 public currentTick;
/// @notice BUG: Does not initialize feeGrowthOutside on tick creation.
/// An attacker can create a tick just below current price, let fees accumulate globally,
/// then open a position — claiming historical fees they weren't entitled to.
function initializeTick(int24 tick) external {
// Missing: set feeGrowthOutside to current global if tick <= currentTick
ticks[tick].feeGrowthOutside0X128 = 0; // Should be feeGrowthGlobal0X128 if below current
}
function computeFeeGrowthInside(
int24 tickLower,
int24 tickUpper
) external view returns (uint256 feeGrowthInside0X128) {
TickInfo storage lower = ticks[tickLower];
TickInfo storage upper = ticks[tickUpper];
uint256 feeGrowthBelow = currentTick >= tickLower
? lower.feeGrowthOutside0X128
: feeGrowthGlobal0X128 - lower.feeGrowthOutside0X128;
uint256 feeGrowthAbove = currentTick < tickUpper
? upper.feeGrowthOutside0X128
: feeGrowthGlobal0X128 - upper.feeGrowthOutside0X128;
feeGrowthInside0X128 = feeGrowthGlobal0X128 - feeGrowthBelow - feeGrowthAbove;
}
}
The Fix: Proper Tick Initialization
function initializeTick(int24 tick) external {
require(ticks[tick].liquidityNet == 0, "Tick already initialized");
// Correctly set feeGrowthOutside to the CURRENT global value if the tick
// is at or below the current price, preventing retroactive fee claiming.
if (tick <= currentTick) {
ticks[tick].feeGrowthOutside0X128 = feeGrowthGlobal0X128;
}
// If above current tick, feeGrowthOutside stays at 0 (no fees have passed through yet)
}
Additionally, protocols that allow governance to change fee tiers on existing pools (rather than
creating new pools) must ensure that accumulated feeGrowthGlobal values are not retroactively
reinterpreted under the new fee basis.
6. Fee Recipient Address Manipulation
Individuals controlling the so-called “fee-to” addresses, which receive transaction fees paid by users on the blockchain, were granted excessive permissions in documented exploits — making the fee recipient a direct attack surface.
When a protocol stores its fee recipient in a mutable state variable accessible to a privileged role — without a timelock or multi-sig — any compromise of that role immediately redirects all protocol revenue to an attacker-controlled address.
Vulnerable Fee Recipient Pattern
contract VulnerableFeeRouter {
address public feeRecipient;
address public owner;
constructor(address _feeRecipient) {
feeRecipient = _feeRecipient;
owner = msg.sender;
}
/// @notice BUG: Instant fee recipient change — no timelock, no multi-sig
function setFeeRecipient(address newRecipient) external {
require(msg.sender == owner, "Not owner");
// Change takes effect immediately on next fee distribution
feeRecipient = newRecipient;
}
function distributeFees(address token, uint256 amount) external {
IERC20(token).transfer(feeRecipient, amount);
}
}
An attacker who compromises the owner key — via phishing, private key theft, or a governance
attack — instantly gains all future fee flows.
The Fix: Timelock + Two-Step Recipient Change
contract SafeFeeRouter {
address public feeRecipient;
address public pendingFeeRecipient;
uint256 public recipientChangeTimestamp;
uint256 public constant TIMELOCK_DELAY = 48 hours;
address public owner;
event FeeRecipientChangeProposed(address indexed proposed, uint256 executableAt);
event FeeRecipientChanged(address indexed oldRecipient, address indexed newRecipient);
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
/// @notice Step 1: Propose a change — starts the timelock
function proposeFeeRecipientChange(address newRecipient) external onlyOwner {
require(newRecipient != address(0), "Zero address");
pendingFeeRecipient = newRecipient;
recipientChangeTimestamp = block.timestamp + TIMELOCK_DELAY;
emit FeeRecipientChangeProposed(newRecipient, recipientChangeTimestamp);
}
/// @notice Step 2: Execute after timelock expires
function executeFeeRecipientChange() external onlyOwner {
require(pendingFeeRecipient != address(0), "No pending change");
require(block.timestamp >= recipientChangeTimestamp, "Timelock not expired");
address old = feeRecipient;
feeRecipient = pendingFeeRecipient;
pendingFeeRecipient = address(0);
recipientChangeTimestamp = 0;
emit FeeRecipientChanged(old, feeRecipient);
}
function distributeFees(address token, uint256 amount) external {
require(feeRecipient != address(0), "No recipient set");
IERC20(token).transfer(feeRecipient, amount);
}
}
The 48-hour window gives community members and monitoring bots time to detect a compromised admin attempting to redirect fees before the change is irreversible.
7. Fee Switching Governance Attacks
Protocol fees are used by various DeFi protocols to allow value capture by their governance smart contracts. Fees can then be used to produce dividends for token holders or to accumulate treasury value, increasing the governance token’s value.
Governance-controlled fee switches — where a DAO vote can enable or disable fee collection, or change the fee rate — are a critical attack surface because they combine economic incentive (capturing all future fees) with the power of on-chain voting.
The Beanstalk governance exploit is the canonical case. The attacker submitted two proposals: a malicious one that would drain the funds, and another to donate some funds to a good cause. The attacker mislabeled the malicious proposal so people thought they were voting for the donation proposal. They then manipulated the voting through a flash loan to make it pass and executed the transaction to drain the protocol, stealing $181 million.
The same vector applies specifically to fee switching: an attacker flash-borrows the governance token, acquires a supermajority, proposes a fee change that redirects the treasury, and executes — all within one block if the governance contract lacks a minimum voting period.
Vulnerable Governance Fee Switch
contract VulnerableGovernance {
IERC20 public governanceToken;
uint256 public protocolFeeBps;
address public feeRecipient;
struct Proposal {
bytes callData;
uint256 votesFor;
bool executed;
}
Proposal[] public proposals;
/// @notice BUG: Snapshot taken at vote-cast time, not at proposal-creation time.
/// BUG: No minimum voting delay — vote and execute in same block.
function vote(uint256 proposalId) external {
uint256 balance = governanceToken.balanceOf(msg.sender);
proposals[proposalId].votesFor += balance;
}
function execute(uint256 proposalId) external {
Proposal storage p = proposals[proposalId];
require(!p.executed, "Already executed");
require(
p.votesFor > governanceToken.totalSupply() / 2,
"Insufficient votes"
);
p.executed = true;
(bool ok,) = address(this).call(p.callData);
require(ok, "Execution failed");
}
function setFee(uint256 newFeeBps, address newRecipient) external {
require(msg.sender == address(this), "Governance only");
protocolFeeBps = newFeeBps;
feeRecipient = newRecipient;
}
}
The Fix: Snapshot + Timelock + Quorum
import "@openzeppelin/contracts/governance/Governor.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorVotesQuorumFraction.sol";
contract SecureGovernor is Governor, GovernorTimelockControl, GovernorVotesQuorumFraction {
constructor(IVotes token, TimelockController timelock)
Governor("SecureGov")
GovernorVotesQuorumFraction(10) // 10% quorum required
GovernorTimelockControl(timelock)
{}
// Voting delay: 1 day after proposal before voting starts (prevents flash-loan snapshot attacks)
function votingDelay() public pure override returns (uint256) { return 7200; } // ~1 day
// Voting period: 5 days
function votingPeriod() public pure override returns (uint256) { return 36000; }
// Proposal threshold: must hold 1% of supply to propose
function proposalThreshold() public pure override returns (uint256) { return 1e18; }
}
Implement a timelock security pattern for significant changes. This provides enough time for the community to react to and potentially counteract malicious proposals. For fee-switch proposals specifically, consider a separate, higher quorum threshold than general governance proposals.
8. Protocol Token as Fee Currency: Self-Referential Fee Vulnerabilities
Protocols that denominate their own fees in their native token create a circular dependency between protocol solvency, token price, and fee revenue. This self-referentiality becomes an attack vector in several ways.
Price manipulation collapses real fee value. If a protocol charges fees in TOKEN and an attacker can crash the TOKEN price via a flash loan against a thin liquidity pool, the real (USD-denominated) cost of using the protocol drops to near zero during the attack window.
Minting authority and fee burning create reflexive loops. Protocols that burn their token as a fee mechanism can have that mechanism weaponized: if burning is triggered by on-chain usage and the attacker can force massive artificial usage, the attacker can drain the token supply or trigger deflationary spirals.
contract VulnerableNativeFeePayer {
IERC20 public protocolToken;
// Fees are priced in protocolToken — vulnerable to price manipulation
uint256 public feeInProtocolToken = 100e18; // "100 TOKEN" per operation
IOracle public oracle; // BUG: If this is a thin DEX pool, it can be manipulated
function performOperation() external {
// Fee is fixed in TOKEN terms — if TOKEN crashes, fee is worthless
protocolToken.transferFrom(msg.sender, address(this), feeInProtocolToken);
// ... operation logic
}
}
The Fix: USD-Denominated Fees with Token Payment
interface IChainlinkFeed {
function latestRoundData() external view returns (
uint80 roundId, int256 answer, uint256 startedAt,
uint256 updatedAt, uint80 answeredInRound
);
}
contract SafeNativeFeePayer {
IERC20 public protocolToken;
IChainlinkFeed public tokenPriceFeed; // Chainlink TWAP — resistant to single-block manipulation
uint256 public feeInUSD_8Dec = 10e8; // $10.00 in 8-decimal USD
uint256 public constant STALE_PRICE_THRESHOLD = 1
hours; // staleness threshold
uint256 public feeInToken; // cached fee in token units, updated on harvest
constructor(address _token, address _feed) {
protocolToken = IERC20(_token);
tokenPriceFeed = IChainlinkFeed(_feed);
}
function updateFee() external {
(, int256 tokenPriceUSD, , uint256 updatedAt, ) = tokenPriceFeed.latestRoundData();
require(block.timestamp - updatedAt <= STALE_PRICE_THRESHOLD, "stale price");
require(tokenPriceUSD > 0, "invalid price");
// feeInUSD_8Dec / tokenPriceUSD (both 8-decimal) → fee in token base units (18 dec)
feeInToken = (feeInUSD_8Dec * 1e18) / uint256(tokenPriceUSD);
}
function chargeFee(address payer) external {
require(feeInToken > 0, "fee not initialized");
protocolToken.transferFrom(payer, address(this), feeInToken);
}
}
Fee Manipulation Audit Checklist
Fee parameter governance
- Fee rates are bounded by a hard-coded ceiling that cannot be bypassed by governance
- Fee changes are subject to a timelock — immediate fee changes are not possible
- Fee recipient address changes require a two-step process or timelock
- Fee-related events are emitted for every change to enable off-chain monitoring
Dynamic fee safety
- If fees are computed from an oracle price, the oracle is validated for staleness and zero
- Fee calculations cannot overflow or underflow for any valid input range
- Fee-in-token calculations account for token decimal differences
Fee accumulation and extraction
- Accumulated fees cannot be re-interpreted as user collateral or reserves
- Fee extraction does not affect protocol solvency or share-price calculations
- Flash-loan-funded fee manipulation (inflating fee base, then extracting) is analyzed
MEV and sandwich exposure
- Dynamic fee updates are not exploitable by sandwiching: update fee, trade, revert fee
- Fee-on-transfer token behavior is accounted for — protocol receives the net amount
- Swap fee tiers are appropriate for pool volatility and cannot be gamed via governance