LinkdaoDex USDT Pair Drain
Exploit Transactions
0x4ed59e3013215c272536775a966f4365112997a6eec534d38325be014f2e15eeVictim Addresses
0x6524a5fd3fec179db3b3c1d21f700da7abe6b0deBSCLoss Breakdown
Similar Incidents
Public Mint Drains USDT Pair
43%CS Pair Balance Burn Drain
40%StarlinkCoin Pair Drain
40%SellToken Arbitrary-Pair LP Drain
38%T3913 Pair-Skim Referral Drain
37%MetaverseToken fee misconfiguration drains USDT from Pancake pair
37%Root Cause Analysis
LinkdaoDex USDT Pair Drain
1. Incident Overview TL;DR
On BSC block 33527745, exploit tx 0x4ed59e3013215c272536775a966f4365112997a6eec534d38325be014f2e15ee drained the USDT side of LinkdaoDex pair 0x6524a5fd3fec179db3b3c1d21f700da7abe6b0de. The attacker-controlled helper contract 0x721a66c7767103e7dcacf8440e8dd074edff40a8, which had been deployed earlier in tx 0x6402eecdd3bf15cd2b954b422dd9fe737fb06bc60549c24cd38a30229f6b3bd6, received 29663356140000000000000 USDT from the pair, returned only 986000000000000000 USDT during the callback, and then received another 986000000000000 USDT fee rebate after reserves had already been updated.
The root cause is a math bug in LinkdaoDexPair.swap. The contract scales the adjusted balances by totalFeePercentage = 10000, but it compares the result to reserve0 * reserve1 * 1e6 instead of reserve0 * reserve1 * 10000^2. That lowers the effective invariant threshold by 100x and lets any unprivileged caller with a callback-capable contract withdraw almost the entire USDT reserve while repaying only dust. The helper ended the transaction with 29662357126000000000000 raw USDT units, and after valuing gas through the pre-state PancakeSwap USDT/WBNB pool, the net profit remains 29661985011081741594147 raw USDT units.
2. Key Background
LinkdaoDex is a Uniswap-V2-style AMM. The victim pair 0x6524a5fd3fec179db3b3c1d21f700da7abe6b0de is the USDT/LKD market, where token0 = USDT (0x55d398326f99059ff775485246999027b3197955) and token1 = LKD (0xaf027427dc6d31a3e7e162a710a5fe27e63e275f). The pair is not directly explorer-verified, but BscScan’s verified source bundle for factory 0x717d0a192f4c5f9bc8a2d45b01d696dab2bd1b7a includes the deployed LinkdaoDexPair.sol implementation.
At the pre-state immediately after block 33527744, the pair held reserves of 29963000000000000000000 USDT and 48277499838300344044804 LKD. The governing factory exposed feePercentage = 25, buyBackLkdPercentage = 3000, treasuryPercentage = 1000, and totalFeePercentage = 10000. LinkdaoDex also supports flash-swap style callbacks through LinkdaoDexCall(address,uint256,uint256,bytes), so any public caller can ask the pair to transfer output tokens first and settle later in the callback.
The adversary model here is fully permissionless. The exploit sender was a normal EOA, no protocol role was needed, and the only attacker-side requirement was a self-controlled helper contract that implements the callback interface and can return a small amount of the borrowed token.
3. Vulnerability Analysis & Root Cause Summary
This is an ATTACK case caused by a broken constant-product invariant in LinkdaoDexPair.swap, not by privileged access. The pair computes balance0Adjusted and balance1Adjusted using the live factory denominator totalFeePercentage = 10000, so both adjusted-balance terms are scaled by 10000. A correct Uniswap-V2-style invariant must therefore compare those adjusted terms against reserve0 * reserve1 * 10000^2.
Instead, the deployed implementation hard-codes 1e6, which is only correct for a 1000-based denominator. The bug makes the right-hand side 100x too small, so the pair accepts swaps that repay only about 1% of the value required by the intended invariant. The exploit transaction is a direct realization of that gap: the helper withdraws almost the full USDT reserve, returns only 1 USDT during the callback, still passes the buggy inequality, and keeps the rest. Because the callback path is public and the exploit only needs on-chain state plus a generic helper contract, the opportunity is ACT.
4. Detailed Root Cause Analysis
The verified factory bundle shows both the fee configuration and the vulnerable pair logic:
uint256 private TOTAL_FEE_PERCENTAGE = 10000; // 100%
uint256 feePercentage = ILinkdaoDexFactory(factory).getFeePercentage();
uint256 totalFeePercentage = ILinkdaoDexFactory(factory).getTotalFeePercentage();
uint256 balance0Adjusted = balance0 * totalFeePercentage - amount0In * feePercentage;
uint256 balance1Adjusted = balance1 * totalFeePercentage - amount1In * feePercentage;
require(
balance0Adjusted * balance1Adjusted >=
uint256(_reserve0) * _reserve1 * 1e6,
"LinkdaoDex: K"
);
That code is dimensionally inconsistent. Since each adjusted-balance term is multiplied by 10000, the invariant should compare against reserve0 * reserve1 * 10000 * 10000, not reserve0 * reserve1 * 1000000.
Using the actual exploit-state values from the trace and receipt:
reserve0 = 29963000000000000000000
reserve1 = 48277499838300344044804
amount0Out = 29663356140000000000000
amount0In = 986000000000000000
balance0 = 300629860000000000000
balance1 = 48277499838300344044804
balance0Adjusted = 3006273950000000000000000
balance1Adjusted = 482774998383003440448040000
buggyLhs = 1451353901350115365779318980558000000000000000000000
buggyRhs = 1446538727654993208614462252000000000000000000000000
correctRhs = 144653872765499320861446225200000000000000000000000000
The exploit passes because buggyLhs >= buggyRhs, but it would fail immediately under the correct invariant because buggyLhs < correctRhs. The ratio is roughly 1:99, which matches the observed near-total drain.
The transaction trace and receipt logs show the flow clearly:
0x6524...b0de::swap(29663356140000000000000, 0, 0x721a...40a8, 0x313233)
BEP20USDT::transfer(0x721a...40a8, 29663356140000000000000)
0x721a...40a8::LinkdaoDexCall(...)
Transfer pair -> helper : 29663356140000000000000 USDT
Transfer helper-> pair : 986000000000000000 USDT
Sync reserve0,reserve1 : 300629860000000000000 / 48277499838300344044804
Transfer pair -> helper : 986000000000000 USDT
The last transfer is important. The pair calls _update before paying the rebate to msg.sender, so reserve storage is left higher than the pair’s real USDT balance by 986000000000000 raw units immediately after the exploit. That accounting drift is a secondary effect of the same vulnerable swap path.
5. Adversary Flow Analysis
The exploit is a single-chain, two-transaction attacker sequence:
- In tx
0x6402eecdd3bf15cd2b954b422dd9fe737fb06bc60549c24cd38a30229f6b3bd6at block33527727, EOA0xdf6b0200b4e1bc4a310f33df95a9087cc2c79038deployed helper0x721a66c7767103e7dcacf8440e8dd074edff40a8. - Storage slot
0of that helper contains the same EOA address at block33527744, confirming control. - In tx
0x4ed59e3013215c272536775a966f4365112997a6eec534d38325be014f2e15ee, the EOA called the helper, which then calledpair.swap(...)with itself as the callback recipient. - The pair optimistically transferred out
29663356140000000000000USDT. - During
LinkdaoDexCall, the helper returned only986000000000000000USDT, just enough to satisfy the buggy1e6-scaled inequality. - After
_update, the pair paid a986000000000000USDT rebate back tomsg.sender, which further improved attacker profit and left reserves stale. - The helper finished the transaction with
29662357126000000000000more raw USDT units, while the pair retained only300628874000000000000.
Nothing in this sequence depends on the original attacker EOA or original helper bytecode. Any searcher could deploy a fresh callback contract, target the same public pair state, and realize the same profit condition.
6. Impact & Losses
The direct loss is the USDT drained from the LinkdaoDex USDT/LKD pool:
- Token:
USDT - Raw loss amount:
29662357126000000000000 - Decimals:
18 - Display amount:
29662.357126USDT
The pair’s real USDT balance moved from 29962986000000000000000 before the exploit to 300628874000000000000 after it, leaving roughly 1% of the original USDT side in place. The exploit therefore removed about 99% of the pair’s USDT liquidity in one transaction.
Using the transaction receipt gas usage (147214) and effective gas price (10000000000 wei), gas cost was 1472140000000000 wei. Valuing that gas via the pre-state PancakeSwap USDT/WBNB pair 0x16b9a82891338f9ba80e2d6970fdda79d1eb0dae gives 372114918258405853 raw USDT units of cost, so net profit remains 29661985011081741594147 raw USDT units of USDT-equivalent value.
7. References
- Exploit tx:
0x4ed59e3013215c272536775a966f4365112997a6eec534d38325be014f2e15ee - Helper deployment tx:
0x6402eecdd3bf15cd2b954b422dd9fe737fb06bc60549c24cd38a30229f6b3bd6 - Victim pair:
0x6524a5fd3fec179db3b3c1d21f700da7abe6b0de - Protocol factory:
0x717d0a192f4c5f9bc8a2d45b01d696dab2bd1b7a - Helper contract:
0x721a66c7767103e7dcacf8440e8dd074edff40a8 - Collector trace:
/workspace/session/artifacts/collector/seed/56/0x4ed59e3013215c272536775a966f4365112997a6eec534d38325be014f2e15ee/trace.cast.log - Collector balance diff:
/workspace/session/artifacts/collector/seed/56/0x4ed59e3013215c272536775a966f4365112997a6eec534d38325be014f2e15ee/balance_diff.json - Collector tx metadata:
/workspace/session/artifacts/collector/seed/56/0x4ed59e3013215c272536775a966f4365112997a6eec534d38325be014f2e15ee/metadata.json - BscScan verified factory source bundle:
https://bscscan.com/address/0x717d0a192f4c5f9bc8a2d45b01d696dab2bd1b7a#code - BscScan helper contract page:
https://bscscan.com/address/0x721a66c7767103e7dcacf8440e8dd074edff40a8 - BscScan pair source check for
0x6524a5fd3fec179db3b3c1d21f700da7abe6b0de: returnsContract source code not verified