EIP-6780 Vulnerability Patterns

December 23, 2025

EIP-6780 (SELFDESTRUCT only in same transaction) was introduced in the Dencun upgrade (March 2024) to modify the behavior of the SELFDESTRUCT opcode. Before Dencun, SELFDESTRUCT would delete the contract’s code and storage and send all remaining ETH to a target address. After Dencun, SELFDESTRUCT only deletes code and storage if called in the same transaction the contract was created; otherwise, it only sends the balance without deleting anything.

This article summarizes EIP-6780 vulnerability patterns with real audit cases to help you quickly understand the security implications.

EIP-6780 Core Concepts

What Changes with EIP-6780?

┌─────────────────────────────────────────────────────────────────┐
│                    Before Dencun (Pre EIP-6780)                 │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  SELFDESTRUCT behavior (always the same):                       │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │ 1. Send all ETH balance to target address               │   │
│  │ 2. Delete contract code                                 │   │
│  │ 3. Delete all storage                                   │   │
│  │ 4. Contract address becomes empty account               │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│  extcodesize(addr) == 0 means "no contract at this address"    │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│                    After Dencun (Post EIP-6780)                 │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Case 1: CREATE and SELFDESTRUCT in the SAME transaction       │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │                                                         │   │
│  │  TX 0x123...                                            │   │
│  │  ┌───────────────────────────────────────────────────┐ │   │
│  │  │  Step 1: CREATE contract at 0xABC                 │ │   │
│  │  │  Step 2: ... do something ...                     │ │   │
│  │  │  Step 3: SELFDESTRUCT 0xABC                       │ │   │
│  │  └───────────────────────────────────────────────────┘ │   │
│  │                                                         │   │
│  │  Result: Same behavior as before Dencun                 │   │
│  │  ✓ ETH sent to target                                   │   │
│  │  ✓ Code deleted                                         │   │
│  │  ✓ Storage deleted                                      │   │
│  │                                                         │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│  Case 2: Contract already deployed, SELFDESTRUCT in later tx   │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │                                                         │   │
│  │  TX 0x111... (earlier)                                  │   │
│  │  ┌───────────────────────────────────────────────────┐ │   │
│  │  │  CREATE contract at 0xABC                         │ │   │
│  │  └───────────────────────────────────────────────────┘ │   │
│  │                                                         │   │
│  │  TX 0x222... (later, could be days/months/years later)  │   │
│  │  ┌───────────────────────────────────────────────────┐ │   │
│  │  │  SELFDESTRUCT 0xABC                               │ │   │
│  │  └───────────────────────────────────────────────────┘ │   │
│  │                                                         │   │
│  │  Result: SELFDESTRUCT is nerfed!                        │   │
│  │  ✓ ETH sent to target                                   │   │
│  │  ✗ Code NOT deleted (contract still exists!)            │   │
│  │  ✗ Storage NOT deleted                                  │   │
│  │                                                         │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│  Security insight:                                              │
│  In Case 1, attacker can CREATE a contract, pass extcodesize   │
│  check, then SELFDESTRUCT - all in ONE transaction.            │
│  The contract disappears after tx ends!                        │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Why This Matters

The ability to create and selfdestruct a contract in the same transaction creates a timing attack vector:

  1. Create contract → extcodesize > 0
  2. Pass any “is this a contract?” check ✓
  3. Selfdestruct in same tx → code deleted
  4. Later calls to this address fail unexpectedly

Vulnerability Patterns

1. Selfdestruct Bypass of Contract Existence Check

Prerequisite Knowledge:

Many protocols check extcodesize(address) > 0 to verify an address is a valid contract before storing it as a callback target. The assumption is: “if extcodesize > 0 now, this address will always be a contract.”

EIP-6780 breaks this assumption: a contract can be created, pass the extcodesize check, then immediately selfdestruct in the same transaction.

Root Cause:

The contract assumes that checking extcodesize > 0 is sufficient to ensure an address will always contain contract code. However, EIP-6780 allows a contract to selfdestruct in the same transaction it was created, causing subsequent external calls to that address to fail.

Attack Vector:

A malicious lender (as a contract) accepts a borrower’s loan bid, then in a single transaction: deploys a Listener contract, registers it via setRepaymentListenerForBid() (passing the extcodesize > 0 check), and immediately calls selfdestruct on the Listener. Since EIP-6780 allows selfdestruct to delete code within the same transaction, the Listener address now has no code. When the borrower later calls repayLoan(), the protocol’s try IListener(listener).repayLoanCallback() {} catch {} triggers a full revert (not caught!) because calling a no-code address via interface reverts entirely. The borrower cannot repay, eventually defaults, and the lender claims their collateral.

