0x63ac9bc4e53dbcfaac3a65cb90917531cfdb1c79c0a334dda3f06e42373ff3a00x061944c0f3c2d7dabafb50813efb05c4e0c952e1BSCAt BSC block 44555338, transaction 0x63ac9bc4e53dbcfaac3a65cb90917531cfdb1c79c0a334dda3f06e42373ff3a0 let an unprivileged attacker drain nearly all MFT held by Pledge at 0x061944c0f3c2d7dabafb50813efb05c4e0c952e1. The attacker EOA 0x59367b057055fd5d38ab9c5f0927f45dc2637390 called helper contract 0x4aa0548019bfecd343179d054b1c7fa63e1e0b6c, which forwarded into Pledge.swapTokenU(uint256,address). That call sold 989644232342705000000000000 MFT from the Pledge contract through PancakeRouter and delivered 14994304057732608091714 USDT to the attacker.
The root cause is a direct access-control failure inside Pledge: swapTokenU(uint256,address) is public, uses Pledge-held MFT as the input asset, and lets the external caller choose the USDT recipient. The exploit did not require owner privileges, private keys, attacker-specific bytecode, or any privileged integration path.
Pledge is a BSC contract that holds MFT and USDT and integrates PancakeRouter 0x10ED43C718714eb63d5aA57B78B54704E256024E for swaps. Its verified source shows an Ownable base with an owners() array and modifier, plus several owner-gated administrative functions such as , , and .
onlyOwnersetUrlsetTokenaddresswithdrawThe same source also exposes swapTokenU(uint256,address) as a public helper. Unlike withdraw, it is not protected by onlyOwner. Instead, it approves PancakeRouter for Pledge's MFT, constructs the MFT -> USDT path, and executes swapExactTokensForTokensSupportingFeeOnTransferTokens with caller-chosen _target as the recipient.
MFT itself is fee-on-transfer when interacting with the main pair. Its _transfer implementation sends 3% to a target address and 97% to the pair whenever recipient == _mainPair || sender == _mainPair. That fee logic explains why the observed draining call left 1e18 MFT dust instead of reducing Pledge to exact zero.
This incident is an ACT ATTACK, not a benign MEV event. The violated invariant is straightforward: only protocol-authorized owners should be able to spend or swap treasury assets held by Pledge, and any conversion of those assets should route proceeds through protocol-controlled accounting flows. Pledge.swapTokenU(uint256,address) breaks that invariant because it is a public spend primitive over contract-held MFT.
The relevant victim-side code path is:
function swapTokenU(uint256 amount, address _target) public {
IERC20(_token).approve(address(_swapRouter), MAX);
address[] memory path = new address[](2);
path[0] = _token;
path[1] = _USDT;
_swapRouter.swapExactTokensForTokensSupportingFeeOnTransferTokens(
amount,
0,
path,
_target,
block.timestamp
);
}
The same verified source contains owner-gated functions such as:
function withdraw(address _tokens, address _target, uint256 _amount) public onlyOwner {
require(ERC20(_tokens).balanceOf(address(this)) >= _amount, "no balance");
IERC20(_tokens).transfer(_target, _amount);
}
That contrast shows the exact code-level breakpoint: the contract recognized treasury movement as privileged in withdraw, but failed to apply the same restriction to swapTokenU. Because the function operates on assets already held by Pledge, any non-owner caller could monetize the treasury balance into attacker-directed USDT.
The pre-state at block 44555337 is well defined. The collected balance diff and validator replay both show that Pledge held 989644233342705000000000000 MFT and the attacker held 0 USDT before the exploit. The owner set at that state was [0x05A1612597766E9dEb1737C0387d76cEAE242658], which is different from the attacker.
The exploit transaction then followed a single deterministic path. The transaction metadata shows the EOA sender was 0x59367b057055fd5d38ab9c5f0927f45dc2637390, and the transaction target was helper contract 0x4aa0548019bfecd343179d054b1c7fa63e1e0b6c. The opcode-level trace shows the helper forwarding into Pledge.swapTokenU with the precise amount 989644232342705000000000000 and with the attacker EOA as recipient:
0x4AA0548019bFECd343179d054b1c7Fa63e1e0B6c::280d41a0(...)
└─ 0x061944c0f3c2d7DABafB50813Efb05c4e0c952e1::swapTokenU(
989644232342705000000000000,
0x59367B057055FD5d38AB9c5F0927f45dC2637390
)
From there, the trace records the victim contract approving PancakeRouter and executing the swap:
Pledge::swapTokenU(...)
├─ MFT::approve(PancakeRouter, MAX)
└─ PancakeRouter::swapExactTokensForTokensSupportingFeeOnTransferTokens(
989644232342705000000000000,
0,
[MFT, USDT],
attacker,
...
)
The balance-diff artifact confirms the exact economic effect:
{
"token": "0x4e5a19335017d69c986065b21e9dfe7965f84413",
"holder": "0x061944c0f3c2d7dabafb50813efb05c4e0c952e1",
"before": "989644233342705000000000000",
"after": "1000000000000000000",
"delta": "-989644232342705000000000000"
}
{
"token": "0x55d398326f99059ff775485246999027b3197955",
"holder": "0x59367b057055fd5d38ab9c5f0927f45dc2637390",
"before": "0",
"after": "14994304057732608091714",
"delta": "14994304057732608091714"
}
The MFT token code explains the residual dust. Its _transfer path applies a 3% fee when tokens are sent to the main pair:
if(recipient == _mainPair || sender == _mainPair){
super._transfer(sender, _targetAdress, amount * 3 / 100);
super._transfer(sender, recipient, amount * 97 / 100);
return;
}
Because swapTokenU sold almost the entire Pledge-held MFT into the pair, the attacker-selected amount left the observed 1e18 remainder. That behavior is consistent with both the trace and the post-state balance diff.
The adversary execution was a one-transaction ACT realization.
0x59367b057055fd5d38ab9c5f0927f45dc2637390 submitted transaction 0x63ac9bc4e53dbcfaac3a65cb90917531cfdb1c79c0a334dda3f06e42373ff3a0 on BSC.0x4aa0548019bfecd343179d054b1c7fa63e1e0b6c. The helper is visible in the trace, but it is not the source of privilege; it only automated parameter selection and call forwarding.Pledge.swapTokenU(989644232342705000000000000, attacker).Pledge approved PancakeRouter and sold treasury-held MFT through the MFT/USDT route.14994304057732608091714 USDT to the attacker EOA.Pledge with only 1000000000000000000 MFT.The critical point is that every step after the helper is driven by publicly reachable victim code. Any other unprivileged address could have executed the same treasury drain directly against swapTokenU from the same pre-state.
The victim contract Pledge lost effectively its full MFT treasury inventory. The measured MFT loss was 989644232342705000000000000 raw units (989,644,232.342705 MFT at 18 decimals), leaving only 1e18 dust in the contract. The attacker simultaneously realized 14994304057732608091714 raw USDT units from the swap output.
The operational impact is broader than the single recipient transfer: a public caller could spend protocol-held inventory and redirect proceeds externally without any ownership or accounting gate. That means the treasury asset-management boundary inside Pledge was completely broken.
0x63ac9bc4e53dbcfaac3a65cb90917531cfdb1c79c0a334dda3f06e42373ff3a0Pledge at 0x061944c0f3c2d7dabafb50813efb05c4e0c952e10x59367b057055fd5d38ab9c5f0927f45dc26373900x4aa0548019bfecd343179d054b1c7fa63e1e0b6cMFT at 0x4e5a19335017d69c986065b21e9dfe7965f84413, USDT at 0x55d398326f99059ff775485246999027b31979550x10ED43C718714eb63d5aA57B78B54704E256024E