# external-call-safety

Detects unsafe external call patterns and token integration vulnerabilities in smart contracts. Covers unchecked call/delegatecall/staticcall return values, fee-on-transfer tokens, rebasing tokens, tokens with missing return values (USDT), ERC-777 callback risks, unsafe approve race conditions, return data bombs, gas stipend limitations, and push vs pull payment patterns. Use when auditing contracts that interact with external contracts, integrate arbitrary ERC20 tokens, distribute payments, or make low-level calls.

- **Kind:** skill
- **Source:** https://github.com/quillai-network/qs_skills
- **Page:** https://forefy.com/skills/9fe2b01b-2db6-4b9b-a380-49e731058660
- **API (JSON + files):** https://forefy.com/api/asr/9fe2b01b-2db6-4b9b-a380-49e731058660

---

## SKILL.md

---
name: external-call-safety
description: Detects unsafe external call patterns and token integration vulnerabilities in smart contracts. Covers unchecked call/delegatecall/staticcall return values, fee-on-transfer tokens, rebasing tokens, tokens with missing return values (USDT), ERC-777 callback risks, unsafe approve race conditions, return data bombs, gas stipend limitations, and push vs pull payment patterns. Use when auditing contracts that interact with external contracts, integrate arbitrary ERC20 tokens, distribute payments, or make low-level calls.
---

# External Call Safety

Detect vulnerabilities arising from **unsafe interactions with external contracts** and **non-standard token behaviors** that break protocol assumptions. Covers OWASP SC06 (Unchecked External Calls) plus the entire "weird ERC20" problem space.

## When to Use

- Auditing any contract that calls external contracts (token transfers, cross-contract interactions)
- Reviewing protocols that support arbitrary/user-supplied ERC20 tokens
- Analyzing ETH payment distribution logic (airdrops, reward distribution, refunds)
- Verifying low-level call safety (`call`, `delegatecall`, `staticcall`)
- When a protocol claims to support "any ERC20 token"

## When NOT to Use

- Reentrancy-specific analysis (use reentrancy-pattern-analysis — though there is overlap)
- Oracle/price feed analysis (use oracle-flashloan-analysis)
- Pure access control review (use semantic-guard-analysis)

## Part 1: External Call Safety

### Vulnerability Class 1: Unchecked Return Values

Low-level calls (`call`, `delegatecall`, `staticcall`) return a boolean indicating success. If unchecked, failed calls are silently ignored.

```solidity
// VULNERABLE: Return value not checked
function withdraw(uint256 amount) external {
    balances[msg.sender] -= amount;
    payable(msg.sender).call{value: amount}(""); // Can fail silently!
    // User's balance decreased but ETH not sent
}

// SAFE: Check return value
function withdraw(uint256 amount) external {
    balances[msg.sender] -= amount;
    (bool success, ) = payable(msg.sender).call{value: amount}("");
    require(success, "Transfer failed");
}
```

**Detection Algorithm:**

```
For each low-level call expression:
  1. Is the return value captured? (bool success, bytes memory data) = ...
  2. Is the success boolean checked? require(success) or if(!success) revert
  3. If not captured or not checked → UNCHECKED RETURN VALUE

Severity:
  - ETH transfer unchecked → CRITICAL (funds lost)
  - Token operation unchecked → HIGH (state desync)
  - Non-financial call unchecked → MEDIUM
```

### Vulnerability Class 2: Gas Stipend Limitations

```solidity
// DANGEROUS: transfer() and send() forward only 2300 gas
payable(recipient).transfer(amount); // Reverts if recipient needs > 2300 gas
payable(recipient).send(amount);     // Returns false, often unchecked

// SAFE: Use call() with gas
(bool success, ) = payable(recipient).call{value: amount}("");
require(success, "Transfer failed");
```

**Why 2300 gas is dangerous:**
- Contracts with `receive()` or `fallback()` that do more than emit an event will fail
- EIP-1884 changed `SLOAD` gas cost, breaking some existing contracts
- Multi-sig wallets and smart contract wallets often need more gas

