Calculated from recorded token losses using historical USD prices at the incident time.
0x84c385aab658d86b64e132e8db0c092756d5a9331a1131bf05f8214d08efba560xb9c3401c846f3ac4ccd2bdb1901e41c1da463e10BSC0xf738de9913bc1e21b1a985bb0e39db75091263b7BSC0x7c98b0ceeafcf5b5b30871362035f728955b328cBSC0x493361d6164093936c86dcb35ad03b4c0d032076BSCOn BSC (chainid 56), an adversary-controlled helper contract executed a single flash‑loan transaction (tx 0x84c385aab658d86b64e132e8db0c092756d5a9331a1131bf05f8214d08efba56) that:
The root cause is a mispriced, owner-controlled oracle (vistaForcePlan at 0xb9c3401c846f3ac4ccd2bdb1901e41c1da463e10) whose price variable was set to approximately 22.86 while the ICO continued to sell VISTA at 1 BUSD per token. The VistaFinance sell/buy contract (0xf738de9913bc1e21b1a985bb0E39Db75091263b7) trusted this oracle price without any sanity checks or linkage to the ICO rate, creating a deterministic, permissionless on‑chain arbitrage: buy VISTA at 1 BUSD and immediately redeem at ~22.86 USDT.
This price inconsistency between the ICO and the sell contract is entirely on‑chain and does not require any special privileges. The adversary simply automated the arbitrage using a flash loan and helper contract, extracting protocol‑owned USDT and WBNB reserves.
VISTA Token (VistaFinance)
0x493361D6164093936c86Dcb35Ad03b4C0D032076ERC20FlashMint extension, which allows uncollateralized flash‑minting of arbitrary amounts of VISTA within a single transaction.Minimal excerpt showing the VISTA token contract and staking hook:
// Collected VistaFinance token source (verified on explorer)
// Contract: VistaFinance (VISTA) - snippet
contract VistaFinance is ERC20, ERC20Burnable, ERC20Permit, ERC20Votes, ERC20FlashMint, AccessControl {
struct Stake {
uint256 amount;
uint256 releaseTime;
}
mapping(address => Stake[]) private stakedTokens;
event TokensStaked(address indexed wallet, uint256 amount, uint256 releaseTime);
event TokensUnstaked(address indexed wallet, uint256 amount);
constructor() ERC20("Vista Finance", "VISTA") ERC20Permit("Vista Finance") {
_mint(msg.sender, 21000000 * 10 ** decimals());
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
}
...
function getFreeBalance(address _userAddress) public returns (uint256) {
Stake[] storage userStakings = stakedTokens[_userAddress];
uint256 stakedAmount = 0;
uint256 balance = balanceOf(_userAddress);
...
uint256 freeBalance = balance - stakedAmount;
return freeBalance;
}
}
Caption: VistaFinance token contract with staking and ERC20FlashMint, used both for ICO distribution and flash‑minting during the exploit.
VistaFinanceICO (ICO contract)
0x7C98b0cEEaFCf5b5B30871362035f728955b328c// Collected VistaFinanceICO contract source
contract VistaFinanceICO {
IBUSD private busdToken;
IVista private vistaToken;
uint256 private day = 86400;
uint256 private month = day * 30;
address admin = 0x3a923ae336112bcADA95d1A44393f16C51A68C33;
...
function stake(uint256 amount, address sponsor) external {
amount = amount * 10 ** 18;
require(amount > 0, "Amount must be greater than 0");
require(busdToken.balanceOf(msg.sender) >= amount, "Insufficient BUSD balance");
require(busdToken.allowance(msg.sender, address(this)) >= amount, "Insufficient allowance");
busdToken.transferFrom(msg.sender, address(this), amount);
busdToken.transfer(admin, amount * 95 / 100);
busdToken.transfer(sponsor, amount * 5 / 100);
vistaToken.transfer(msg.sender, amount);
...
}
}
Caption: VistaFinanceICO sells VISTA at a fixed 1:1 rate versus BUSD (1 BUSD → 1 VISTA).
vistaForcePlan Oracle (price source)
0xb9c3401c846f3ac4ccd2bdb1901e41c1da463e10price variable (Solidity auto‑generated getter price()).// Collected vistaForcePlan oracle source
contract vistaForcePlan is Initializable, OwnableUpgradeable, UUPSUpgradeable {
uint256 private day = 86400;
uint256 private month = day * 30;
uint256 private startmonth = 18 * (day * 30);
uint256 public price = 8100000000000000000;
// uint public price;
address public usdt = 0x55d398326f99059fF775485246999027B3197955;
address public vista = 0x493361D6164093936c86Dcb35Ad03b4C0D032076;
...
function initialize() initializer public {
__Ownable_init();
__UUPSUpgradeable_init();
_transferOwnership(msg.sender);
...
// Initialize the Token contract
tokenPriceInBusd = 490;
...
}
...
function updatePrice(uint256 _newPrice) public onlyOwner {
require(_newPrice != price, "New price must be different.");
uint256 oldPrice = price;
price = _newPrice;
emit PriceUpdated(oldPrice, _newPrice);
}
// Function to stake ERC-20 tokens into the contract
function stake(uint256 USDTAmount, address refer, address user) public {
IERC20 vistaToken = IERC20(vista);
IERC20 usdToken = IERC20(usdt);
uint256 amount = (USDTAmount * 1 ether) / price;
require(usdToken.transferFrom(user, address(this), USDTAmount), "USDT transferFrom failed");
...
require(vistaToken.transfer(user, amount), "VISTA transfer to sender failed");
...
}
}
Caption: vistaForcePlan exposes a mutable price used directly in a linear formula to convert USDT deposits into VISTA amounts; the same getter is later used by the sell contract.
VistaFinance sell/buy contract
0xf738de9913bc1e21b1a985bb0E39Db75091263b7vistaForcePlan.price() to convert between USDT and VISTA in both buy and sell directions. The implementation is only available as a decompiled EVM artifact.// Decompiled VistaFinance sell/buy contract - buy() snippet
function buy(uint256 arg0, address arg1) public {
...
// Transfer USDT from buyer into this contract
(bool success, bytes memory ret0) =
address(0x55d398326f99059ff775485246999027b3197955).Unresolved_23b872dd(var_b); // transferFrom
...
// Read vistaForcePlan oracle price
(bool success, bytes memory ret0) =
address(0xb9c3401c846f3ac4ccd2bdb1901e41c1da463e10).price(); // staticcall
...
// Compute VISTA amount ~ arg0 * 1e18 / price
require(((arg0 * 0x0de0b6b3a7640000) / var_e.length) > 0);
uint256 var_g = (arg0 * 0x0de0b6b3a7640000) / var_e.length;
// Transfer VISTA to buyer
(bool success, bytes memory ret0) =
address(0x493361d6164093936c86dcb35ad03b4c0d032076).Unresolved_a9059cbb(var_d); // transfer
...
}
// Decompiled VistaFinance sell/buy contract - sell() snippet
function sell(uint256 arg0, address arg1) public {
require(arg1 == (address(arg1)));
// Read vistaForcePlan oracle price
(bool success, bytes memory ret0) =
address(0xb9c3401c846f3ac4ccd2bdb1901e41c1da463e10).price(); // staticcall
...
// Compute required VISTA from arg0 and oracle price
require(((arg0 * 0x0de0b6b3a7640000) / var_b.length) > 0);
...
// Check allowance and transfer VISTA from seller into this contract
(bool success, bytes memory ret0) =
address(0x493361d6164093936c86dcb35ad03b4c0d032076).Unresolved_dd62ed3e(var_f); // allowance
...
(bool success, bytes memory ret0) =
address(0x493361d6164093936c86dcb35ad03b4c0d032076).Unresolved_23b872dd(var_g); // transferFrom
...
// Pay USDT to seller
(bool success, bytes memory ret0) =
address(0x55d398326f99059ff775485246999027b3197955).Unresolved_a9059cbb(var_i); // transfer
...
}
Caption: Decompiled sell/buy contract uses vistaForcePlan.price() directly to compute VISTA amounts for given USDT values and vice versa, with no checks against the ICO price or external markets.
Adversary EOA: 0x3D71366228EBD5196D45eE72f82405da601190ad
Helper contract: 0x10036dAD92fd0459daAb57C506eA656d46BF5727
VistaFinanceICO::stake.VistaFinance::flashLoan.sell() on the VistaFinance sell/buy contract.The existence and usage of these actors is visible in the seed transaction trace and in the balance diff summary.
The core vulnerability is the inconsistent pricing of VISTA across two protocol components:
vistaForcePlan.price() and, during the incident, treats VISTA as worth approximately 22.86 USDT per token.Because:
vistaForcePlan owner via updatePrice(uint256 _newPrice).Any user who can buy VISTA from the ICO can immediately redeem it into the sell contract at ~22.86x its purchase cost, limited only by the sell contract’s USDT reserves. This creates a deterministic, permissionless arbitrage between two contracts of the same protocol.
The seed transaction’s execution trace shows the sell contract calling the oracle’s price() getter and receiving a value corresponding to 22.86:
# Seed transaction trace (cast run -vvvvv) for tx 0x84c3...
│ │ │ ├─ [130937] VistaFinance::flashLoan(..., 1000000000000000000000000 [1e24], ...)
│ │ │ │ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000,
│ │ │ │ │ to: 0x10036dAD92fd0459daAb57C506eA656d46BF5727,
│ │ │ │ │ value: 1000000000000000000000000 [1e24])
│ │ │ │ ├─ [83727] 0x10036dAD92fd0459daAb57C506eA656d46BF5727::onFlashLoan(...)
│ │ │ │ │ ├─ [78648] 0xf738de9913bc1e21b1a985bb0E39Db75091263b7::sell(
│ │ │ │ │ │ 36438840000000000000000 [3.643e22], 0x10036dAD92fd0459daAb57C506eA656d46BF5727)
│ │ │ │ │ │ ├─ [2362] vistaForcePlan::price() [staticcall]
│ │ │ │ │ │ │ └─ ← [Return] 22860000000000000000 [2.286e19]
Caption: Seed transaction trace showing the sell contract calling vistaForcePlan::price() and receiving 22.86 × 10¹⁸ as the oracle price.
This price (2.286e19) corresponds to 22.86 when interpreted as a standard 18‑decimal fixed‑point value. The decompiled sell/buy contract uses this value in a formula of the form:
VISTA_amount ≈ USDT_amount * 1e18 / price.At the same time, the ICO contract continues to offer VISTA at 1 BUSD per token:
// VistaFinanceICO::stake
function stake(uint256 amount, address sponsor) external {
amount = amount * 10 ** 18;
...
busdToken.transferFrom(msg.sender, address(this), amount);
...
vistaToken.transfer(msg.sender, amount);
...
}
Caption: ICO stake function minting exactly amount VISTA for amount BUSD (1:1 pricing).
Taken together:
Ignoring minor BUSD/USDT differences, this yields an exploitable ~22.86× profit factor between buying from the ICO and selling into the sell contract.
The adversary leverages both the ICO and the VISTA flash‑mint feature to source enough VISTA to meaningfully exploit the mispricing:
Buy VISTA from ICO at 1 BUSD each
From the trace:
# Seed transaction trace – ICO purchase segment
│ │ │ ├─ [69604] PancakeRouter::swapExactTokensForTokensSupportingFeeOnTransferTokens(
│ │ │ │ 2000000000000000000000 [2e21], ..., [WBNB, BUSD], 0x10036d..., ...)
...
│ │ │ ├─ [1081385] VistaFinanceICO::stake(1594, 0x10036dAD92fd0459daAb57C506eA656d46BF5727)
│ │ │ │ ├─ emit PurchasedICO(user: 0x10036d..., amount: 1594000000000000000000 [1.594e21])
Caption: Trace shows the helper contract swapping WBNB to BUSD and then calling VistaFinanceICO::stake(1594, ...), buying 1,594 VISTA‑denominated units (scaled by 1e18).
Flash‑mint additional VISTA
The VISTA token supports ERC‑3156 flash loans via ERC20FlashMint. The trace shows a flash loan of 1,000,000 VISTA (1e24 units):
# Seed transaction trace – VISTA flash-mint segment
│ │ ├─ [130937] VistaFinance::flashLoan(
│ │ │ 0x10036dAD92fd0459daAb57C506eA656d46BF5727,
│ │ │ VistaFinance: [0x493361D6...2076],
│ │ │ 1000000000000000000000000 [1e24], ...)
│ │ │ ├─ emit Transfer(
│ │ │ │ from: 0x0000000000000000000000000000000000000000,
│ │ │ │ to: 0x10036dAD92fd0459daAb57C506eA656d46BF5727,
│ │ │ │ value: 1000000000000000000000000 [1e24])
Caption: Trace evidence that VistaFinance flash‑mints 1,000,000 VISTA to the helper contract for use within the same transaction.
These flash‑minted tokens, together with the ICO‑purchased VISTA, allow the helper contract to work around staking or balance constraints while still repaying the flash loan within the transaction.
Sell 1,594 VISTA into the mispriced sell contract
The trace shows the helper contract calling sell() on the sell/buy contract with a USDT amount of 36,438.84 (3.643884e22 with 18 decimals), and the contract internally pulling 1,594 VISTA from the helper:
# Seed transaction trace – sell() segment
│ │ │ │ ├─ [78648] 0xf738de9913bc1e21b1a985bb0E39Db75091263b7::sell(
│ │ │ │ │ 36438840000000000000000 [3.643e22],
│ │ │ │ │ 0x10036dAD92fd0459daAb57C506eA656d46BF5727)
│ │ │ │ │ ├─ [2362] vistaForcePlan::price() [staticcall]
│ │ │ │ │ │ └─ ← [Return] 22860000000000000000 [2.286e19]
...
│ │ │ │ │ ├─ [40653] VistaFinance::transferFrom(
│ │ │ │ │ │ 0x10036dAD92fd0459daAb57C506eA656d46BF5727,
│ │ │ │ │ │ 0xf738de9913bc1e21b1a985bb0E39Db75091263b7,
│ │ │ │ │ │ 1594000000000000000000 [1.594e21])
│ │ │ │ │ ├─ [29971] BEP20USDT::transfer(
│ │ │ │ │ │ 0x10036dAD92fd0459daAb57C506eA656d46BF5727,
│ │ │ │ │ │ 36438840000000000000000 [3.643e22])
Caption: sell() call uses oracle price 22.86 to accept 1,594 VISTA and pay out 36,438.84 USDT from the sell contract to the helper contract.
Unwind to WBNB and repay flash loan
After receiving 36,438.84 USDT, the helper contract swaps USDT back to WBNB and repays the 2,001 WBNB (principal + fee), leaving surplus WBNB that is then withdrawn to the adversary EOA.
The seed balance_diff.json (pre‑state vs post‑state) confirms the net movement of native BNB (via WBNB) and tokens:
// Seed transaction balance diff summary
{
"chainid": 56,
"txhash": "0x84c385aab658d86b64e132e8db0c092756d5a9331a1131bf05f8214d08efba56",
"native_balance_deltas": [
{
"address": "0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c",
"before_wei": "1332205739154792335149888",
"after_wei": "1332157504159917891237446",
"delta_wei": "-48234994874443912442"
},
{
"address": "0x3d71366228ebd5196d45ee72f82405da601190ad",
"before_wei": "2798071956935597970",
"after_wei": "51031144695127510412",
"delta_wei": "48233072738191912442"
}
],
"erc20_transfers": [
{
"token": "0x55d398326f99059ff775485246999027b3197955",
"from": "0xf738de9913bc1e21b1a985bb0e39db75091263b7",
"to": "0x10036dad92fd0459daab57c506ea656d46bf5727",
"value": "36438840000000000000000"
},
...
]
}
Caption: Balance diff shows ~48.23 WBNB net loss from the WBNB pool and 36,438.84 USDT moving from the sell contract to the adversary helper, matching the exploit description.
The native balance deltas show:
0xbb4c...095c): delta_wei = -48.234994874443912442 BNB equivalent.delta_wei = +48.233072738191912442 BNB equivalent.This is consistent with:
The ERC‑20 transfer section corroborates:
0x55d3...7955) transferred from the sell contract to the helper contract during sell().Combining these, the effective sell‑side price realized is:
36,438.84 USDT / 1,594 VISTA ≈ 22.86 USDT per VISTA, matching the oracle‑reported price.Centralized price oracle without safeguards
vistaForcePlan exposes price as a mutable, owner‑set variable and provides updatePrice(uint256 _newPrice) without any invariants, bounds, or checks against on‑chain markets.Inconsistent pricing across protocol components
price is set to ~22.86. There is no mechanism tying the ICO price to the oracle or vice versa.Sell/buy contract over‑trusts oracle
vistaForcePlan.price() as its authoritative price for both buy and sell, directly converting USDT ↔ VISTA based on the provided value, without any sanity checks or caps relative to its USDT reserves.Flash‑mint amplifies exploitability but is not the root bug
ERC20FlashMint on VISTA allows the adversary to temporarily borrow a large amount of VISTA, making it easier to hit meaningful volumes. However, the core issue remains the price inconsistency between the ICO and sell contract; even without flash‑mint, the arbitrage would exist.Permissionless, reproducible arbitrage
price. It only exploits the already‑mispriced oracle by routing publicly available liquidity and contracts in one transaction. Any unprivileged actor could replicate the same sequence from the publicly observable pre‑state.Security principles violated:
The adversary conducts a single, carefully constructed flash‑loan transaction that:
VistaFinance::flashLoan (ERC20FlashMint) to work around staking constraints.EOA (adversary): 0x3D71366228EBD5196D45eE72f82405da601190ad
native_balance_deltas in balance_diff.json.Helper contract: 0x10036dAD92fd0459daAb57C506eA656d46BF5727
VistaFinanceICO::stake.VistaFinance::flashLoan.VistaFinance approve / transfer / transferFrom.sell() function.Victim protocol components:
VistaFinanceICO (0x7C98b0cEEaFCf5b5B30871362035f728955b328c) – sells underpriced VISTA.VistaFinance token (0x493361D6164093936c86Dcb35Ad03b4C0D032076) – provides flash‑minted VISTA.VistaFinance sell/buy (0xf738de9913bc1e21b1a985bb0E39Db75091263b7) – holds mispriced USDT liquidity and honors overpriced redemptions.vistaForcePlan (0xb9c3401c846f3ac4ccd2bdb1901e41c1da463e10) – mispriced oracle used by the sell/buy contract.0x3D7136... accumulates initial BNB to pay gas and deploys the helper contract 0x10036d....No privileged operations or protocol configuration changes are required at this stage; the attacker simply sets up standard infrastructure.
0x84c385aab658d86b64e132e8db0c092756d5a9331a1131bf05f8214d08efba5643791254 on BSC.0x36696169C63e42cd08ce11f5deeBbCeBae652050).0x58F876857a02D6762E0101bb5C46A8c1ED44Dc16).│ ├─ PancakeV3Pool::flash(0x10036d..., 0, 2000000000000000000000 [2e21], ...)
│ │ ├─ WBNB::transfer(0x10036d..., 2000000000000000000000 [2e21])
...
│ │ ├─ PancakeRouter::swapExactTokensForTokensSupportingFeeOnTransferTokens(
│ │ │ 2000000000000000000000 [2e21], ..., [WBNB, BUSD], 0x10036d..., ...)
│ │ │ ├─ BEP20Token::transfer(0x10036d..., 955431835233099194383151 [9.554e23]) // BUSD out
Caption: Flash loan of 2,000 WBNB and subsequent swap to ~9.55e23 BUSD received by the helper contract.
VistaFinanceICO::stake(1594, helper):
Evidence from trace:
│ │ │ ├─ VistaFinanceICO::stake(1594, 0x10036d...)
│ │ │ │ ├─ BEP20Token::transfer(admin, 1514300000000000000000 [1.514e21]) // 95% BUSD
│ │ │ │ ├─ BEP20Token::transfer(0x10036d..., 79700000000000000000 [7.97e19]) // 5% BUSD
│ │ │ │ ├─ VistaFinance::transfer(0x10036d..., 1594000000000000000000 [1.594e21]) // VISTA
│ │ │ │ ├─ VistaFinance::stakeTokens(... multiple calls ...)
│ │ │ │ ├─ emit PurchasedICO(user: 0x10036d..., amount: 1594000000000000000000 [1.594e21])
Caption: ICO stake sequence where 1,594 BUSD is converted into 1,594 VISTA (plus related staking), confirming the 1:1 ICO price.
│ │ ├─ VistaFinance::flashLoan(
│ │ │ 0x10036d..., VistaFinance: [0x493361D6...2076],
│ │ │ 1000000000000000000000000 [1e24], ...)
│ │ │ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000,
│ │ │ │ to: 0x10036d..., value: 1000000000000000000000000 [1e24])
Caption: Flash‑mint of 1,000,000 VISTA tokens to the helper contract.
These tokens are used transiently during the exploit and are burned back at the end of the flash loan, but they provide temporary balance to satisfy staking and allowance flows.
sell(36438840000000000000000, helper) on the sell/buy contract.vistaForcePlan::price() and receives 22.86 × 10¹⁸.VistaFinance::transferFrom.BEP20USDT::transfer.Evidence (combined trace snippet):
│ │ │ │ ├─ 0xf738de...::sell(36438840000000000000000 [3.643e22], 0x10036d...)
│ │ │ │ │ ├─ vistaForcePlan::price() [staticcall]
│ │ │ │ │ │ └─ ← [Return] 22860000000000000000 [2.286e19]
│ │ │ │ │ ├─ VistaFinance::transferFrom(
│ │ │ │ │ │ 0x10036d..., 0xf738de..., 1594000000000000000000 [1.594e21])
│ │ │ │ │ ├─ BEP20USDT::transfer(
│ │ │ │ │ │ 0xf738de..., 0x10036d..., 36438840000000000000000 [3.643e22])
Caption: sell() call exchanging 1,594 VISTA for 36,438.84 USDT at the mispriced oracle rate of 22.86.
At the end of the transaction:
USDT reserves in the sell contract
0xf738de...) to the adversary helper (0x10036d...) during sell().WBNB / BNB reserves
These figures are directly supported by balance_diff.json for the seed transaction.
Immediate economic loss
Structural risk
price remains higher (e.g., 22.86), andUser‑facing effects (not fully covered by artifacts)
The following references correspond to the on‑disk artifacts used in this report (paths relative to the incident session root). They are listed for provenance; all essential information has been summarized in this document so that readers do not need to inspect the raw files.
[1] Seed transaction metadata and trace
0x84c385aab658d86b64e132e8db0c092756d5a9331a1131bf05f8214d08efba56metadata.json (basic tx info, block, gas).trace.cast.log (full cast run -vvvvv trace).balance_diff.json (pre‑/post‑state balance changes).[2] VistaFinance token and ICO source code
0x493361D6164093936c86Dcb35Ad03b4C0D032076.ERC20FlashMint implementation.[3] VistaFinanceICO contract source
0x7C98b0cEEaFCf5b5B30871362035f728955b328c.stake() logic.[4] VistaFinance sell/buy decompiled contract
0xf738de9913bc1e21b1a985bb0E39Db75091263b7.buy() and sell() implementations calling vistaForcePlan.price() and converting between USDT and VISTA.[5] vistaForcePlan oracle source
0xb9c3401c846f3ac4ccd2bdb1901e41c1da463e10.price variable and its use in stake() and the auto‑generated price() getter.[6] Iter_2 Root Cause Analyzer intermediate analysis
current_analysis_result.json from root cause analyzer iteration 2.