Skip to the content.

Previous: Advanced Topics | Next: Bug Bounty Playbook → anced | | Vault rounding direction | §13.11 | Advanced | | Governance timelock bypass | §13.12 | Advanced | | Cross-chain replay | §13.13 | Advanced | | Assembly pitfalls | §13.14 | Advanced | | Staking reward bugs | §13.15 | Intermediate | | Donation price manipulation | §13.16 | Advanced | | Signature malleability | §13.17 | Intermediate | | Liquidation mechanism bugs | §13.18 | Advanced | | Bundle ordering attacks | §13.19 | Expert | | Proxy upgrade patterns | §13.20 | Advanced |dvanced | | ERC-721/1155 callback reentrancy | §13.2 | Intermediate | | Permit front-running & phishing | §13.3 | Intermediate | | Multicall msg.value reuse | §13.4 | Advanced | | Read-only reentrancy (deep) | §13.5 | Advanced | | Weird ERC-20 complete reference | §13.6 | Intermediate | | Compiler bugs | §13.7 | Intermediate | | Return bomb / gas griefing | §13.8 | Intermediate | | Chainlink edge cases (complete) | §13.9 | Advanced | | TWAP manipulation cost analysis | §13.10 | Adventation // If the beacon is compromised, ALL proxies are compromised simultaneously

// Attack surface: // 1. Beacon owner key compromise → upgrade all proxies at once // 2. Beacon contract bug → all proxies affected // 3. No timelock on beacon upgrades → instant mass upgrade

// Defense: Timelock on beacon upgrades, multisig beacon owner

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
---

## Summary — Gap Coverage

| Vulnerability Class | Covered In | Difficulty |
|--------------------|-----------|------------|
| Transient storage bugs | §13.1 |  Atation is deployed without a proxy and initialized by an attacker,
// they can call upgradeTo(maliciousContract) then selfdestruct the implementation

// Post-Dencun (EIP-6780): selfdestruct only works in same tx as creation
// But the upgrade-to-malicious-implementation attack still works

// [YES] Fix: _disableInitializers() in implementation constructor
constructor() {
    _disableInitializers();
}

Beacon Proxy Attacks

1
2
3
4
5
6
7
8
// Beacon proxies: all proxies point to a beacon, beacon points to implemndent,
an attacker can exploit ordering within the bundle.

Example:
Bundle: [TX1: deposit, TX2: borrow, TX3: manipulate oracle, TX4: liquidate]
- TX3 happens AFTER TX2  the borrow was at the correct price
- TX4 liquidates a different user whose position became underwater due to TX3
- This is legitimate MEV but can be used maliciously

13.20 Proxy Upgrade Attack Patterns

UUPS Self-Destruct (Pre-Dencun)

1
2
3
4
5
6
7
8
9
10
// UUPS proxies have the upgrade logic in the IMPLEMENTATION
// If the implemen The protocol absorbs the bad debt
- This dilutes all depositors

Attack:
1. Create many small undercollateralized positions
2. Each position creates bad debt when liquidated
3. Aggregate bad debt drains the protocol's reserves

Defense: Minimum position sizes, liquidation buffers, insurance funds

13.19 Flashbots Bundle Ordering Attacks

Bundle Dependency Exploitation

1
2
3
4
5
6
7
8
9
10
11
12
When submitting Flashbots bundles, transactions execute in order.
If a protocol assumes transactions in a bundle are indepecan be exploited

// Attack: Self-liquidation
// 1. Deposit collateral
// 2. Borrow to the edge of liquidation threshold
// 3. Slightly push your own position underwater (via oracle manipulation or donation)
// 4. Liquidate yourself from a different address
// 5. Collect the liquidation bonus

// Defense: Prevent self-liquidation (check borrower != liquidator)
// Or: Ensure liquidation bonus < oracle manipulation cost

Bad Debt Socialization

1
2
3
4
5
6
7
8
9
10
When a position is liquidated but collateral < debt:
-sses the check
}

// [YES] Fixed: Use OpenZeppelin's ECDSA which enforces lower-s
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

function verify(bytes32 hash, bytes memory sig) internal pure returns (address) {
    return ECDSA.recover(hash, sig); // Enforces s <= secp256k1.n/2
}

13.18 Liquidation Mechanism Vulnerabilities

Liquidation Incentive Manipulation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Protocols offer liquidation bonuses to incentivize liquidators
// Vulnerability: If bonus is too high, it 