### Vulnerability Class 3: Return Data Bomb

A malicious contract can return extremely large data to consume the caller's gas.

```solidity
// Vulnerable to return data bomb
(bool success, bytes memory data) = untrustedContract.call(calldata);
// If untrustedContract returns 1MB of data, copying it costs massive gas

// SAFE: Limit return data or ignore it
(bool success, ) = untrustedContract.call(calldata); // Ignore return data
// Or use assembly to limit return data size
```

### Vulnerability Class 4: Delegatecall to Untrusted Contract

```solidity
// CRITICAL: delegatecall executes untrusted code in OUR storage context
function execute(address target, bytes calldata data) external {
    target.delegatecall(data); // Untrusted code can overwrite ANY storage
}

// delegatecall should ONLY be used with trusted, immutable targets
```

## Part 2: Token Integration Safety ("Weird ERC20" Tokens)

### Issue 1: Fee-on-Transfer Tokens

Some tokens deduct a fee during `transfer()` and `transferFrom()`. The recipient receives less than the specified amount.

```solidity
// VULNERABLE: Assumes received amount equals input amount
function deposit(uint256 amount) external {
    token.transferFrom(msg.sender, address(this), amount);
    balances[msg.sender] += amount; // Credits MORE than actually received!
}

// SAFE: Check actual balance change
function deposit(uint256 amount) external {
    uint256 balanceBefore = token.balanceOf(address(this));
    token.transferFrom(msg.sender, address(this), amount);
    uint256 balanceAfter = token.balanceOf(address(this));
    uint256 actualReceived = balanceAfter - balanceBefore;
    balances[msg.sender] += actualReceived; // Credits actual amount
}
```

**Known fee-on-transfer tokens:** STA, PAXG, USDT (fee currently 0 but can be activated), RFI/SAFEMOON forks.

### Issue 2: Rebasing Tokens

Rebasing tokens change all balances proportionally without transfers. Protocol's accounting desynchronizes from actual balances.

```solidity
// VULNERABLE: Stores absolute balance amounts
function deposit(uint256 amount) external {
    token.transferFrom(msg.sender, address(this), amount);
    userDeposit[msg.sender] = amount; // After rebase, actual balance differs!
}

// Mitigation options:
// 1. Store shares instead of amounts
// 2. Wrap rebasing token (wstETH pattern)
// 3. Explicitly state: "rebasing tokens not supported"
```

**Known rebasing tokens:** stETH, AMPL, OHM, YAM, BASED.

### Issue 3: Missing Return Values

Some tokens don't return a boolean from `transfer()`/`transferFrom()`/`approve()`, breaking the ERC20 standard.

```solidity
// VULNERABLE: Assumes return value exists
bool success = token.transfer(recipient, amount); // Reverts if token returns nothing

// SAFE: Use SafeERC20
using SafeERC20 for IERC20;
token.safeTransfer(recipient, amount); // Handles missing return values
```

**Known tokens with missing returns:** USDT, BNB, OMG, KNC (legacy versions).

### Issue 4: Tokens with Callbacks (ERC-777)

ERC-777 tokens trigger `tokensToSend()` on the sender and `tokensReceived()` on the recipient during transfers, enabling reentrancy.

```
ERC-777 callback hooks:
  transfer() → calls tokensReceived() on recipient
  transferFrom() → calls tokensToSend() on sender, tokensReceived() on recipient
  send() → calls tokensToSend() on sender, tokensReceived() on recipient

ANY of these can re-enter the calling contract!
```

**Cross-reference:** See reentrancy-pattern-analysis for detailed ERC-777 reentrancy detection.

### Issue 5: Unsafe Approve Pattern

