Bitpaid Mature-Lock Top-Up Exploit
Exploit Transactions
0x1ae499ccf292a2ee5e550702b81a4a7f65cd03af2c604e2d401d52786f459ba6Victim Addresses
0x9d6d817ea5d4a69ff4c4509bea8f9b2534cec108BSCLoss Breakdown
Similar Incidents
QiQi Reward Quote Override Drain
30%StakingDYNA Reward Backdating Drain
29%NeverFallToken LP Drain
29%GDS LP Mining Rewards Were Reclaimable By Reusing Transferable LP Shares
29%SellToken Arbitrary-Pair LP Drain
28%CS Pair Balance Burn Drain
28%Root Cause Analysis
Bitpaid Mature-Lock Top-Up Exploit
1. Incident Overview TL;DR
On BNB Smart Chain block 28176676, EOA 0x878a36edfb757e8640ff78b612f839b63adc2e51 sent transaction 0x1ae499ccf292a2ee5e550702b81a4a7f65cd03af2c604e2d401d52786f459ba6 to helper contract 0x7b9265c6aa4b026b7220eee2e8697bf5ffa6bb9a. The helper flash-borrowed 219349 BTP from Pancake pair 0x858de6f832c9b92e2ea5c18582551ccd6add0295, added that liquidity to an already-mature Bitpaid six-month staking slot, withdrew immediately for principal plus the full plan reward, repaid the pair, and retained 10417.703132832080200503 BTP.
The root cause is a stale-lock bug in Bitpaid Staking at 0x9d6d817ea5d4a69ff4c4509bea8f9b2534cec108. When reinvest != 0, Lock_Token() increases the stored principal but reuses the old start_time and end_time. withdraw() then checks only whether that preserved end_time has passed and pays ROI on the full enlarged balance, so newly added principal inherits an already-expired vesting schedule.
2. Key Background
Bitpaid Staking offers three fixed-term plans keyed by msg.sender: six months at 5%, nine months at 10%, and twelve months at 20%. Each plan stores amount, start_time, end_time, and reinvest in per-address mappings such as sixMonth.
This design is only safe if any increase in principal resets the lock for the newly added amount. Otherwise, a mature slot can be enlarged and treated as if the new deposit has already served the advertised term. That is exactly what happened here.
The financing leg came from a permissionless Pancake flash swap. The bug itself is inside Bitpaid Staking. Once the attacker controlled a mature slot and the staking contract held enough BTP, the exploit reduced to a public transaction that called public protocol functions.
3. Vulnerability Analysis & Root Cause Summary
This incident is an ATTACK, not a pricing-only MEV event. Bitpaid Staking violates a core staking invariant: newly added principal must not become withdrawable or reward-eligible before serving its own lock period. The contract breaks that invariant whenever an address with reinvest != 0 tops up an existing position.
The verified victim code shows the exact issue. In the first-deposit branch, Lock_Token() sets endTime = block.timestamp + 180 days for plan 1. In the top-up branch, it reads the old start_time and end_time from storage and writes them back unchanged while increasing amount. withdraw() later gates only on block.timestamp >= sixMonth[msg.sender].end_time and computes the payout from the full current amount.
That combination lets a mature position absorb fresh BTP and claim the full six-month 5% ROI immediately. The flash loan only scaled the attack size. It did not create the faulty state transition.
4. Detailed Root Cause Analysis
The vulnerable logic is visible in Bitpaid Staking's verified source:
function Lock_Token(uint256 plan, uint256 _amount) external {
if (plan == 1) {
uint256 currentAmount = sixMonth[msg.sender].amount;
uint256 total = SafeMath.add(currentAmount, _amount);
if (sixMonth[msg.sender].reinvest == 0) {
uint256 startTime = block.timestamp;
uint256 endTime = block.timestamp + 180 days;
sixMonth[msg.sender] = TimeLock_Six_Month(msg.sender, total, startTime, endTime, 1);
} else {
uint256 startTime = sixMonth[msg.sender].start_time;
uint256 endTime = sixMonth[msg.sender].end_time;
sixMonth[msg.sender] = TimeLock_Six_Month(msg.sender, total, startTime, endTime, 1);
}
ERC20interface.transferFrom(msg.sender, address(this), _amount);
}
}
function withdraw(uint256 _plan) public {
if (_plan == 1) {
require(block.timestamp >= sixMonth[msg.sender].end_time, "You cant unstake now");
uint256 roi = sixMonth[msg.sender].amount;
uint256 RoiReturn = plan_1_Roi(roi);
uint256 investedAmount = sixMonth[msg.sender].amount;
uint256 total = SafeMath.add(RoiReturn, investedAmount);
ERC20interface.transfer(msg.sender, total);
sixMonth[msg.sender] = TimeLock_Six_Month(msg.sender, 0, 0, 0, 0);
}
}
The exploit pre-state is deterministic. A direct RPC read at block 28176675 returned:
sixMonth(0x7b9265c6aa4b026b7220eee2e8697bf5ffa6bb9a)
= (0x7b9265c6aa4b026b7220eee2e8697bf5ffa6bb9a, 2, 1668433277, 1683985277, 1)
Block 28176676 had timestamp 1683987896, so the helper's stored end_time was already expired before the exploit transaction began.
The collector trace shows the exploit path inside one Pancake callback:
0x858DE6F832c9b92E2EA5C18582551ccd6add0295::swap(219349000000000000000000, 0, 0x7b9265..., data)
0x7b9265...::pancakeCall(...)
Staking::Lock_Token(1, 219349000000000000000000)
Staking::withdraw(1)
CommonBEP20::transfer(0x858DE6F832c9b92E2EA5C18582551ccd6add0295, 219898746867167919799499)
The same trace records Bitpaid's payout:
CommonBEP20::transfer(0x7b9265..., 230316450000000000000002)
That amount equals the enlarged principal plus Bitpaid's 5% six-month reward. Because Lock_Token() preserved the stale mature end_time, withdraw(1) treated the newly added 219349 BTP as fully vested immediately.
The attack required three conditions:
- The adversary controlled a mature staking slot for one of the plans.
- Bitpaid Staking held enough BTP to pay principal plus ROI on the enlarged balance.
- Temporary financing cost was below the immediate reward. Here the 5% plan ROI exceeded the Pancake flash-swap fee.
5. Adversary Flow Analysis
The adversary cluster consists of EOA 0x878a36edfb757e8640ff78b612f839b63adc2e51 and helper contract 0x7b9265c6aa4b026b7220eee2e8697bf5ffa6bb9a. The EOA created the helper in transaction 0xd4ee8a5ad903a00b03af10653cebde64d81781e7d4140a309ea707613106d4bb, and the helper later executed the exploit path and held the resulting BTP profit.
The on-chain sequence was:
- Before the exploit, the helper already held a six-month Bitpaid position with
amount=2,start_time=1668433277,end_time=1683985277, andreinvest=1. - In transaction
0x1ae499ccf292a2ee5e550702b81a4a7f65cd03af2c604e2d401d52786f459ba6, the helper flash-borrowed219349BTP from Pancake pair0x858de6f832c9b92e2ea5c18582551ccd6add0295. - Inside
pancakeCall, the helper calledBitpaidStaking.Lock_Token(1, 219349e18). Becausereinvest != 0, Bitpaid increasedamountbut kept the stale matureend_time. - The helper immediately called
BitpaidStaking.withdraw(1). Since the stale expiry was already in the past, the contract transferred230316450000000000000002wei of BTP to the helper and cleared the slot. - The helper repaid
219898746867167919799499wei of BTP to the Pancake pair and retained the remainder as profit.
Every step used public contracts and public entrypoints, so the opportunity was permissionless once the mature slot existed.
6. Impact & Losses
The balance-diff artifact for the exploit transaction shows the full BTP movement:
[
{
"holder": "0x858de6f832c9b92e2ea5c18582551ccd6add0295",
"delta": "549746867167919799499"
},
{
"holder": "0x7b9265c6aa4b026b7220eee2e8697bf5ffa6bb9a",
"delta": "10417703132832080200503"
},
{
"holder": "0x9d6d817ea5d4a69ff4c4509bea8f9b2534cec108",
"delta": "-10967450000000000000002"
}
]
Bitpaid Staking lost 10967.450000000000000002 BTP. The Pancake pair collected 549.746867167919799499 BTP as the flash-swap premium, and the attacker helper retained 10417.703132832080200503 BTP. The loss was borne directly by the staking contract's inventory because the contract paid rewards on principal that had not served the plan's lock duration.
7. References
- Exploit transaction:
0x1ae499ccf292a2ee5e550702b81a4a7f65cd03af2c604e2d401d52786f459ba6 - Helper creation transaction:
0xd4ee8a5ad903a00b03af10653cebde64d81781e7d4140a309ea707613106d4bb - Victim contract: Bitpaid Staking
0x9d6d817ea5d4a69ff4c4509bea8f9b2534cec108 - Token contract: Bitpaid Token
0x40f75ed09c7bc89bf596ce0ff6fb2ff8d02ac019 - Flash-swap source: Pancake pair
0x858de6f832c9b92e2ea5c18582551ccd6add0295 - Victim code source: verified Bitpaid Staking source from the Etherscan v2 contract API for chain
56 - Execution evidence: collector trace and balance-diff artifacts for tx
0x1ae499ccf292a2ee5e550702b81a4a7f65cd03af2c604e2d401d52786f459ba6 - State evidence: direct BSC RPC read of
sixMonth(0x7b9265c6aa4b026b7220eee2e8697bf5ffa6bb9a)at block28176675