WILL/USDT expired short overfunding exploit
Exploit Transactions
0xc12ccc3bdaf3f0ec1efa09d089a0c1dbad05519e1eb0fa6475ffcc6317cbde4d0xefe58a14fc0022872262678b358aaae64a26fe2389d09093eb14752ea99415e90x4a40421fc7849c0f4b27331d4ccff3a6721a362c97e6a94dcfba0e480a4e155dVictim Addresses
0x566777ed780dbbe17c130ae97b9fbc0a3ab829dfBSCLoss Breakdown
Similar Incidents
SellToken Short Oracle Manipulation
38%AI IPC destroy-sync mechanism drains IPC-USDT pair USDT reserves
35%Marketplace proxy 0x9b3e9b92 bug drains USDT and mints rewards
35%Public Mint Drains USDT Pair
34%BBX auto-burn sync flaw drains USDT from BBX pool
34%Public mint flaw drains USDT from c3b1 token pool
34%Root Cause Analysis
WILL/USDT expired short overfunding exploit
1. Incident Overview TL;DR
An adversary-controlled orchestrator contract on BNB Chain opened an oversized WILL/USDT short via a public leveraged trading contract, then triggered updateExpiredOrders() and settleExpiredPositions() to force the protocol to spend an inflated amount of USDT into the WILL/USDT AMM. A final withdrawAll(USDT) call moved the resulting USDT from the orchestrator to the attacker EOA, realising a net gain of exactly 45429486804908004411522 units of USDT at the expense of the trading contract’s users and WILL/USDT LPs.
The root cause is a logic bug in the victim trading contract’s expired-short accounting. updateExpiredOrders() overcounts each expired short by adding an extra 80% on top of usdtShorted when populating expiredNotClosedUSDT. settleExpiredPositions(uint256) then spends this inflated amount in a single PancakeSwap USDT→WILL swap, transferring excess USDT value from protocol stakeholders to whoever calls these public functions after opening expirable short positions.
2. Key Background
The victim protocol is a leveraged short and staking contract (trading) deployed at 0x566777ed780dbbe17c130ae97b9fbc0a3ab829df on BNB Chain (chainid 56). Users can open WILL/USDT short positions via placeSellOrder(uint256 usdtAmount, uint256 margin, uint256 minUsdtReceived). The function:
- Pulls
usdtAmount + marginUSDT from the caller. - Uses the WILL/USDT AMM pair at
0x1aaa8e1fd2f4137bbf83bd40d08746ce2862ed55(via PancakeSwap router) to sell WILL for USDT. - Stores per-order state in
sellOrders[orderId](fields includeusdtShorted,margin,tokenAmount,priceAtTimeOfSale,user,isActive,openTime,closeTime). - Tracks active orders in
activeOrders[orderId]and incrementsnextSellOrderId.
Expired shorts are intended to be handled in two steps:
updateExpiredOrders()scansactiveOrdersfromlastProcessedOrderIdin batches of sizebatchSize, finds orders whereorder.isActiveandorder.closeTime < block.timestamp, and aggregates their required USDT intoexpiredNotClosedUSDTwhile marking them inactive and advancinglastProcessedOrderId.settleExpiredPositions(uint256 minTokensToReceive)then uses the contract’s USDT balance, limited byexpiredNotClosedUSDT, to buy back WILL on PancakeSwap (USDT→token path), resettingexpiredNotClosedUSDTto zero afterward.
An external adversary deployed an orchestrator contract at 0x63b4de190c35f900bb7adf1a13d66fb1f0d624a1. Decompilation shows that its key entrypoints (selectors 0x7f14cf11, 0x1b51e969, 0xfa09e630, and the destroy function) are gated by require(tx.origin == owner), where owner is a stored address. Normal transaction history and the decompiled code show that this owner is the attacker EOA 0xb6911dee6a5b1c65ad1ac11a99aec09c2cf83c0e, which also deploys and later self-destructs the orchestrator.
This orchestrator is not privileged in the trading contract. It simply batches public calls into trading and the AMM: pre-funding and approvals, opening a large short via placeSellOrder, then later calling updateExpiredOrders() and settleExpiredPositions(uint256) in a single transaction, and finally withdrawing all USDT profit back to the attacker EOA.
3. Vulnerability Analysis & Root Cause Summary
The vulnerability class is incorrect settlement accounting for expired leveraged short positions. Conceptually, the protocol should only ever spend, per expired short, at most the outstanding borrowed notional (plus properly computed interest and fees) to close the position. Instead, updateExpiredOrders() systematically overfunds the internal USDT pool expiredNotClosedUSDT by adding an extra 80% of usdtShorted for each expired order.
Formally stated invariant:
- For every expired short position, the total USDT spent by the protocol to close the position (including any fees or penalties) must not exceed the initial USDT notional
usdtShortedprovided by the short seller, so that protocol-wide USDT outflows for expired shorts are bounded by accumulated margin and fee reserves.
Concrete breakpoint:
- In
updateExpiredOrders(), for each expiredorderId, the contract computestotalPosition = sellOrders[orderId].usdtShortedandadditionalFunds = totalPosition * 80 / 100, then updates a running total astotal += totalPosition + additionalFunds, deletesactiveOrders[orderId], and finally setsexpiredNotClosedUSDT = total. This overfundsexpiredNotClosedUSDTby 80% ofusdtShortedper order without adjusting any margin or liability fields.settleExpiredPositions(uint256)then spends the fullexpiredNotClosedUSDTamount via a PancakeSwap USDT→WILL swap, violating the value-conservation invariant and transferring excess USDT to the short side.
Because updateExpiredOrders() and settleExpiredPositions(uint256) are public and rely only on on-chain state, any unprivileged actor who can open expirable shorts on the trading contract can reproduce this overfunded settlement pattern. The observed exploit shows precisely one such ACT opportunity being exercised with a single large WILL/USDT short and one settlement cycle, yielding a net USDT profit of 45429486804908004411522 units to the adversary cluster.
4. Detailed Root Cause Analysis
Vulnerable components
- Victim leveraged trading contract
tradingat0x566777ed780dbbe17c130ae97b9fbc0a3ab829df:- Public function
placeSellOrder(uint256 usdtAmount, uint256 margin, uint256 minUsdtReceived)opens leveraged shorts by borrowing USDT, selling WILL into the AMM pair, and recording aSellOrderstruct. - Public function
updateExpiredOrders()loops overactiveOrdersstarting fromlastProcessedOrderIdup to a batch limitbatchSize. For each expired short it:- Reads
SellOrderfromsellOrders[orderId]. - Computes
totalPosition = sellOrders[orderId].usdtShorted. - Computes
additionalFunds = totalPosition * 80 / 100. - Accumulates
total += totalPosition + additionalFunds. - Clears
activeOrders[orderId]and advancescurrentOrderIdandlastProcessedOrderId. - After the loop, writes
expiredNotClosedUSDT = total.
- Reads
- Public function
settleExpiredPositions(uint256 minTokensToReceive):- Requires
expiredNotClosedUSDT > 0andIERC20(USDT).balanceOf(address(this)) >= expiredNotClosedUSDT. - Approves PancakeSwap router and calls
swapExactTokensForTokensSupportingFeeOnTransferTokens(expiredNotClosedUSDT, minTokensToReceive, [USDT, token], address(this), deadline). - Resets
expiredNotClosedUSDT = 0after the swap.
- Requires
- Public function
- WILL/USDT AMM pair at
0x1aaa8e1fd2f4137bbf83bd40d08746ce2862ed55:- Standard PancakeSwap V2-style pair holding WILL and USDT reserves that are manipulated by the victim contract’s selling and buying flows.
- Adversary-owned orchestrator contract at
0x63b4de190c35f900bb7adf1a13d66fb1f0d624a1:- Decompiled source shows
require(tx.origin == owner)gating for selectors0x7f14cf11(short-opening entrypoint),0x1b51e969(expired-settlement entrypoint),0xfa09e630(withdrawAll(address)), and the self-destruct function. - Internally wires these entrypoints to the trading contract and the AMM, but does not grant any special privileges on the trading contract itself.
- Decompiled source shows
How the bug manifests in code
In the verified trading source, the expired-short sweep logic is implemented as follows (simplified from the collected Contract.sol for readability):
uint256 public expiredNotClosedUSDT;
uint256 public lastProcessedOrderId;
uint256 public batchSize;
function updateExpiredOrders() public {
uint256 total = 0;
uint256 currentOrderId = lastProcessedOrderId;
uint256 processed = 0;
while (processed < batchSize && currentOrderId < nextSellOrderId) {
if (activeOrders[currentOrderId]) {
SellOrder storage order = sellOrders[currentOrderId];
if (order.isActive && order.closeTime < block.timestamp) {
uint256 totalPosition = order.usdtShorted;
uint256 additionalFunds = totalPosition * 80 / 100;
total += totalPosition + additionalFunds;
activeOrders[currentOrderId] = false;
}
}
currentOrderId++;
processed++;
}
expiredNotClosedUSDT = total;
lastProcessedOrderId = currentOrderId;
}
This implementation violates the stated invariant in two ways:
- It adds an extra
additionalFunds = usdtShorted * 80 / 100per expired order intoexpiredNotClosedUSDTwithout burning a corresponding liability or consuming margin. - It writes only the aggregated total into
expiredNotClosedUSDTand does not keep a precise mapping from expired orders to settlement amounts, so the subsequent settlement is a lump-sum USDT spend unconstrained by per-order accounting.
The paired settlement function uses this inflated value directly:
function settleExpiredPositions(uint256 minTokensToReceive) public nonReentrant {
require(expiredNotClosedUSDT > 0, "No funds to settle positions");
uint256 usdtAvailable = IERC20(USDT).balanceOf(address(this));
require(usdtAvailable >= expiredNotClosedUSDT, "Insufficient USDT available");
address[] memory path = new address[](2);
path[0] = USDT;
path[1] = token;
IERC20(USDT).approve(address(pancakeSwapRouter), expiredNotClosedUSDT);
pancakeSwapRouter.swapExactTokensForTokensSupportingFeeOnTransferTokens(
expiredNotClosedUSDT,
minTokensToReceive,
path,
address(this),
block.timestamp + 300
);
expiredNotClosedUSDT = 0;
}
Here, expiredNotClosedUSDT is treated as an authoritative lower bound on the amount of USDT to spend on buybacks. Because updateExpiredOrders() inflates it by 80% per expired order, settleExpiredPositions deterministically overspends USDT relative to the actual borrowed notional and any reasonable interest or fee schedule.
Evidence from traces and state diffs
- Pre-state tracer for
0xc12ccc3bdaf3f0ec1efa09d089a0c1dbad05519e1eb0fa6475ffcc6317cbde4d(block 39979797) showsnextSellOrderIdat the trading contract increasing from 10 to 11 and new storage entries undersellOrdersandactiveOrders, consistent with a new short opened viaplaceSellOrder. The AMM pair’s reserve slots also update, reflecting a large WILL sale into USDT. - Pre-state and post-state data for
0xefe58a14fc0022872262678b358aaae64a26fe2389d09093eb14752ea99415e9(block 39979801) show:activeOrdersfor the exploited order flipping from 1 to 0.lastProcessedOrderIdupdated, withexpiredNotClosedUSDTtransiently non-zero during execution but zero at the end of the tx (becausesettleExpiredPositionsis called within the same transaction).- USDT and WILL balances shifting from trading and the AMM pair to the orchestrator, in amounts matching the overfunded USDT spend implied by the 80% uplift.
- Combined
balance_diff.jsonfor the two seed transactions shows:- The orchestrator
0x63b4de…ends with exactly205153639715236281623315USDT and zero WILL after tx 2. - Trading contract
0x5667…has a net USDT gain of13854514520029004365887and a large WILL inventory, while the AMM pair loses a large amount of USDT and gains WILL.
- The orchestrator
These observations match the expected effect of applying an inflated expiredNotClosedUSDT through a single USDT→WILL swap, with the orchestrator configured as the ultimate USDT recipient.
5. Adversary Flow Analysis
Adversary-related cluster accounts
- EOA
0xb6911dee6a5b1c65ad1ac11a99aec09c2cf83c0e(BNB Chain 56):- Sender of all three exploit-related transactions.
- Deployer and destroyer of orchestrator
0x63b4de190c35f900bb7adf1a13d66fb1f0d624a1. - Final recipient of
205153639715236281623315USDT viawithdrawAll(USDT).
- Orchestrator contract
0x63b4de190c35f900bb7adf1a13d66fb1f0d624a1(BNB Chain 56):- Decompiled source shows
require(tx.origin == owner)gating exploit entrypoints andwithdrawAll. - Serves as intermediary holder of USDT profit between the settlement tx and the withdrawal tx.
- Decompiled source shows
Victim-related accounts
- WILL leveraged trading contract
tradingat0x566777ed780dbbe17c130ae97b9fbc0a3ab829df(verified source collected). - WILL/USDT AMM pair at
0x1aaa8e1fd2f4137bbf83bd40d08746ce2862ed55, representing WILL/USDT LP capital.
Lifecycle stages and transactions
-
Adversary priming and orchestrator setup (Tx 0xc12ccc3bdaf3f0ec1efa09d089a0c1dbad05519e1eb0fa6475ffcc6317cbde4d, block 39979797, BNB Chain 56)
- The attacker EOA calls the orchestrator with selector
0x7f14cf11and the WILL token address. - The orchestrator configures APPROVE flows and routes calls into
trading.placeSellOrder, opening a large WILL/USDT short from the adversary’s perspective. balance_diff.jsonshows the orchestrator sending159724152910328277211793USDT into the system,tradinggaining142206005229984470168758USDT, and the AMM pair gaining17518147680343807043035USDT while losing WILL. The orchestrator receives a large amount of WILL (3284690997786476824236791units), leaving the net cluster exposure as short USDT-long WILL relative to the AMM.- This stage sets up the state for an overfunded expired short, ensuring later
updateExpiredOrders()will see an expired order with substantialusdtShorted.
- The attacker EOA calls the orchestrator with selector
-
Inflating
expiredNotClosedUSDTand settling expired shorts (Tx 0xefe58a14fc0022872262678b358aaae64a26fe2389d09093eb14752ea99415e9, block 39979801, BNB Chain 56)- The same attacker EOA calls the orchestrator with selector
0x1b51e969. - Decompilation shows this entrypoint calling
trading.updateExpiredOrders()followed by a function that corresponds tosettleExpiredPositions(uint256). prestate_tracer_full.jsonfor this tx shows:activeOrdersslot for the exploited order flipping from 1 to 0.lastProcessedOrderIdupdated to 1.expiredNotClosedUSDTnon-zero inside the call, then reset to 0 at the end, consistent with the two-step sweep+settle pattern.
balance_diff.jsonshows:tradingUSDT balance drops from142246507916003663493149to13895017206048197690278(delta-128351490709955465802871).- WILL/USDT AMM pair USDT balance drops from
105120900137170629892524to28318751131889814072080(delta-76802149005280815820444). - The orchestrator’s USDT balance increases from 0 to
205153639715236281623315. - The orchestrator’s WILL balance returns to zero.
- These diffs are exactly consistent with
expiredNotClosedUSDThaving been set tousdtShorted + 0.8 * usdtShortedand then fully spent viasettleExpiredPositions, dumping the inflated USDT amount into the AMM and returning the proceeds to the orchestrator.
- The same attacker EOA calls the orchestrator with selector
-
Profit realization to attacker EOA (Tx 0x4a40421fc7849c0f4b27331d4ccff3a6721a362c97e6a94dcfba0e480a4e155d, block 39979822, BNB Chain 56)
- The attacker EOA calls
withdrawAll(address _token)on the orchestrator with the USDT token address. - Decompiled code for
withdrawAll:- Enforces
require(tx.origin == owner). - Reads the orchestrator’s entire token balance via an ERC-20
balanceOf. - Calls the token’s
transferto send the full balance toowner(the attacker EOA).
- Enforces
balance_diff_prestate_tracer.jsonfor this tx shows:- At BEP20USDT mapping key corresponding to the orchestrator, USDT balance decreases by
205153639715236281623315. - At the mapping key corresponding to the EOA, USDT balance increases by the same amount.
- At BEP20USDT mapping key corresponding to the orchestrator, USDT balance decreases by
- After this transfer, the orchestrator holds no USDT, and the attacker EOA holds the entire
205153639715236281623315USDT.
- The attacker EOA calls
Profit computation and ACT nature
- Summing the adversary cluster’s USDT across all three txs:
- Before tx 1, the orchestrator holds
159724152910328277211793USDT. - After tx 2 and tx 3, the EOA holds
205153639715236281623315USDT and the orchestrator holds zero. - Net delta:
205153639715236281623315 - 159724152910328277211793 = 45429486804908004411522USDT.
- Before tx 1, the orchestrator holds
- Summing
native_balance_deltas(BNB) for the attacker EOA across the three txs yields a total gas cost of44831452914590600wei (≈0.0448 BNB), negligible relative to the USDT profit. - All calls are standard, permissionless transactions to public functions on
tradingand a standard AMM, with the orchestrator merely enforcing attacker-only control viatx.origin == owner. Any other unprivileged actor can reproduce the strategy by deploying their own orchestrator (or equivalent batching contract), opening similarly structured shorts, and then callingupdateExpiredOrders()andsettleExpiredPositions(uint256)under the same on-chain conditions.
Selected trace-based evidence snippet
To illustrate the on-chain value movements in the critical settlement tx, here is an excerpt from balance_diff.json for tx 0xefe58a14fc0022872262678b358aaae64a26fe2389d09093eb14752ea99415e9 (BNB Chain 56), focusing on USDT balances:
[
{
"token": "0x55d398326f99059ff775485246999027b3197955",
"holder": "0x566777ed780dbbe17c130ae97b9fbc0a3ab829df",
"before": "142246507916003663493149",
"after": "13895017206048197690278",
"delta": "-128351490709955465802871",
"balances_slot": "1",
"slot_key": "0x80a5e16cd7a6626c482f9b77b3cef351c7968037b145aeb5cecf3d8642c6fef8",
"contract_name": "BEP20USDT"
},
{
"token": "0x55d398326f99059ff775485246999027b3197955",
"holder": "0x1aaa8e1fd2f4137bbf83bd40d08746ce2862ed55",
"before": "105120900137170629892524",
"after": "28318751131889814072080",
"delta": "-76802149005280815820444",
"balances_slot": "1",
"slot_key": "0xc82a3b67d2d8fcd4b7e14a8177520f300b53e2cfea7fb6fc350b7b4a96b11c9e",
"contract_name": "BEP20USDT"
},
{
"token": "0x55d398326f99059ff775485246999027b3197955",
"holder": "0x63b4de190c35f900bb7adf1a13d66fb1f0d624a1",
"before": "0",
"after": "205153639715236281623315",
"delta": "205153639715236281623315",
"balances_slot": "1",
"slot_key": "0xb584b7ab01ee700baf86e0cb44f8db636a7f1af8b8ac3d09867684b6357532b6",
"contract_name": "BEP20USDT"
}
]
This snippet shows trading and the AMM losing USDT while the orchestrator gains exactly 205153639715236281623315 USDT, matching the inflated settlement amount derived from the victim contract’s bug.
6. Impact & Losses
Across exploit txs 1 and 2, balance diffs show trading contract 0x566777ed780dbbe17c130ae97b9fbc0a3ab829df gaining 13854514520029004365887 units of USDT while the WILL/USDT AMM pair 0x1aaa8e1fd2f4137bbf83bd40d08746ce2862ed55 loses 59284001324937008777409 units of USDT. The adversary cluster (EOA plus orchestrator) gains 45429486804908004411522 units of USDT, with the residual USDT remaining in trading.
This reflects a deterministic transfer of 45429486804908004411522 units of USDT from protocol stakeholders (trading users and WILL/USDT LPs) to the adversary cluster. An additional small loss in BNB gas fees equal to 44831452914590600 wei is paid by the attacker EOA, but this is negligible compared to the USDT profit and does not change the qualitative impact.
7. References
- [1] Seed tx
0xc12ccc3bdaf3f0ec1efa09d089a0c1dbad05519e1eb0fa6475ffcc6317cbde4dmetadata and balance diffs:artifacts/root_cause/seed/56/0xc12ccc3bdaf3f0ec1efa09d089a0c1dbad05519e1eb0fa6475ffcc6317cbde4d - [2] Seed tx
0xefe58a14fc0022872262678b358aaae64a26fe2389d09093eb14752ea99415e9metadata and balance diffs:artifacts/root_cause/seed/56/0xefe58a14fc0022872262678b358aaae64a26fe2389d09093eb14752ea99415e9 - [3]
withdrawAll(USDT)tx0x4a40421fc7849c0f4b27331d4ccff3a6721a362c97e6a94dcfba0e480a4e155dprestate tracer and balance diff:artifacts/root_cause/data_collector/iter_3/tx/56/0x4a40421fc7849c0f4b27331d4ccff3a6721a362c97e6a94dcfba0e480a4e155d - [4] Trading contract
0x566777ed780dbbe17c130ae97b9fbc0a3ab829dfverified source and layout:artifacts/root_cause/data_collector/iter_2/contract/56/0x566777ed780dbbe17c130ae97b9fbc0a3ab829df - [5] Orchestrator
0x63b4de190c35f900bb7adf1a13d66fb1f0d624a1decompiled source:artifacts/root_cause/data_collector/iter_1/contract/56/0x63b4de190c35f900bb7adf1a13d66fb1f0d624a1/decompile