```solidity
// VULNERABLE: Approve race condition
token.approve(spender, newAmount);
// Between the approval TX and the spending TX, the spender can:
// 1. Spend the OLD allowance
// 2. Then spend the NEW allowance
// Total spent: oldAmount + newAmount (double spending)

// SAFE: Reset to zero first, or use increaseAllowance
token.approve(spender, 0); // Reset
token.approve(spender, newAmount); // Set new

// Or use SafeERC20
token.safeIncreaseAllowance(spender, amount);

// ALSO DANGEROUS: Some tokens (USDT) revert on non-zero to non-zero approve
token.approve(spender, newAmount); // REVERTS if current allowance != 0
// MUST reset to 0 first for USDT
```

### Issue 6: Tokens with Blacklists

Some tokens can blacklist addresses, causing transfers to/from those addresses to revert.

```solidity
// VULNERABLE: Assumes transfer always succeeds for valid amounts
function distribute(address[] calldata users, uint256[] calldata amounts) external {
    for (uint i = 0; i < users.length; i++) {
        token.transfer(users[i], amounts[i]); // Reverts if ANY user is blacklisted
        // Entire batch fails!
    }
}

// SAFE: Handle per-user failures
function distribute(address[] calldata users, uint256[] calldata amounts) external {
    for (uint i = 0; i < users.length; i++) {
        try IERC20(token).transfer(users[i], amounts[i]) {
            // Success
        } catch {
            // Log failure, skip this user, don't block others
        }
    }
}
```

**Known blacklist tokens:** USDC, USDT, TUSD.

### Issue 7: Tokens with Max Supply / Transfer Limits

Some tokens have maximum transfer amounts per transaction or maximum holding amounts per address.

```solidity
// Protocol may assume any amount can be transferred
// But some tokens: require(amount <= maxTransferAmount)
// This can brick protocols that batch large transfers
```

## Part 3: Payment Pattern Analysis

### Push vs Pull Pattern

```
PUSH (Dangerous):
  Contract sends funds TO recipients
  - Can fail if recipient is a contract that reverts
  - Can be DoS'd by one malicious recipient
  - Gas costs unpredictable

PULL (Safe):
  Recipients claim funds FROM contract
  - Each claim is independent
  - One user's failure doesn't affect others
  - Gas costs predictable per claim
```

**Detection:**

```
For each function that sends ETH or tokens to external addresses:
  If sending to user-supplied addresses in a loop → PUSH pattern
  If sending to individual addresses via claim function → PULL pattern
  PUSH pattern with untrusted recipients → HIGH risk of DoS
```

## Workflow

```
Task Progress:
- [ ] Step 1: Find all external calls (call, delegatecall, staticcall, transfer, send)
- [ ] Step 2: Verify return values are checked for all external calls
- [ ] Step 3: Identify all token interactions and classify token assumptions
- [ ] Step 4: Check for fee-on-transfer compatibility (balance before/after pattern)
- [ ] Step 5: Check for rebasing token compatibility
- [ ] Step 6: Verify SafeERC20 usage for tokens with missing return values
- [ ] Step 7: Check approve patterns for race conditions and USDT compatibility
- [ ] Step 8: Analyze payment distribution pattern (push vs pull)
- [ ] Step 9: Score findings and generate report
```

## Output Format

```markdown
## External Call Safety Report

### Finding: [Title]

**Function:** `functionName()` at `Contract.sol:L42`
**Category:** [Unchecked Return | Fee-on-Transfer | Rebasing | Missing Return | Callback | Approve Race | DoS]
**Severity:** [CRITICAL | HIGH | MEDIUM]

**Issue:**
[Description of the unsafe external call or token integration issue]

**Affected Tokens:**
[List of known tokens that trigger this issue, e.g., USDT, USDC, stETH]

**Vulnerable Code:**
[Code snippet]

**Attack Scenario:**
1. [Step-by-step exploitation]

**Recommendation:**
[Use SafeERC20, balance-before-after, pull pattern, etc.]
```

## Quick Detection Checklist