Source:

  • [sherlock/2024-11-teller-finance-update] Malicious lender can prevent borrower from repayment due to try/catch block revert (issue-39.md)
    • teller-protocol-v2-audit-2024/packages/contracts/contracts/TellerV2.sol

      Business Scenario:

      Teller Finance - P2P Lending Protocol
      ┌────────────────────────────────────────────────────────────────┐
      │                                                                │
      │  What is Teller?                                               │
      │  A peer-to-peer lending protocol where borrowers and lenders  │
      │  can create collateralized loans without intermediaries.      │
      │                                                                │
      │  Key Terms:                                                    │
      │  • Borrower: Needs money, provides collateral as security     │
      │  • Lender: Provides money, earns interest, can claim          │
      │            collateral if borrower defaults                    │
      │  • Bid: A loan request containing terms (amount, duration,    │
      │         APR, collateral type/amount)                          │
      │                                                                │
      │  Core Flow:                                                    │
      │  ┌──────────────────────────────────────────────────────────┐ │
      │  │                                                          │ │
      │  │  1. Borrower submits bid (submitBid)                     │ │
      │  │     → Specifies: token, amount, duration, APR, collateral│ │
      │  │     → State: PENDING                                     │ │
      │  │                                                          │ │
      │  │  2. Lender accepts bid (lenderAcceptBid)                 │ │
      │  │     → Transfers principal to borrower                    │ │
      │  │     → Collateral locked in escrow                        │ │
      │  │     → State: ACCEPTED                                    │ │
      │  │                                                          │ │
      │  │  3. Borrower repays loan (repayLoan)                     │ │
      │  │     → Pays principal + interest to lender                │ │
      │  │     → Gets collateral back                               │ │
      │  │     → State: PAID                                        │ │
      │  │                                                          │ │
      │  │  4. If borrower defaults (misses payment deadline)       │ │
      │  │     → Lender can claim collateral (lenderCloseLoan)      │ │
      │  │     → State: CLOSED                                      │ │
      │  │                                                          │ │
      │  └──────────────────────────────────────────────────────────┘ │
      │                                                                │
      │  Repayment Listener Feature (where vulnerability exists):     │
      │  ┌──────────────────────────────────────────────────────────┐ │
      │  │                                                          │ │
      │  │  Lenders can register a "listener" contract to receive   │ │
      │  │  callbacks when borrowers make repayments.               │ │
      │  │                                                          │ │
      │  │  Use case: Lender pools, automated reinvestment, etc.    │ │
      │  │                                                          │ │
      │  │  setRepaymentListenerForBid(_bidId, _listener):          │ │
      │  │    1. Check caller is the lender                         │ │
      │  │    2. Check _listener has code (extcodesize > 0)         │ │
      │  │    3. Store listener address                             │ │
      │  │                                                          │ │
      │  │  When repayLoan() is called:                             │ │
      │  │    → Protocol calls listener.repayLoanCallback()         │ │
      │  │    → Wrapped in try/catch to prevent griefing            │ │
      │  │    → BUT: try/catch doesn't catch calls to no-code addr! │ │
      │  │                                                          │ │
      │  └──────────────────────────────────────────────────────────┘ │
      │                                                                │
      └────────────────────────────────────────────────────────────────┘
      
      Attack Flow:
      ┌────────────────────────────────────────────────────────────────┐
      │                                                                │
      │  Attacker (Lender) Goal: Force borrower to default and        │
      │  claim their collateral without letting them repay.           │
      │                                                                │
      │  Step 1: Lender accepts a loan bid normally                   │
      │  ┌──────────────────────────────────────────────────────────┐ │
      │  │  lenderAcceptBid(bidId)                                  │ │
      │  │  → Borrower receives principal                           │ │
      │  │  → Borrower's collateral locked                          │ │
      │  └──────────────────────────────────────────────────────────┘ │
      │                                                                │
      │  Step 2: Execute attack in ONE transaction                    │
      │  ┌──────────────────────────────────────────────────────────┐ │
      │  │                                                          │ │
      │  │  // Lender is a malicious contract:                      │ │
      │  │  contract MaliciousLender {                              │ │
      │  │      function attack(address teller, uint256 bidId)      │ │
      │  │          external                                        │ │
      │  │      {                                                   │ │
      │  │          // 1. Deploy listener                           │ │
      │  │          Listener l = new Listener();                    │ │
      │  │                                                          │ │
      │  │          // 2. Register it (passes extcodesize check)    │ │
      │  │          //    msg.sender = this = lender, passes check  │ │
      │  │          teller.setRepaymentListenerForBid(bidId,        │ │
      │  │              address(l));                                │ │
      │  │                                                          │ │
      │  │          // 3. Destroy in same tx → code deleted         │ │
      │  │          l.destroy();                                    │ │
      │  │      }                                                   │ │
      │  │  }                                                       │ │
      │  │                                                          │ │
      │  │  contract Listener {                                     │ │
      │  │      function destroy() external {                       │ │
      │  │          selfdestruct(payable(msg.sender));              │ │
      │  │      }                                                   │ │
      │  │  }                                                       │ │
      │  │                                                          │ │
      │  └──────────────────────────────────────────────────────────┘ │
      │                                                                │
      │  Step 3: Borrower tries to repay but fails                    │
      │  ┌──────────────────────────────────────────────────────────┐ │
      │  │  borrower.repayLoan(bidId, amount)                       │ │
      │  │    → _sendOrEscrowFunds() called                         │ │
      │  │    → try listener.repayLoanCallback() {} catch {}        │ │
      │  │    → Listener address has no code!                       │ │
      │  │    → try/catch REVERTS entire transaction!               │ │
      │  │    → Repayment fails                                     │ │
      │  └──────────────────────────────────────────────────────────┘ │
      │                                                                │
      │  Step 4: Loan defaults, lender claims collateral              │
      │  ┌──────────────────────────────────────────────────────────┐ │
      │  │  // After default duration passes...                     │ │
      │  │  lender.lenderCloseLoan(bidId)                           │ │
      │  │  → Collateral transferred to lender                      │ │
      │  │  → Borrower loses collateral despite wanting to repay    │ │
      │  └──────────────────────────────────────────────────────────┘ │
      │                                                                │
      └────────────────────────────────────────────────────────────────┘
      
      Mitigation:
      ┌────────────────────────────────────────────────────────────────┐
      │                                                                │
      │  Use low-level .call instead of try/catch.                     │
      │  Unlike try/catch, .call to a non-existent contract returns    │
      │  success = true (it doesn't revert).                           │
      │                                                                │
      │  // Before (vulnerable):                                       │
      │  try IListener(addr).callback(...) {} catch {}                 │
      │                                                                │
      │  // After (fixed):                                             │
      │  (bool success, ) = addr.call(                                 │
      │      abi.encodeWithSelector(IListener.callback.selector, ...)  │
      │  );                                                            │
      │  // success = true even if addr has no code                    │
      │                                                                │
      └────────────────────────────────────────────────────────────────┘
      // Vulnerability: try/catch doesn't catch calls to non-existent contracts
      if (loanRepaymentListener != address(0)) {
          require(gasleft() >= 80000, "NR gas");
          try
              ILoanRepaymentListener(loanRepaymentListener).repayLoanCallback{
                  gas: 80000
              }(
                  _bidId,
                  _msgSenderForMarket(bid.marketplaceId),
                  _payment.principal,
                  _payment.interest
              )
          {} catch {}
          // If loanRepaymentListener has no code, this REVERTS entirely!
      }
      
      function setRepaymentListenerForBid(uint256 _bidId, address _listener) external {
          uint256 codeSize;
          assembly {
              codeSize := extcodesize(_listener)
          }
          require(codeSize > 0, "Not a contract");
          // Attacker's contract passes this check, then selfdestructs
      
          address sender = _msgSenderForMarket(bids[_bidId].marketplaceId);
          require(sender == getLoanLender(_bidId), "Not lender");
      
          repaymentListenerForBid[_bidId] = _listener;
      }

EIP-6780 Audit Checklist

High-Risk Scenarios: Callback registrations, listener patterns, any code that stores external contract addresses for later calls.

# Check Item Related Pattern
Does the code store an address after checking extcodesize? The contract could selfdestruct after passing the check Pattern 1
Does try/catch wrap calls to user-provided addresses? Calls to non-existent contracts revert entirely, not caught by catch Pattern 1
Can users register callback contracts? Consider that the contract may not exist when the callback is invoked Pattern 1