```solidity
// ECDSA signatures have two valid forms for any message:
// (v, r, s) and (v', r, s') where s' = secp256k1.n - s
// Both recover to the same address!

// [NO] Vulnerable: Using raw ecrecover
function verify(bytes32 hash, bytes memory sig) internal pure returns (address) {
    (bytes32 r, bytes32 s, uint8 v) = splitSig(sig);
    return ecrecover(hash, v, r, s);
    // Attacker can submit (v', r, s') — different bytes, same signer
    // If signature is used as a key (mapping[sig] = true), this bypatokens directly to Uniswap V2 pair
// 2. Call sync() to update reserves
// 3. Price is now manipulated
// 4. Any protocol reading the spot price is affected

// This is different from flash loan manipulation:
// - Flash loan: borrow → manipulate → repay (atomic)
// - Donation: permanent price change (until arbitraged away)
// - Cost: the donated tokens (not free like flash loans)
// - Benefit: can persist across blocks (affects TWAP over time)

13.17 Signature Malleability

ECDSA Malleabilitytake them right before reward distribution

  1. Claim disproportionate rewards
  2. Unstake and repay flash loan

Defense: Use snapshot-based reward calculation Implement minimum staking duration Use time-weighted average stake

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
---

## 13.16 Price Manipulation via Donation

### Direct Token Donation to Pools

```solidity
// Uniswap V2: Reserves are tracked via balanceOf()
// If you send tokens directly to the pool (not via swap), reserves are off
// until sync() is called

// Attack:
// 1. Send oken() returns 0 due to integer division
    function rewardPerToken() public view returns (uint256) {
        if (totalStaked == 0) return rewardPerTokenStored;
        return rewardPerTokenStored + (rewardRate * (block.timestamp - lastUpdate) * 1e18 / totalStaked);
    }

    // Attack: Stake a massive amount to make rewardPerToken() = 0
    // Other stakers earn 0 rewards while attacker holds the stake
}

Reward Manipulation via Flash Stake

1
2
3
4
5
Attack:
1. Flash loan governance/staking tokens
2. Srn(0, 32)
    }
}

13.15 Staking & Reward Distribution Bugs

Reward Calculation Precision

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// Common pattern: rewards per token accumulated
// Vulnerable to precision loss and manipulation

contract VulnerableStaking {
    uint256 public rewardPerTokenStored;
    uint256 public totalStaked;
    mapping(address => uint256) public userRewardPerTokenPaid;
    mapping(address => uint256) public rewards;

    // [NO] If totalStaked is very large and rewardRate is small,
    // rewardPerTal pure returns (bytes32) {
    assembly {
        // [NO] Upper bits of addr may not be clean (if passed from calldata)
        let packed := or(shl(96, addr), value)
        mstore(0, packed)
        return(0, 32)
    }
}

// [YES] Clean the address first
function packDataSafe(address addr, uint96 value) internal pure returns (bytes32) {
    assembly {
        let cleanAddr := and(addr, 0xffffffffffffffffffffffffffffffffffffffff)
        let packed := or(shl(96, cleanAddr), value)
        mstore(0, packed)
        retuemory data) internal pure returns (bytes32 result) {
    assembly {
        result := mload(data) // [NO] Reads the LENGTH, not the data!
        // data points to the length prefix
        // data + 32 points to the actual bytes
    }
}

// [YES] Correct
function goodAssembly(bytes memory data) internal pure returns (bytes32 result) {
    assembly {
        result := mload(add(data, 32)) // [YES] Skip the 32-byte length prefix
    }
}

// [NO] Dirty bits in packed data
function packData(address addr, uint96 value) intern);
}

Bridge Message Replay

1
2
3
4
5
6
7
8
Attack: A message processed on Chain A is replayed on Chain B
        (or replayed multiple times on the same chain)

Required protections:
1. Nonce per sender per destination chain
2. Message hash tracking (mark as processed)
3. Chain ID in message payload
4. Destination contract address in message payload

13.14 Solidity Assembly Pitfalls

Common Inline Assembly Bugs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// [NO] Memory corruption via incorrect offset
function badAssembly(bytes mInvalid sig");
    // No chainId in hash → same sig works on Ethereum, Polygon, BSC, etc.
}