- [ ] Are ALL low-level `call` return values checked (`require(success)`)?
- [ ] Does the protocol use `SafeERC20` for all token interactions?
- [ ] Does the deposit function use balance-before-after pattern for fee-on-transfer tokens?
- [ ] Does the protocol explicitly handle or reject rebasing tokens?
- [ ] Does `approve()` reset to 0 before setting new allowance (USDT compatibility)?
- [ ] Are batch payment operations using pull pattern (not push)?
- [ ] Is `delegatecall` only used with trusted, immutable targets?
- [ ] Are return data sizes from untrusted contracts limited?
- [ ] Does the protocol handle token blacklisting gracefully?

For weird ERC20 catalog, see [{baseDir}/references/weird-erc20.md]({baseDir}/references/weird-erc20.md).
For call safety patterns, see [{baseDir}/references/call-safety-patterns.md]({baseDir}/references/call-safety-patterns.md).

## Rationalizations to Reject

- "We only support standard ERC20 tokens" → USDT is the most used token and it's non-standard (no return value, fee capability)
- "The call will always succeed" → Smart contract wallets, blacklisted addresses, and gas changes can cause failures
- "We trust the token contract" → Token contracts can be upgraded (proxies) or have hidden features
- "transfer() is safe enough" → 2300 gas stipend breaks with gas repricing EIPs; use call()
- "We checked the token before listing" → Fee-on-transfer can be toggled on after listing (USDT has this capability)
- "Rebasing tokens are rare" → stETH is one of the largest tokens by TVL

## references

```

```

## references/call-safety-patterns.md

# External Call Safety Patterns — Reference

## Low-Level Call Types

### `call` — General Purpose External Call

```solidity
// Pattern: (bool success, bytes memory data) = target.call{value: v, gas: g}(payload)

// UNSAFE: Return not checked
target.call{value: amount}("");

// UNSAFE: Success checked but data ignored when it matters
(bool success, ) = target.call{value: amount}("");
require(success); // OK for ETH transfer

// SAFE: Full check
(bool success, bytes memory data) = target.call(
    abi.encodeWithSelector(IERC20.transfer.selector, recipient, amount)
);
require(success && (data.length == 0 || abi.decode(data, (bool))), "Transfer failed");

// SAFEST: Use high-level call or SafeERC20
token.safeTransfer(recipient, amount);
```

### `delegatecall` — Execute in Caller's Context

```solidity
// CRITICAL: delegatecall runs target's code with OUR storage, msg.sender, msg.value
// Should ONLY be used with immutable, trusted contracts

// DANGEROUS: User-supplied target
function execute(address target, bytes calldata data) external {
    target.delegatecall(data); // Attacker can overwrite ANY storage slot
}

// SAFE: Only to known, immutable implementation
function _delegate(address implementation) internal {
    assembly {
        calldatacopy(0, 0, calldatasize())
        let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)
        returndatacopy(0, 0, returndatasize())
        switch result
        case 0 { revert(0, returndatasize()) }
        default { return(0, returndatasize()) }
    }
}
```

### `staticcall` — Read-Only External Call

```solidity
// staticcall prevents state modification in the called contract
// SAFE for reading data, but:
// - Can still consume gas (DoS vector)
// - Return data can be arbitrarily large (return data bomb)
// - Reverts in called contract bubble up

(bool success, bytes memory data) = target.staticcall(
    abi.encodeWithSelector(IERC20.balanceOf.selector, address(this))
);
```

---

## ETH Transfer Patterns

### Pattern Comparison

| Method | Gas Forwarded | On Failure | Safety |
|--------|-------------|------------|--------|
| `transfer()` | 2300 (fixed) | Reverts | UNSAFE (gas limit) |
| `send()` | 2300 (fixed) | Returns false | UNSAFE (gas limit + often unchecked) |
| `call{value: x}("")` | All remaining | Returns false | SAFE (if checked) |

### Recommended Pattern

