This is a lower bound: only assets with reliable historical USD prices are counted, so the actual loss may be higher.
0x7fe46c2746855dd57e18f4d33522849ff192e4e26c74835799ba8dab890994570xad2d8f920a3795c1eee47a00c1056c801eaad18cBSC0x86043747dae9b6dd80c463a29e5b21e50bef5e7dBSC0xa41bf81be90fe9666cd566a80c85871f41529aedBSCAn adversary exploited pfiProtocol on BNB Smart Chain in transaction 0x7fe46c2746855dd57e18f4d33522849ff192e4e26c74835799ba8dab89099457 at block 12886417. The attacker used a predeployed helper contract, flash-borrowed 1,000,400 BUSD from the Pancake BUSD/WBNB pair, manipulated the Twindex DOP/BUSD pool, then borrowed six assets from pfiProtocol against DOP collateral that was only valuable under the manipulated intra-transaction price.
The root cause is a direct trust dependency on a manipulable spot-reserve oracle. PriceFeeds::getCurrentMargin valued DOP collateral using PancakeOracle::latestAnswer(), and that oracle simply read the current Twindex pair reserves. Because the same transaction first rewrote those reserves and then opened the loans, the protocol accepted undercollateralized borrowing and transferred lender-pool assets to the attacker helper.
pfiProtocol is a proxy-style lending system. The verified protocol contract at 0xad2d8f920a3795c1eee47a00c1056c801eaad18c exposes a fallback that delegatecalls logic targets selected by function signature. The lending path therefore depends on external modules for borrow execution while sharing protocol storage.
Collateral valuation is handled by the separate PriceFeeds contract at 0x86043747dae9b6dd80c463a29e5b21e50bef5e7d. Its queryRate and routines read configured external feeds and its function converts collateral into loan-token terms before the borrow path decides whether the position satisfies margin requirements.
_queryRategetCurrentMarginThe DOP feed was configured to use PancakeOracle at 0xa41bf81be90fe9666cd566a80c85871f41529aed. Despite the name, this contract is not a time-weighted oracle. It reads pairRef.getReserves() and returns an instantaneous reserve ratio for its configured pair. The relevant pair was the thin Twindex DOP/BUSD pool at 0xc789f6c658809eed4d1769a46fc7bce5dbb8316e, so a large one-transaction trade could radically change the reported DOP price.
This is an ATTACK-class incident caused by oracle manipulation during loan origination. The violated invariant is straightforward: a new loan should only be issued when the posted collateral remains sufficient under a manipulation-resistant market price. Instead, pfiProtocol used the same mutable AMM reserve state that the adversary could rewrite immediately before the margin check.
Independent source review confirms the critical code path. The protocol fallback delegatecalls active logic targets, the borrow flow reaches borrowOrTradeFromPool, and the trace then shows PriceFeeds::getCurrentMargin staticcalls before each Borrow event. In PriceFeeds, _queryRate calls pricesFeeds[sourceToken].latestAnswer() for non-base assets, and getCurrentMargin multiplies the collateral amount by that derived rate. In PancakeOracle, latestAnswer() reads pairRef.getReserves() and computes reserve1 * 1e18 / reserve0 when baseTokenIndex == 1.
That design makes the borrow authorization dependent on an unprotected spot ratio. In the exploit transaction, the attacker first moved the Twindex DOP/BUSD reserves from 584267772559703112832721 DOP / 24535809866719119382771 BUSD to 14033265246616006481312 DOP / 1024535809866719119382771 BUSD. This changed the effective DOP price from about 0.041994118140774125 BUSD to about 73.007656583258593795 BUSD, and the protocol then approved loans that would not have passed under the pre-manipulation price.
The oracle contract itself shows the core defect:
function latestAnswer() external view override returns (uint256 _price) {
(uint112 _reserve0, uint112 _reserve1, uint32) = IPancakePair(pairRef).getReserves();
if (baseTokenIndex == 1) {
_price = _reserve1.mul(1e18).div(_reserve0);
} else {
_price = _reserve0.mul(1e18).div(_reserve1);
}
}
There is no averaging window, no stale-price guard, and no independent source. The feed is only the current reserve ratio of the configured pair.
The consumer path in PriceFeeds then turns that raw oracle output into loan authorization state:
(collateralToLoanRate, collateralToLoanPrecision) = queryRate(collateralToken, loanToken);
collateralToLoanRate = collateralToLoanRate.mul(10**18).div(collateralToLoanPrecision);
collateralToLoanAmount = collateralAmount.mul(collateralToLoanRate).div(10**18);
return (
collateralToLoanAmount.sub(loanAmount).mul(10**20).div(loanAmount),
collateralToLoanRate
);
The exploit trace shows the attacker exercising exactly that path. The entry transaction came from EOA 0x2f618493b9ff77d61426e4dbf3b844666a6b315e into helper contract 0xcd8206410b55e278a9538071a69ef9e185856d24. The helper flash-borrowed BUSD from Pancake, sent 1,000,000 BUSD into Twindex to move the DOP/BUSD reserves, then bought 8841.131306506726194396 DOP on Pancake for collateral inventory.
After the reserve manipulation, the protocol borrow path repeatedly called the pricing module. The trace records:
PriceFeeds::getCurrentMargin(..., 89019517202160146941255, 1998200000000000000000)
emit Borrow(... loanToken: USDT, newPrincipal: 89019517202160146941255, newCollateral: 1998200000000000000000, ...)
PriceFeeds::getCurrentMargin(..., 90019374766490038850884, 1998200000000000000000)
emit Borrow(... loanToken: BUSD, newPrincipal: 90019374766490038850884, newCollateral: 1998200000000000000000, ...)
The same pattern appears for CAKE, DOLLY, ETH, and BTCB loans. The protocol therefore made its credit decision after the manipulated reserve ratio was already live inside the same transaction.
The resulting asset movements match the root cause. The balance-diff artifact shows:
{
"token": "0x844fa82f1e54824655470970f7004dd90546bb28",
"holder": "0xad2d8f920a3795c1eee47a00c1056c801eaad18c",
"delta": "8450000000000000000000"
}
pfiProtocol received 8450 DOP, but only under the manipulated price did that collateral appear sufficient. At the same time, lender pools lost large principals, including 90019.291732938574352076 BUSD and 89019.514634734071188017 USDT from their pool balances, while the attacker helper retained the borrowed basket after repaying the flash swap.
The exploit completed in one transaction because the protocol had no TWAP, no reserve-age filter, no minimum-liquidity threshold, and no secondary oracle confirmation on this borrow path. The decisive breakpoint is therefore the call sequence: manipulate Twindex reserves, have PancakeOracle.latestAnswer() read those manipulated reserves, have PriceFeeds.getCurrentMargin() accept them, and then let the delegated borrow logic transfer out lender funds.
The adversary lifecycle had three concrete stages.
First, the EOA 0x2f618493b9ff77d61426e4dbf3b844666a6b315e deployed helper contract 0xcd8206410b55e278a9538071a69ef9e185856d24 in transaction 0x0d070378f2fd20197ba7fd253d3d78d27dc1f57e5599feec47a3247b0139cee6 at block 12886405. This helper later executed the full exploit sequence and retained the drained assets.
Second, in transaction 0x7fe46c2746855dd57e18f4d33522849ff192e4e26c74835799ba8dab89099457, the helper took a Pancake flash swap for 1,000,400 BUSD, spent 1,000,000 BUSD on Twindex to rewrite the DOP/BUSD reserve ratio, and spent another 400 BUSD on Pancake to buy DOP for collateral. The manipulated Twindex reserves visible in the trace were:
TwindexPair::getReserves() -> 14033265246616006481312 DOP, 1024535809866719119382771 BUSD
PriceFeeds::queryRate(DOP, BUSD) -> 73007656583258593795
Third, the helper approved the six lender pools, posted DOP collateral across six borrow calls, received the loan principals, swapped the remaining DOP back into BUSD on Twindex, repaid 1002951020000000000000000 BUSD to the Pancake flash pair, and kept the remaining borrowed assets. The trace ends with the helper holding at least:
BUSD = 86921171888029716855925
USDT = 89019514634734071188017
BTCB = 1600274620781858497
ETH = 18003949206565767597
CAKE = 85019287705282065987
DOLLY = 18004008679347594905139
This sequence satisfies the ACT model. The adversary needed only public flash liquidity, public AMM liquidity, public borrow entrypoints, and a fresh helper contract. No privileged keys, governance powers, or private attacker artifacts were required.
The protocol issued six undercollateralized loans and transferred value out of lender pools while retaining only DOP collateral whose real market value had already collapsed once the manipulation unwound. The recorded losses in smallest units are:
BUSD: "90019291732938574352076" with decimal: 18USDT: "89019514634734071188017" with decimal: 18BTCB: "1600274620781858497" with decimal: 18ETH: "18003949206565767597" with decimal: 18CAKE: "85019287705282065987" with decimal: 18DOLLY: "18004008679347594905139" with decimal: 18The attacker helper contract retained the drained basket after flash-loan repayment, so the protocol was left with positions that were only collateralized under the manipulated DOP price and materially underwater under the pre-attack price regime.
0x7fe46c2746855dd57e18f4d33522849ff192e4e26c74835799ba8dab890994570x0d070378f2fd20197ba7fd253d3d78d27dc1f57e5599feec47a3247b0139cee60xad2d8f920a3795c1eee47a00c1056c801eaad18c0x86043747dae9b6dd80c463a29e5b21e50bef5e7d0xa41bf81be90fe9666cd566a80c85871f41529aed0xc789f6c658809eed4d1769a46fc7bce5dbb8316e0x58f876857a02d6762e0101bb5c46a8c1ed44dc16