// [YES] Include chainId in signed data
function execute(bytes32 dataHash, bytes memory sig) external {
    bytes32 hash = keccak256(abi.encodePacked(
        block.chainid,          // [YES] Chain-specific
        address(this),          // [YES] Contract-specific
        nonces[owner]++,        // [YES] Replay protection
        dataHash
    ));
    address signer = ECDSA.recover(hash, sig);
    require(signer == owner, "Invalid sig""Not guardian");
        token.transfer(to, amount); // No timelock, no multisig
    }
}

// Attack: Compromise the guardian key → instant fund drain
// Defense: Even emergency functions should require multisig
//          or have a short (1-hour) timelock

13.13 Cross-Chain Replay Attacks

Chain ID Validation

1
2
3
4
5
// [NO] Signature valid on all chains
function execute(bytes32 hash, bytes memory sig) external {
    address signer = ECDSA.recover(hash, sig);
    require(signer == owner, "Require independent security review for each proposal
         Implement proposal dependency tracking

Emergency Function Abuse

1
2
3
4
5
6
7
8
9
10
11
// Many protocols have emergency functions that bypass timelock
// These are intended for crisis response but create attack vectors

contract VulnerableProtocol {
    address public guardian;

    // [NO] Guardian can drain funds instantly with no timelock
    function emergencyWithdraw(address to, uint256 amount) external {
        require(msg.sender == guardian,  harvest proportional to deposit

Defense: Implement withdrawal fees, vesting periods, or per-block deposit limits

13.12 Governance Timelock Bypass Patterns

Proposal Batching Attack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Attack: Submit multiple proposals that individually look harmless
        but combined execute a malicious action

Example:
- Proposal A: "Add new token as collateral" (looks benign)
- Proposal B: "Update oracle for new token" (looks benign)
- Combined: New token uses a manipulable oracle → exploit

Defense: ays less
    // Attacker can repeatedly withdraw/deposit to drain vault via rounding
}

// [YES] Correct: Round UP for withdrawals
function previewWithdraw(uint256 assets) public view returns (uint256 shares) {
    return assets.mulDiv(totalSupply(), totalAssets(), Math.Rounding.Ceil);
}

Vault Sandwich Attack

1
2
3
4
5
Attack on vaults with delayed price updates:
1. Deposit large amount before a profitable harvest
2. Harvest increases vault's totalAssets
3. Withdraw immediately after harvest
4. Profit = share ofulable with flash loans

13.11 Vault / Share Accounting Bugs

Rounding Direction Errors

1
2
3
4
5
6
7
8
// ERC-4626 standard: Round DOWN for deposits (user gets fewer shares)
//                    Round UP for withdrawals (user pays more assets)
// This protects the vault from value leakage

// [NO] Wrong rounding direction — rounds in user's favor
function previewWithdraw(uint256 assets) public view returns (uint256 shares) {
    return assets * totalSupply() / totalAssets(); // Rounds DOWN — user pTick);
}

TWAP Manipulation Cost

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
To manipulate a 30-minute TWAP by X%:
- Attacker must hold the price at X% deviation for 30 minutes
- Cost = capital locked × opportunity cost × 30 minutes
- On liquid pairs (ETH/USDC), this costs millions per % of manipulation
- On illiquid pairs (small-cap tokens), manipulation is cheap

Rule of thumb:
- 30-minute TWAP on ETH/USDC: Very expensive to manipulate
- 30-minute TWAP on small-cap token: Potentially cheap
- 1-block TWAP: Essentially a spot price — manipapital over time

function getTWAP(address pool, uint32 secondsAgo) external view returns (uint256 price) {
    uint32[] memory secondsAgos = new uint32[](2);
    secondsAgos[0] = secondsAgo;
    secondsAgos[1] = 0;

    (int56[] memory tickCumulatives, ) = IUniswapV3Pool(pool).observe(secondsAgos);

    int56 tickCumulativesDelta = tickCumulatives[1] - tickCumulatives[0];
    int24 arithmeticMeanTick = int24(tickCumulativesDelta / int56(uint56(secondsAgo)));

    price = TickMath.getSqrtRatioAtTick(arithmeticMeanht LUNA was worth $0.10
- Attackers borrowed against "valuable" LUNA collateral
- Protocols suffered massive bad debt

Detection: Check if the feed has minAnswer/maxAnswer set
cast call 0xFeedAddress "minAnswer()(int192)"
cast call 0xFeedAddress "maxAnswer()(int192)"

13.10 Uniswap V3 TWAP Oracle Manipulation

TWAP Mechanics

1
2
3
4
5
6
7
// Uniswap V3 TWAP: Time-Weighted Average Price over N seconds
// More resistant to flash loan manipulation than spot price
// But still manipulable with sustained cnt256 startedAt,,) = sequencerFeed.latestRoundData();
    require(answer == 0, "Sequencer down"); // 0 = up, 1 = down
    require(block.timestamp - startedAt > GRACE_PERIOD, "Sequencer just restarted");
    // Grace period prevents using prices from right after sequencer restart
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Real scenario: LUNA crash (May 2022)
- Chainlink LUNA/USD feed had minAnswer = $0.10
- When LUNA crashed to $0.0001, the feed still reported $0.10
- Protocols using this feed thougime (for Arbitrum, Optimism, etc.)
    // Must check sequencer uptime feed before using any price
    _checkSequencerUptime();

    // [YES] Check 6: Price within expected bounds (circuit breaker)
    // Some feeds have minAnswer/maxAnswer — price clamps at these values
    // If real price is outside bounds, feed returns the clamped value
    // This is NOT detectable from the feed itself — use secondary validation

    return uint256(answer);
}

function _checkSequencerUptime() internal view {
    (, int256 answer, uice is positive
    require(answer > 0, "Negative price");

    // [YES] Check 2: Round is complete
    require(updatedAt != 0, "Incomplete round");

    // [YES] Check 3: Answer is from the latest round (not stale)
    require(answeredInRound >= roundId, "Stale price");

    // [YES] Check 4: Price is not too old (heartbeat varies by feed)
    // ETH/USD heartbeat = 3600s, BTC/USD = 3600s, some = 86400s
    require(block.timestamp - updatedAt <= HEARTBEAT + GRACE_PERIOD, "Price too old");

    // [YES] Check 5: L2 sequencer upt256 public counter;
    receive() external payable {
        counter++; // Requires SSTORE — 20,000 gas — transfer() will fail!
    }
}

1
2
3
4
5
6
7
8
9
10
11
function getPrice(AggregatorV3Interface feed) internal view returns (uint256) {
    (
        uint80 roundId,
        int256 answer,
        uint256 startedAt,
        uint256 updatedAt,
        uint80 answeredInRound
    ) = feed.latestRoundData();

    // [YES] Check 1: Pri   require(success, "Call failed");
}

Gas Stipend Attacks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// transfer() and send() forward only 2300 gas
// This is NOT enough for:
// - Writing to storage (20,000 gas)
// - Emitting events (375+ gas per topic)
// - Complex receive() logic

// Attack: Make a contract whose receive() requires more than 2300 gas
// Any protocol using transfer() to send ETH to this contract will fail
// This can permanently lock funds if the protocol has no fallback

contract GasGriefingReceiver {
    uint
contract Victim {
    function callExternal(address target) external {
        // [NO] No gas limit on returndata copy
        (bool success, bytes memory data) = target.call{gas: 100000}("");
        // If target returns 1MB, copying it costs enormous gas
        // This can cause the caller's transaction to run out of gas
    }
}

// [YES] Fix: Limit returndata size
function callExternal(address target) external {
    (bool success, ) = target.call{gas: 100000}("");
    // Don't copy returndata — just check success
 ncoded metadata')
"

# Use solc-select to test with specific versions
pip3 install solc-select
solc-select install 0.8.13
solc-select use 0.8.13

13.8 Gas Griefing & Return Bomb

Return Bomb Attack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Attacker returns massive returndata to consume caller's memory expansion gas

contract ReturnBomber {
    fallback() external {
        assembly {
            // Return 1MB of data — caller pays for memory expansion
            return(0, 1000000)
        }
    }
}

// Victim contract:ge pointer | Storage corruption |
| 0.6.x | `calldata` slicing bug | Incorrect data access |
| Vyper 0.2.150.3.0 | Reentrancy lock bug | Reentrancy guard ineffective |

```bash
# Check for known compiler bugs
# https://docs.soliditylang.org/en/latest/bugs.html

# Extract compiler version from deployed bytecode
cast code 0xContract --rpc-url $ETH_RPC | python3 -c "
import sys, json
bytecode = sys.stdin.read().strip()
# Last 43 bytes are CBOR metadata
# Contains compiler version
print('Check last bytes for CBOR-eeived < amount) {
            console.log("Fee on transfer detected:", amount - received);
        }
    }
}

13.7 Solidity Compiler Bugs

Known Critical Compiler Bugs

Version Bug Impact
0.8.13–0.8.15 ABI encoding bug with nested arrays Incorrect calldata encoding
0.8.13–0.8.15 Optimizer bug with verbatim Incorrect code generation
0.7.0–0.8.16 abi.encode with bytes in optimizer Data corruption
0.4.x–0.5.x Uninitialized storame token  

Testing for Token Compatibility

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Foundry test template for token compatibility
contract TokenCompatibilityTest is Test {
    function test_feeOnTransfer(address token, address from, address to, uint256 amount) public {
        uint256 balBefore = IERC20(token).balanceOf(to);
        vm.prank(from);
        IERC20(token).transfer(to, amount);
        uint256 received = IERC20(token).balanceOf(to) - balBefore;
        // If received < amount, it's fee-on-transfer
        if (reccy |
| **Low decimals** | USDC (6), WBTC (8) | Precision loss in price calculations |
| **High decimals** | YAM (24) | Overflow risk in multiplication |
| **Transfer to zero** | Some tokens revert | DoS if protocol sends to address(0) |
| **Self-transfer** | Some tokens break | Accounting errors on `transfer(self, amount)` |
| **Flash mintable** | Some DeFi tokens | Temporary infinite supply |
| **Deflationary** | BOMB | Balance decreases over time |
| **Multiple entry points** | TUSD | Two addresses for saansfer** | STA, PAXG | Protocol credits more than received |
| **Rebasing** | stETH, AMPL, OHM | Balance changes without transfer events |
| **Approval race condition** | USDT | `approve(X)` then `approve(Y)`  front-run to spend both |
| **Blocklist** | USDC, USDT | Protocol funds frozen if contract is blacklisted |
| **Pausable** | USDC, USDT | Protocol DoS if token is paused |
| **Upgradeable** | USDC | Token behavior can change after audit |
| **ERC-777 hooks** | imBTC | `tokensReceived` callback = reentran that protocol makes external calls before updating state

13.6 Weird ERC-20 Edge Cases (Complete Reference)

The Weird ERC-20 Repository

github.com/d-xo/weird-erc20 catalogs all non-standard token behaviors. Every auditor must know these.

| Token Behavior | Example Tokens | Attack Vector | |—————|—————-|—————| | Missing return value | USDT (mainnet), BNB | transfer() reverts if caller uses bool return | | **Fee on trllback: → Protocol A’s balances are NOT yet updated → get_virtual_price() returns STALE (inflated) value → Attacker borrows from Protocol B using inflated collateral → Protocol A finishes updating balances → Attacker’s position is now undercollateralized

1
2
3
4
5
6
7
8
9
10
### Detection

```bash
# Look for view functions that read state that could be mid-update
# during an external call

# Slither doesn't catch this — manual review required
# Pattern: Protocol reads price/state from another protocol
#         ANDt has special permissions

13.5 Cross-Contract Reentrancy (Read-Only Reentrancy Deep Dive)

The Pattern

Read-only reentrancy is the most subtle and dangerous reentrancy variant. It doesn’t steal from the re-entered contract — it exploits stale state that another protocol reads.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Protocol A (Curve pool):
  remove_liquidity() → sends ETH → [callback] → updates balances

Protocol B (Lending protocol):
  Uses Curve's get_virtual_price() as collateral oracle

Attack:
  During Protocol A's ETH ca

**Real-world exploit:** Uniswap V3 Multicall (2021) — This exact pattern was identified. Uniswap's multicall is safe because it uses `payable` only on specific functions, but many forks got this wrong.

### Delegatecall in Multicall Context

```solidity
// When multicall uses delegatecall, msg.sender is preserved
// But if the called function uses msg.sender for access control,
// the multicall contract itself becomes the "caller" in some contexts

// This can bypass access control if the multicall contraccall {
    function multicall(bytes[] calldata data) external payable {
        for (uint i = 0; i < data.length; i++) {
            (bool success, ) = address(this).delegatecall(data[i]);
            require(success);
        }
    }

    function deposit() external payable {
        balances[msg.sender] += msg.value; // [NO] msg.value is the SAME for all calls!
    }
}

// Attack: Call multicall with 1 ETH, but include deposit() 10 times
// Each deposit() sees msg.value = 1 ETH → credited 10 ETH for 1 ETH sent
  1. The message is actually an EIP-2612 permit for all their tokens
  2. Attacker submits the permit + transferFrom in one tx
  3. User’s tokens are drained without them ever sending a transaction

Defense: Wallets should decode and display permit signatures clearly

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
---

## 13.4 Multicall Vulnerabilities

### msg.value Reuse in Multicall

```solidity
// Critical vulnerability: msg.value is shared across all calls in a multicall
// An attacker can reuse the same ETH value multiple times

contract VulnerableMulti
function depositWithPermit(uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external {
    // If permit was already called (front-run), this try/catch handles it gracefully
    try token.permit(msg.sender, address(this), amount, deadline, v, r, s) {} catch {}
    // The allowance is set either way — proceed with deposit
    token.transferFrom(msg.sender, address(this), amount);
}

Permit Phishing

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Attack:
1. Attacker creates a malicious dApp
2. User is asked to sign a "login message"sless approvals via signatures
// Vulnerability: permit() can be front-run

// Victim signs: permit(owner=Alice, spender=Bob, value=100, deadline=X)
// Attacker sees this in mempool
// Attacker front-runs with the SAME signature to call permit() first
// Then calls transferFrom() before Bob can

// This is NOT a vulnerability in permit itself — it's a design consideration
// Protocols that use permit() must handle the case where permit() was already called

// [YES] Correct pattern: wrap permit + action atomicallyId still listed at old price!
        market.buy{value: price}(tokenId);
        return this.onERC721Received.selector;
    }
}

ERC-1155 Batch Transfer Callbacks

1
2
3
// ERC-1155 safeBatchTransferFrom calls onERC1155BatchReceived
// Multiple tokens transferred in one call = multiple reentrancy opportunities
// Each token in the batch can trigger a different callback path

13.3 Permit / EIP-2612 Vulnerabilities

Permit Front-Running

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// EIP-2612 permit() allows gaiggers onERC721Received callback BEFORE state cleanup
        nft.safeTransferFrom(seller, msg.sender, tokenId);

        // State cleanup happens AFTER the callback — reentrancy window!
        delete sellers[tokenId];
        delete prices[tokenId];
        payable(seller).transfer(msg.value);
    }
}

// Attacker's contract:
contract NFTReentrancyAttacker {
    function onERC721Received(address, address, uint256 tokenId, bytes calldata)
        external returns (bytes4)
    {
        // Re-enter buy() — tokenulnerabilities

### ERC-721/1155 Callback Reentrancy

```solidity
// ERC-721 safeTransferFrom calls onERC721Received on the recipient
// This is a reentrancy vector if state isn't updated first

contract VulnerableNFTMarket {
    mapping(uint256 => address) public sellers;
    mapping(uint256 => uint256) public prices;

    function buy(uint256 tokenId) external payable {
        require(msg.value >= prices[tokenId], "Insufficient payment");
        address seller = sellers[tokenId];

        // [NO] NFT transfer trransient storage used as a "flag" that can be pre-set
contract VulnerableWithTransient {
    // Attacker can call setFlag() before calling privilegedAction()
    // in the same transaction
    function setFlag() external {
        assembly { tstore(0, 1) }
    }

    function privilegedAction() external {
        uint256 flag;
        assembly { flag := tload(0) }
        require(flag == 1, "Not authorized"); // [NO] Anyone can set this!
        // ... privileged logic
    }
}

13.2 Callback & Hook Vsactions but NOT between calls in the same tx | Reentrancy if guard logic is flawed |

| Cross-function transient state | Transient values set in one function are readable in another within the same tx | Unexpected state sharing | | Callback manipulation | Attacker sets transient values before calling a contract that reads them | Logic bypass | | Flash loan interaction | Transient storage persists through flash loan callbacks | State leakage across protocol boundaries |

1
2
3
4
5
6
7
8
// Vulnerable: Tonly for the duration of a transaction, then is automatically cleared.

```solidity
// Solidity 0.8.24+ syntax
assembly {
    tstore(slot, value)   // Write to transient storage
    value := tload(slot)  // Read from transient storage
}

Security Implications

Issue Description Impact
Reentrancy guard bypass If a reentrancy guard uses transient storage, it resets between transses & Advanced Attack Patterns  

Difficulty: Advanced → Expert

This module covers critical vulnerability classes and attack patterns that are absent from the core guide but appear regularly in competitive audits and real-world exploits. Mastering these is what separates a top-10% auditor from a top-1% auditor.


13.1 Transient Storage Vulnerabilities (EIP-1153)

What Is Transient Storage?

EIP-1153 (activated in Dencun, March 2024) introduces TSTORE/TLOAD opcodes — storage that persists # Module 13 — Missing Vulnerability Cla