Gangster Finance Over-Distribution
Exploit Transactions
0xf34e59e4fe2c9b454d2b73a1a3f3aaf07d484a0c71ff8278b1c068cdedc4b64dVictim Addresses
0xe968d2e4adc89609773571301abec3399d163c3bBSC0x0935072f012190354ef41a66078250f1cf2846ddBSCLoss Breakdown
Similar Incidents
EGD Finance Reward Oracle Manipulation
36%Channels Dust-Share Drain
34%Bankroll Stack Dividend Drain
34%xDAO / Unicorn Finance DAO Public LP Offer Treasury Drain
31%Transit Router V5 Drain
31%LAYER3 Oracle-Mint Drain
30%Root Cause Analysis
Gangster Finance Over-Distribution
1. Incident Overview TL;DR
Gangster Finance's TokenVault on BNB Chain was exploited in transaction 0xf34e59e4fe2c9b454d2b73a1a3f3aaf07d484a0c71ff8278b1c068cdedc4b64d. The attacker deployed a helper contract, flash-borrowed 1.02 BTCB from PancakePair 0x0b32ea94da1f6679b11686ead47aa4c6bf38cd59, donated 1 BTCB into TokenVault 0xe968d2e4adc89609773571301abec3399d163c3b, made a small follow-on deposit, and then harvested more BTCB than the funded drip pool contained. The core bug is that TokenVault::distribute() computes elapsed-time profit first, zero-saturates dripPoolBalance with safeSub, and still credits the full oversized profit into profitPerShare_. That accounting error let the attacker realize 155793734101992050 BTCB profit at the EOA level while also minting 6288000000000000 OGX to the helper contract.
2. Key Background
TokenVault is a BTCB-denominated dividend vault. donate() adds BTCB to dripPoolBalance, depositTo() stakes BTCB and mints OGX rewards, resolve() burns vault shares, and harvest() pays out dividendsOf(msg.sender) in BTCB. The vault streams drip-pool value over time using lastPayout, dripRate, and the current total staked supply.
Immediately before the exploit pre-state at block 51782712, the vault had lastPayout = 1724317950, dripPoolBalance = 0, dripRate = 3, rewardRate = 2, and totalSupply = 96387555310360346. Those values matter because the stale lastPayout created a very large elapsed-time window, while the non-zero staked supply gave a new depositor a way to capture a share of the oversized distribution.
The production configuration is visible in the verified BscScan source and is also reproduced in the validator PoC pre-checks:
assertEq(ITokenVault(VAULT).rewardRate(), 2);
assertEq(uint256(ITokenVault(VAULT).dripRate()), 3);
assertEq(ITokenVault(VAULT).dripPoolBalance(), 0);
assertEq(ITokenVault(VAULT).lastPayout(), 1_724_317_950);
assertGt(ITokenVault(VAULT).totalSupply(), 0);
3. Vulnerability Analysis & Root Cause Summary
The vulnerability is an accounting error in the vault's time-based reward distribution path. TokenVault treats safeSub as a saturating subtraction helper: if the subtraction would underflow, it returns zero instead of reverting. That behavior is unsafe inside distribute() because the function first computes profit from elapsed time and only afterward updates the pool balance. When elapsed time is very large, profit can exceed the actual dripPoolBalance. The contract then sets dripPoolBalance to zero but still increases profitPerShare_ by the full oversized profit. As a result, withdrawable dividends can exceed the BTCB that was actually available for dripping. Because donate, depositTo, resolve, and harvest are all public, an unprivileged attacker can fund the pool, trigger the buggy distribution, and withdraw the inflated earnings in one transaction.
4. Detailed Root Cause Analysis
The verified TokenVault source shows the unsafe arithmetic directly:
function safeSub(uint a, uint b) internal pure returns (uint) {
if (b > a) {return 0;} else {return a - b;}
}
function distribute() private {
uint _currentTimestamp = (block.timestamp);
if (SafeMath.safeSub(_currentTimestamp, lastPayout) > payoutFrequency && currentTotalStaked > 0) {
uint256 share = dripPoolBalance.mul(dripRate).div(100).div(24 hours);
uint256 profit = share * _currentTimestamp.safeSub(lastPayout);
dripPoolBalance = dripPoolBalance.safeSub(profit);
profitPerShare_ = SafeMath.add(profitPerShare_, (profit * magnitude) / currentTotalStaked);
lastPayout = _currentTimestamp;
}
}
This breaks the vault's intended invariant: the amount credited into profitPerShare_ must never exceed the real pre-call dripPoolBalance. In the exploit pre-state, lastPayout lagged the block timestamp by roughly 26100425 seconds. After the attacker donated 1 BTCB, share was derived from that 1 BTCB pool and dripRate = 3, but multiplying by the stale elapsed time produced a profit larger than the funded pool. Because safeSub saturates, dripPoolBalance was only zeroed instead of forcing profit to stay within available funds. The contract still credited the full oversized profit to all stakers through profitPerShare_.
The seed trace shows the exact transition during depositTo():
0xe968...c3b::donate(1000000000000000000)
emit onDonate(..., 1000000000000000000, 1750418376)
@ 12: 0 -> 0x0de0b6b3a7640000
0xe968...c3b::depositTo(0x268D..., 15720000000000000)
emit onDeposit(..., 15720000000000000, 14148000000000000, 3144000000000000, 1750418376)
emit onRebase(..., 1750418376)
@ 14: 0x66c700fe -> 0x685543c8
@ 12: 0x0de0b6b3a7640000 -> 0
The helper contract then queried its own staking position and saw withdrawable dividends above the entire funded pool. The historical trace recorded harvest() paying 1174165734101992050 BTCB to the helper, while the validator's fresh-address reproduction on the same pre-state produced 1161432489603387541 recorded dividends before resolve and 1174165689603387541 harvested BTCB. The small delta comes from a one-second timestamp difference, not from any change in exploit mechanics.
5. Adversary Flow Analysis
The adversary-controlled EOA was 0xc49f2938327aa2cdc3f2f89ed17b54b3671f05de. Inside the exploit transaction it deployed helper contract 0x268d1581a34fb63dc46c92f07cb0d739517ca51c, then used that helper as the PancakePair flash-swap callback recipient.
The end-to-end flow from the seed trace was:
0x268D...51C::pancakeCall(..., 1020000000000000000, 0, 0x01)
-> TokenVault::donate(1000000000000000000)
-> TokenVault::depositTo(0x268D..., 15720000000000000)
-> OGX::mintTokens(0x268D..., 6288000000000000)
-> TokenVault::resolve(14148000000000000)
-> TokenVault::harvest()
-> BTCB::transfer(PancakePair, 1022652000000000000)
-> BTCB::transfer(EOA, 155793734101992050)
This sequence is permissionless. The helper only needs temporary BTCB liquidity, which the flash swap supplies, and the vault methods used in the exploit are public. The attacker does not require admin rights, privileged calldata, or any pre-existing attacker-owned on-chain contract. The ACT opportunity therefore exists for any unprivileged actor observing the same stale TokenVault state.
6. Impact & Losses
The direct victim-side BTCB loss in the seed transaction was 158445734101992050 wei of BTCB, as shown by the balance diff for TokenVault. Of that amount, 155793734101992050 wei of BTCB ended as attacker EOA profit and 2652000000000000 wei became the PancakePair fee spread on flash-swap repayment. The same exploit path also minted 6288000000000000 wei of OGX to the attacker helper contract.
The seed balance diff captures the realized asset movement:
{
"token": "0x7130d2a12b9bcbfae4f2634d864a1ee1ce3ead9c",
"holder": "0xe968d2e4adc89609773571301abec3399d163c3b",
"delta": "-158445734101992050"
}
{
"token": "0x7130d2a12b9bcbfae4f2634d864a1ee1ce3ead9c",
"holder": "0xc49f2938327aa2cdc3f2f89ed17b54b3671f05de",
"delta": "155793734101992050"
}
{
"token": "0x0935072f012190354ef41a66078250f1cf2846dd",
"holder": "0x268d1581a34fb63dc46c92f07cb0d739517ca51c",
"delta": "6288000000000000"
}
7. References
- Exploit transaction:
0xf34e59e4fe2c9b454d2b73a1a3f3aaf07d484a0c71ff8278b1c068cdedc4b64don BNB Chain. - Victim contract: TokenVault
0xe968d2e4adc89609773571301abec3399d163c3b. - Flash-loan source: PancakePair
0x0b32ea94da1f6679b11686ead47aa4c6bf38cd59. - Reward token: OGX
0x0935072f012190354ef41a66078250f1cf2846dd. - Verified source used for code review:
https://bscscan.com/address/0xe968d2e4adc89609773571301abec3399d163c3b#code. - Seed trace evidence:
/workspace/session/artifacts/collector/seed/56/0xf34e59e4fe2c9b454d2b73a1a3f3aaf07d484a0c71ff8278b1c068cdedc4b64d/trace.cast.log. - Seed balance-diff evidence:
/workspace/session/artifacts/collector/seed/56/0xf34e59e4fe2c9b454d2b73a1a3f3aaf07d484a0c71ff8278b1c068cdedc4b64d/balance_diff.json. - Validator PoC execution log:
/workspace/session/artifacts/validator/forge-test.log.