```solidity
// For sending ETH to a single recipient
function sendETH(address payable recipient, uint256 amount) internal {
    (bool success, ) = recipient.call{value: amount}("");
    require(success, "ETH transfer failed");
}

// For sending ETH to multiple recipients (pull pattern preferred)
mapping(address => uint256) public pendingWithdrawals;

function withdraw() external {
    uint256 amount = pendingWithdrawals[msg.sender];
    require(amount > 0, "Nothing to withdraw");
    pendingWithdrawals[msg.sender] = 0;
    (bool success, ) = msg.sender.call{value: amount}("");
    require(success, "Withdraw failed");
}
```

---

## Token Interaction Patterns

### Safe ERC20 Usage

```solidity
using SafeERC20 for IERC20;

// Transfer
token.safeTransfer(recipient, amount);

// TransferFrom
token.safeTransferFrom(sender, recipient, amount);

// Approve (handles USDT non-zero to non-zero issue)
token.forceApprove(spender, amount); // OZ v5
// or
token.safeApprove(spender, 0);
token.safeApprove(spender, amount);

// Increase/Decrease allowance
token.safeIncreaseAllowance(spender, amount);
token.safeDecreaseAllowance(spender, amount);
```

### Fee-on-Transfer Safe Deposit

```solidity
function deposit(IERC20 token, uint256 amount) external {
    uint256 balanceBefore = token.balanceOf(address(this));
    token.safeTransferFrom(msg.sender, address(this), amount);
    uint256 received = token.balanceOf(address(this)) - balanceBefore;

    // Use 'received' not 'amount' for accounting
    deposits[msg.sender] += received;
    totalDeposits += received;
}
```

### Rebasing Token Wrapper

```solidity
// Pattern: Wrap rebasing token into non-rebasing shares
// Example: stETH → wstETH

interface IWrapperToken {
    function wrap(uint256 amount) external returns (uint256 shares);
    function unwrap(uint256 shares) external returns (uint256 amount);
}

// Protocol only stores and operates on wrapped (share) amounts
```

---

## Push vs Pull Payment Patterns

### Push Pattern (DANGEROUS)

```solidity
// DANGEROUS: One failed transfer blocks all
function distributeRewards(address[] calldata users, uint256[] calldata amounts) external {
    for (uint i = 0; i < users.length; i++) {
        // If ANY user is a contract that reverts → entire distribution fails
        payable(users[i]).transfer(amounts[i]);
    }
}
```

### Pull Pattern (SAFE)

```solidity
// SAFE: Each user claims independently
mapping(address => uint256) public pendingRewards;

function addRewards(address[] calldata users, uint256[] calldata amounts) external onlyAdmin {
    for (uint i = 0; i < users.length; i++) {
        pendingRewards[users[i]] += amounts[i];
    }
}

function claimReward() external {
    uint256 reward = pendingRewards[msg.sender];
    require(reward > 0, "Nothing to claim");
    pendingRewards[msg.sender] = 0;
    (bool success, ) = msg.sender.call{value: reward}("");
    require(success, "Claim failed");
}
```

---

## Return Data Bomb Protection

### The Attack

```solidity
// Malicious contract returns massive data
contract MaliciousReceiver {
    fallback() external payable {
        assembly {
            // Return 1MB of data — costs caller gas to copy
            return(0, 1048576)
        }
    }
}
```

### Protection

```solidity
// Option 1: Ignore return data
(bool success, ) = target.call{value: amount}(""); // data not copied

// Option 2: Limit return data in assembly
assembly {
    let success := call(gas(), target, amount, 0, 0, 0, 0) // outSize = 0
    if iszero(success) {
        // Handle failure
        let size := returndatasize()
        if gt(size, 256) { size := 256 } // Cap error message
        returndatacopy(0, 0, size)
        revert(0, size)
    }
}
```

---

## External Call Risk Classification

