TiFi Oracle Manipulation
Exploit Transactions
0x1c5272ce35338c57c6b9ea710a09766a17bbf14b61438940c3072ed49bfec402Victim Addresses
0x8a6f7834a9d60090668f5db33fec353a7fb4704bBSC0x9d1fc5ad7ac6ff99e2ce4826678c6cc0a0c8f278BSCLoss Breakdown
Similar Incidents
SellToken Reward Oracle Manipulation
43%NovaX TokenStake Oracle Manipulation Exploit
43%SellToken Short Oracle Manipulation
41%EGD Finance Reward Oracle Manipulation
41%BXH Bonus Oracle Manipulation
39%Sturdy LP Oracle Manipulation
38%Root Cause Analysis
TiFi Oracle Manipulation
1. Incident Overview TL;DR
On BNB Smart Chain block 23778727, transaction 0x1c5272ce35338c57c6b9ea710a09766a17bbf14b61438940c3072ed49bfec402 used a single permissionless exploit contract to flash-borrow 5 WBNB and 500 BUSD, distort TiFi’s BUSD/WBNB oracle source, deposit the borrowed 500 BUSD into TiFi LendingPool at 0x8a6f7834a9d60090668f5db33fec353a7fb4704b, and then borrow the pool’s full TIFI inventory. The attacker immediately sold the borrowed TIFI into WBNB, repaid the flash swap, and forwarded proceeds to adversary-controlled EOA 0x5599cec4ba078d7c0c9214a19b690732d0924d0e.
The root cause is that TiFi LendingPool trusted a flash-loan-manipulable spot oracle. For non-WBNB assets, getPriceWBNB() delegated to GetPrice.getTokenToBNBPrice(), and the external GetPrice contract derived prices directly from live AMM reserves on thin pairs. Because borrow() only required isAccountHealthy(msg.sender) after transfer, the attacker could first manipulate the BUSD reserve ratio, inflate the collateral value of a 500 BUSD deposit, and then drain the lending pool’s available TIFI liquidity in the same transaction.
2. Key Background
TiFi LendingPool values each listed asset in WBNB terms. Its verified source shows:
function getPriceWBNB(address _token) internal view returns (uint256) {
return _token == getPrice.WBNB() ? 1e18 : getPrice.getTokenToBNBPrice(_token);
}
That price feeds directly into getUserAccount(), which sums collateral and debt in the same WBNB-denominated base and applies each pool’s collateral factor:
uint256 collateralPercent = pool.poolConfig.getCollateralPercent();
uint256 poolPricePerUnit = getPriceWBNB(address(_token));
uint256 liquidityBalanceBase = (poolPricePerUnit * compoundedLiquidityBalance) / 1e18;
if (collateralPercent > 0 && userUsePoolAsCollateral) {
totalCollateralBalanceBase += (liquidityBalanceBase * collateralPercent) / 1e18;
}
totalBorrowBalanceBase += (poolPricePerUnit * compoundedBorrowBalance) / 1e18;
The oracle contract at 0x9d1fc5ad7ac6ff99e2ce4826678c6cc0a0c8f278 is unverified, but the collected decompile shows repeated calls to selector 0x0902f1ac (getReserves()) and 0x313ce567 (decimals()), matching a direct reserve-ratio pricing routine over hardcoded token/WBNB pairs. No TWAP, delay, or minimum-liquidity defense is present in the recovered logic.
LendingPool also treats borrowable liquidity as the live token balance of the pool contract:
function getTotalAvailableLiquidity(ERC20 _token) public view returns (uint256) {
return _token.balanceOf(address(this));
}
That means once the manipulated health check passes, borrow() can withdraw the full on-chain TIFI balance.
3. Vulnerability Analysis & Root Cause Summary
This was an ATTACK-class oracle-manipulation incident, not a privileged-access event. The explicit invariant is that borrower solvency must be checked against manipulation-resistant prices: a user should not be able to increase collateral value above true market value by trading the oracle source inside the same transaction. TiFi violates that invariant because getUserAccount() and isAccountHealthy() consume getPrice.getTokenToBNBPrice() without any protection against same-block reserve distortion. The code-level breakpoint is the borrow() path: the function transfers the borrowed asset, then checks require(isAccountHealthy(msg.sender), "TIFI: ACCOUNT_UNHEALTHY"). Since the external oracle prices BUSD from a thin, directly tradeable pair, the attacker can temporarily inflate BUSD’s quoted WBNB price, pass the health check, and borrow more value than the real collateral supports. Because getTotalAvailableLiquidity() is just token.balanceOf(address(this)), the exploit can take the entire TIFI pool balance rather than only a bounded tranche.
4. Detailed Root Cause Analysis
The adversary cluster consisted of EOA 0xd3455773c44bf0809e2aeff140e029c632985c50, exploit contract 0xbec576e2e3552f9a1751db6a4f02e224ce216ac1, and payout EOA 0x5599cec4ba078d7c0c9214a19b690732d0924d0e. Contract-creation metadata shows 0xd345... deployed 0xbec576... in transaction 0xdb64e23314606b53f9a9d156dc87497d4b96b554f80b2d1a475f9d4516e6c578, and the creation bytecode embeds 0x5599... as the payout recipient.
The exploit transaction begins with a PancakePair flash swap from 0x58f876857a02d6762e0101bb5c46a8c1ed44dc16:
PancakePair::swap(5000000000000000000, 500000000000000000000, 0xBeC576..., ...)
emit Transfer(... to: 0xBeC576..., value: 5000000000000000000)
emit Transfer(... to: 0xBeC576..., value: 500000000000000000000)
The trace then shows the attacker depositing the borrowed 500 BUSD into LendingPool:
emit Transfer(from: 0xBeC576..., to: LendingPool: [0x8A6F7834...], value: 500000000000000000000)
emit Deposit(pool: BEP20Token: [0xe9e7CEA3...], user: 0xBeC576..., depositAmount: 500000000000000000000)
Next, the attacker manipulates the BUSD oracle source pair 0x76fc4931d9d3a2054aee2d59633e49b759277d69 by pushing 5 WBNB into the pool and pulling out 165.166005723751869908 BUSD:
0x76Fc4931...::getReserves()
emit Transfer(from: 0xBeC576..., to: 0x76Fc4931..., value: 5000000000000000000)
0x76Fc4931...::swap(0, 165166005723751869908, 0xBeC576..., 0x)
emit Transfer(from: 0x76Fc4931..., to: 0xBeC576..., value: 165166005723751869908)
The balance-diff artifact confirms the manipulated reserve outcome: the oracle pair’s BUSD balance falls by 165166005723751869908, leaving only 21158021889510981672 BUSD in the pair. Under TiFi’s reserve-ratio oracle, that makes BUSD appear much more expensive in WBNB terms. The root-cause analysis calculates that the oracle valued BUSD at 254014386050425208 wei WBNB per token and, after applying the BUSD pool’s 85% collateral factor, counted the 500 BUSD deposit as 107.956114071430713400 WBNB of borrowing power.
With the manipulated price in place, the attacker borrows the pool’s full TIFI balance:
emit Transfer(from: LendingPool: [0x8A6F7834...], to: 0xBeC576..., value: 529570181341490839170863757308)
emit Borrow(pool: TiFiToken: [0x17E65E6b...], user: 0xBeC576..., borrowAmount: 529570181341490839170863757308)
This works because borrow() only rejects after recomputing account health with the manipulated oracle state. The same trace shows the protocol querying the external oracle and both reserve pairs during the health check, exactly where the manipulated price enters solvency accounting.
The attacker then sells the borrowed TIFI into the TIFI/WBNB pair 0xb62bb233af2f83028be19626256a9894b68aae5e, receives 94.138130026273038368 WBNB, repays 7 WBNB to the flash-swap pair, and forwards profits:
PancakePair::swap(0, 94138130026273038368, 0xBeC576..., 0x)
emit Transfer(from: 0xBeC576..., to: PancakePair: [0x58F87685...], value: 7000000000000000000)
emit Transfer(from: 0xBeC576..., to: 0x5599cEc4..., value: 87138130026273038368)
emit Transfer(from: 0xBeC576..., to: 0x5599cEc4..., value: 165166005723751869908)
The balance-diff artifact records a conservative realized profit floor of 80.308231018168669344 WBNB net of gas by subtracting the sender EOA’s 6.829899008104369024 BNB gas spend from the 87.138130026273038368 WBNB delivered to the payout EOA. The forwarded 165.166005723751869908 BUSD is additional profit beyond that floor.
5. Adversary Flow Analysis
The adversary flow is fully permissionless and fits the ACT model.
0xd345...deploys exploit contract0xbec576...with payout recipient0x5599....- In the seed transaction,
0xd345...calls the exploit contract. - The exploit contract flash-swaps
5 WBNBand500 BUSDfrom public Pancake liquidity. - It deposits the borrowed
500 BUSDinto TiFi LendingPool, minting BUSD pool shares. - It trades the borrowed
5 WBNBagainst the thin BUSD/WBNB oracle pair, distorting the spot reserve ratio that TiFi uses for BUSD pricing. - It calls
borrow()for the full TIFI balance while the manipulated price is still live. TiFi accepts becauseisAccountHealthy()uses the distorted oracle. - It sells the borrowed TIFI for WBNB, repays the flash swap, and forwards WBNB and BUSD proceeds to
0x5599....
The exploit path did not require privileged keys, governance access, or proprietary attacker-side infrastructure beyond a standard helper contract. All inputs came from public state, public AMM liquidity, and permissionless protocol entrypoints.
6. Impact & Losses
The measurable protocol loss was the full borrowable TIFI balance in LendingPool at the manipulated block:
{
"token_symbol": "TIFI",
"amount": "529570181341490839170863757308",
"decimal": 18
}
Economically, the exploit drained all available TIFI liquidity from the victim lending pool in one transaction. The attacker realized at least 80.308231018168669344 WBNB net of gas, plus 165.166005723751869908 BUSD sent to the payout address, so the realized adversary profit exceeded the WBNB-only floor.
7. References
- Seed exploit transaction:
0x1c5272ce35338c57c6b9ea710a09766a17bbf14b61438940c3072ed49bfec402 - Attacker contract deployment:
0xdb64e23314606b53f9a9d156dc87497d4b96b554f80b2d1a475f9d4516e6c578 - Victim LendingPool:
0x8a6f7834a9d60090668f5db33fec353a7fb4704b - Oracle contract:
0x9d1fc5ad7ac6ff99e2ce4826678c6cc0a0c8f278 - Manipulated BUSD/WBNB pair:
0x76fc4931d9d3a2054aee2d59633e49b759277d69 - TIFI/WBNB exit pair:
0xb62bb233af2f83028be19626256a9894b68aae5e - Verified victim source:
artifacts/collector/iter_1/contract/56/0x8a6f7834a9d60090668f5db33fec353a7fb4704b/src/Contract.sol - Oracle decompile:
artifacts/collector/iter_1/contract/56/0x9d1fc5ad7ac6ff99e2ce4826678c6cc0a0c8f278/heimdall/decompiled-decompiled.sol - Trace evidence:
artifacts/collector/seed/56/0x1c5272ce35338c57c6b9ea710a09766a17bbf14b61438940c3072ed49bfec402/trace.cast.log - Balance deltas:
artifacts/collector/seed/56/0x1c5272ce35338c57c6b9ea710a09766a17bbf14b61438940c3072ed49bfec402/balance_diff.json