| Call Target | Risk Level | Required Checks |
|-------------|-----------|-----------------|
| Known trusted contract (immutable) | LOW | Return value check |
| Known trusted contract (upgradeable) | MEDIUM | Return value + interface verification |
| User-supplied address | HIGH | Return value + gas limit + return data limit |
| Arbitrary contract via delegatecall | CRITICAL | Should NOT be allowed |
| Token contract (standard ERC20) | MEDIUM | SafeERC20 wrapper |
| Token contract (unknown/arbitrary) | HIGH | SafeERC20 + balance-before-after + blacklist handling |

## references/weird-erc20.md

# Weird ERC20 Token Behaviors — Catalog

## Category 1: Missing Return Values

### Behavior
`transfer()`, `transferFrom()`, and `approve()` don't return a boolean, violating the ERC20 standard. Calling code that expects a return value will revert.

### Affected Tokens
- **USDT** (Tether) — the most widely used stablecoin
- **BNB** (Binance Coin)
- **OMG** (OmiseGO)
- **KNC** (Kyber Network, legacy version)

### Impact on Protocols

```solidity
// This REVERTS when called with USDT:
bool success = IERC20(usdt).transfer(recipient, amount);

// This works because SafeERC20 handles missing return:
using SafeERC20 for IERC20;
IERC20(usdt).safeTransfer(recipient, amount);
```

### Detection
Flag any direct `.transfer()`, `.transferFrom()`, or `.approve()` call that doesn't use SafeERC20 wrapper.

---

## Category 2: Fee-on-Transfer

### Behavior
A percentage of every transfer is deducted as a fee. The recipient receives less than the specified `amount`.

### Affected Tokens
- **STA** (Statera) — 1% deflationary fee
- **PAXG** (Pax Gold) — 0.02% transfer fee
- **USDT** — fee mechanism exists (currently set to 0, can be activated)
- **SAFEMOON** and all RFI forks — typically 5-10% fee
- **Reflect Finance (RFI)** — 1% redistributed to holders

### Impact on Protocols

```solidity
// Protocol credits 100 tokens but only receives 99
function deposit(uint256 amount) external {
    token.transferFrom(msg.sender, address(this), amount); // Receives 99
    balances[msg.sender] += amount; // Credits 100!
    // Accounting error: protocol is 1 token short per deposit
    // Over many deposits, protocol becomes insolvent
}
```

### Detection
Check if deposit/stake functions compare `amount` parameter directly vs checking actual balance change.

---

## Category 3: Rebasing (Supply-Adjusting)

### Behavior
Token balances change for ALL holders without transfers. Can increase (positive rebase) or decrease (negative rebase).

### Affected Tokens
- **stETH** (Lido Staked ETH) — daily positive rebase from staking rewards
- **AMPL** (Ampleforth) — daily rebase targeting $1 price
- **OHM** (Olympus) — rebase from protocol emissions
- **YAM** — rebase mechanism
- **BASED** — rebase mechanism

### Impact on Protocols

```solidity
// User deposits 100 stETH
deposits[user] = 100 ether;

// Next day: stETH rebases +0.01%
// User's actual balance: 100.01 stETH
// Protocol records: still 100 stETH
// Difference accumulates forever

// WORSE: Negative rebase
// If AMPL rebases -10%, user has 90 AMPL
// But protocol still shows 100 — user can withdraw more than exists
```

### Detection
Check if the protocol stores absolute token amounts (vulnerable) or shares/ratios (safe).

---

## Category 4: Approval Race Condition

### Behavior
Standard `approve()` has a known race condition. Some tokens (USDT) additionally revert if you try to change a non-zero allowance to another non-zero value.

### Affected Tokens (revert on non-zero to non-zero)
- **USDT**
- **KNC** (legacy)

### Impact on Protocols

```solidity
// Step 1: Set allowance to 100
token.approve(spender, 100);

// Step 2: Try to change allowance to 200
token.approve(spender, 200); // REVERTS with USDT!

// Must do:
token.approve(spender, 0);   // Reset first
token.approve(spender, 200); // Then set new
```

### Detection
Flag any `approve()` call that doesn't first reset to zero, especially if the protocol supports USDT.

---

## Category 5: Tokens with Hooks/Callbacks

### Behavior
Token transfers trigger callback functions on sender and/or recipient, enabling reentrancy.

### Affected Standards
- **ERC-777** — `tokensToSend()` on sender, `tokensReceived()` on recipient
- **ERC-1155** — `onERC1155Received()` on recipient
- **ERC-721** — `onERC721Received()` on recipient (via `safeTransferFrom`)

### Impact
Any state change after a token transfer that triggers callbacks is vulnerable to reentrancy.

### Detection
Cross-reference with reentrancy-pattern-analysis skill.

---

## Category 6: Tokens with Blacklists/Pausable

### Behavior
Certain addresses can be blacklisted, causing all transfers to/from those addresses to revert. Token can also be globally paused.

### Affected Tokens
- **USDC** (Centre) — blacklist controlled by Centre consortium
- **USDT** (Tether) — blacklist and pausable
- **TUSD** (TrueUSD) — blacklist
- **BUSD** (Binance USD) — blacklist

### Impact on Protocols

```solidity
// If a user gets blacklisted after depositing:
function withdraw(uint256 amount) external {
    balances[msg.sender] -= amount;
    token.transfer(msg.sender, amount); // REVERTS — user is blacklisted
    // Funds permanently locked in protocol!
}

// Impact on batch operations:
function distributeRewards(address[] calldata users) external {
    for (uint i = 0; i < users.length; i++) {
        token.transfer(users[i], rewards[i]); // One blacklisted user blocks ALL
    }
}
```

### Detection
Check if the protocol has fallback mechanisms for failed transfers (try/catch, pull pattern).

---

## Category 7: Tokens with Transfer Limits

### Behavior
Maximum amount that can be transferred in a single transaction, or maximum balance an address can hold.

### Affected Tokens
- Many "anti-whale" tokens
- Various meme tokens with anti-dump mechanisms

### Impact
Protocols that batch transfers or accumulate large balances may silently hit limits.

---

## Category 8: Tokens with Multiple Entry Points

### Behavior
Some tokens have multiple addresses or proxy contracts that all reference the same underlying token.

### Affected Tokens
- Upgradeable token proxies
- Tokens with migration contracts

### Impact
Protocol may treat the same token as two different tokens, creating accounting errors.

---

## Category 9: Low-Decimal Tokens

### Behavior
Tokens with very few decimals (0-6) amplify rounding errors.

### Affected Tokens
- **USDC** — 6 decimals
- **USDT** — 6 decimals
- **WBTC** — 8 decimals
- **GUSD** — 2 decimals
- Some tokens — 0 decimals

### Impact
Precision loss in calculations is much worse with fewer decimals. A rounding error of 1 unit in GUSD (2 decimals) is $0.01 per operation.

---

## Category 10: High-Decimal Tokens

### Behavior
Tokens with more than 18 decimals can cause overflow in calculations that assume 18 decimals.

### Affected Tokens
- **YAM-V2** — 24 decimals

### Impact
`amount * price` may overflow uint256 when both have high decimals.

---

## Compatibility Matrix

| Token Behavior | SafeERC20 | Balance Before/After | Pull Pattern | Wrap Token |
|----------------|-----------|---------------------|-------------|-----------|
| Missing returns | FIXES | N/A | N/A | N/A |
| Fee-on-transfer | N/A | FIXES | N/A | FIXES |
| Rebasing | N/A | PARTIAL | N/A | FIXES (wstETH) |
| Approve race | PARTIAL (forceApprove) | N/A | N/A | N/A |
| Callbacks/hooks | N/A | N/A | N/A | Reentrancy guard needed |
| Blacklists | N/A | N/A | HELPS | N/A |
| Transfer limits | N/A | N/A | HELPS | N/A |

