BSC ORT Staking 1-to-6000 Reward Mint ACT Opportunity
Exploit Transactions
Victim Addresses
0x6f40A3d0c89cFfdC8A1af212A019C220A295E9bBBSC0x1d64327C74d6519afeF54E58730aD6fc797f05BaBSCLoss Breakdown
Similar Incidents
BSC staking pool reentrancy drain
38%OLY staking/router reward-abuse drains BEP20USDT from staking flows
35%Public mint flaw drains USDT from c3b1 token pool
32%StakingDYNA Reward Backdating Drain
31%HedgePay Staking Proxy Repeated forceExit Withdrawal Drain
31%H2O helper-token reward drain from unauthorized claim loop
31%Root Cause Analysis
BSC ORT Staking 1-to-6000 Reward Mint ACT Opportunity
1. Incident Overview TL;DR
On BSC, an adversary-controlled helper contract 0xdD87.. repeatedly deposited 1 ORT into a staking pool via proxy 0x6f40.. and implementation 0x26bc12.., then withdrew the deposit to trigger a 6000 ORT reward mint per index, realizing a deterministic 1-to-6000 ORT payout pattern.
The staking implementation misconfigured per-deposit rewards so that a 1 ORT principal at index 139 was recorded with a fixed 6000 ORT reward and the contract, configured as an authorized ORT minter, minted that full 6000 ORT to the depositor on withdrawal without any proportionality to stake size or duration.
2. Key Background
- ORT is an AnyswapV4ERC20-based token on BSC that supports mint(address,uint256) guarded by an onlyMinter modifier; minters can arbitrarily increase both balances and totalSupply by minting new ORT.
- The staking system comprises a TransparentUpgradeableProxy at 0x6f40.. delegating calls to a StakingPool implementation at 0x26bc12..; users (or helper contracts) call the proxy, which forwards calldata to the implementation via delegatecall.
- Helper contract 0xdD87.. is a single-owner wrapper deployed and controlled by EOA 0x9bbd..; its external methods approve(), invest(), withdraw(uint256), and withdrawToken() all enforce msg.sender == owner and forward calls to the ORT token or the staking proxy.
- The intended design is that users deposit ORT into the staking pool, which tracks principal and rewards per deposit index, and later withdraw both principal and a reward computed according to some staking plan; security requires that minted rewards remain bounded relative to principal and time.
3. Vulnerability Analysis & Root Cause Summary
Root cause category: ATTACK. The StakingPool implementation stores an excessively large fixed reward (6000 ORT) for a 1 ORT deposit at index 139 and, when Claim_reward(139) is called, mints the full 6000 ORT to the depositor via ORT::mint, breaking any invariant that rewards must remain proportionate to deposited principal and staking duration.
The intended invariant is: For every staking deposit index i, let principal_i be the ORT principal and reward_i be the configured reward amount. A reasonable economic safety invariant is that reward_i is bounded as a function of principal_i and time, e.g., reward_i ≤ principal_i * R_max * f(time), where R_max is a bounded per-period rate. In particular, for an instantaneous invest- withdraw cycle (zero holding time), reward_i should satisfy reward_i ≤ principal_i (no more than 100% of principal).
The concrete breakpoint is: In the StakingPool::invest path, when helper 0xdD87.. calls invest(0,1) via proxy 0x6f40.., the implementation records principal_i = 1 ORT and reward_i = 6000 ORT for deposit index 139 in storage_map_m and related slots, as shown by the decompiled code and traces. Later, Claim_reward(139) reads this 6000 ORT reward and calls ORT::mint(0xdD87.., 6000e18), minting 6000 ORT to the depositor; this write violates the invariant by setting reward_i = 6000 * principal_i for zero holding time and directly increases ORT totalSupply by that amount.
4. Detailed Root Cause Analysis
Decompiled StakingPool code (artifacts/root_cause/data_collector/iter_2/contract/56/0x26bc1245B84760 86E85553E60eE5e3e59FeD9BE0/decompile/...) shows an invest(uint256 planId, uint256 amount) function that, when called via proxy 0x6f40.., increments a global deposit index, transfers ORT from msg.sender to the pool, and populates per-deposit storage entries including depositor address, principal, and a precomputed reward amount. For the observed call invest(0,1) from helper 0xdD87.., traces and logs confirm that ORT::transferFrom(0xdD87.., 0x6f40.., 1) moves exactly 1 ORT into the pool and that storage for index 139 is initialized with principal = 1 ORT and reward = 6000000000000000000000 (6000 * 10^18) ORT. Later, when helper.withdraw(139) forwards to withdraw_amount(139) and Claim_reward(139), the implementation checks ownership (storage_map_f[139 * 6] == 0xdD87..), returns the 1 ORT principal via ORT::transfer(0xdD87.., 1), and then calls ORT::mint(0xdD87.., 6000e18) using the stored reward value. Because ORT is configured to treat the staking implementation (via proxy) as an authorized minter (isMinter[0x26bc12..] = true), this mint succeeds and increases both 0xdD87..'s ORT balance and totalSupply by 6000 ORT. Nothing in the invest or Claim_reward logic scales reward by deposit size or time, so a minimal 1 ORT deposit immediately yields a 6000 ORT reward, and the helper contract can repeat this pattern across indices using invest(0,1) followed by withdraw/Claim_reward. This misconfiguration/bug in reward storage and minting logic is the concrete root cause enabling the exploit.
ORT token mint function (verified source, BSC 0x1d64327C74d6519afeF54E58730aD6fc797f05Ba):
function mint(uint256 amount) external override onlyMinter returns (bool) {
_mint(msg.sender, amount);
return true;
}
This shows that only authorized minters (including the staking implementation) can call mint, and that it mints directly to msg.sender.
Helper contract invest() forwarding into the staking proxy (0xdD87.. on BSC):
function invest() public payable {
require(address(msg.sender) == (address(store_a / 0x01)));
var_a = 0xd87aa64300000000000000000000000000000000000000000000000000000000;
uint256 var_b = 0;
var_c = 0x01;
require(address(0x6f40a3d0c89cffdc8a1af212a019c220a295e9bb).code.length);
(bool success, bytes memory ret0) = address(0x6f40a3d0c89cffdc8a1af212a019c220a295e9bb).{ value: 0 ether }Unresolved_d87aa643(var_b); // call
}
/// @custom:selector 0x2e1a7d4d
/// @custom:signature withdraw(uint256 arg0) public payable
/// @param arg0 ["uint256", "bytes32", "int256"]
This demonstrates that the helper is owner-gated to EOA 0x9bbd.. and forwards invest() calls to proxy 0x6f40.., which delegatecalls the staking implementation.
Helper contract withdrawToken() sweeping accumulated ORT to the owner:
function withdrawToken() public payable {
require(address(msg.sender) == (address(store_a / 0x01)));
var_a = 0x70a0823100000000000000000000000000000000000000000000000000000000;
address var_b = address(this);
require(address(0x1d64327c74d6519afef54e58730ad6fc797f05ba).code.length);
(bool success, bytes memory ret0) = address(0x1d64327c74d6519afef54e58730ad6fc797f05ba).Unresolved_70a08231(var_b); // staticcall
uint256 var_c = var_c + (uint248(ret0.length + 0x1f));
require(!((var_c + ret0.length) - var_c) < 0x20);
require(var_d == (var_d));
var_e = 0xa9059cbb00000000000000000000000000000000000000000000000000000000;
address var_f = address(store_a / 0x01);
var_g = var_d;
This function queries balanceOf(this) on ORT and then transfers the full balance to the stored owner address, which is the EOA 0x9bbd.., explaining how profits are periodically realized.
Seed transaction trace excerpt for withdraw/claim tx 0xa916674f...e97 (BSC 56):
│ │ │ └─ ← [Return] true
│ │ ├─ emit Withdraw(user: 0xdD87D807774c8aA9D70FC6aF5912C97FaDBF531B, lockId: 139, amountWithdrawn: 1)
│ │ ├─ [10018] ORT::mint(0xdD87D807774c8aA9D70FC6aF5912C97FaDBF531B, 6000000000000000000000 [6e21])
│ │ │ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0xdD87D807774c8aA9D70FC6aF5912C97FaDBF531B, value: 6000000000000000000000 [6e21])
│ │ │ ├─ storage changes:
│ │ │ │ @ 2: 0x000000000000000000000000000000000000000000230834b2695fb788af5d05 → 0x000000000000000000000000000000000000000000230979f523725ac06f5d05
This excerpt shows ORT::mint(0xdD87.., 6000000000000000000000) and the corresponding Transfer and Claim events for lockId 139, confirming the 6000 ORT reward mint on withdrawal.
5. Adversary Flow Analysis
A single EOA 0x9bbd.. deploys an owner-gated helper 0xdD87.., uses it to approve the ORT staking proxy, then repeatedly calls helper.invest() with amount=1 ORT and helper.withdraw(index) for successive indices to mint 6000 ORT rewards per 1 ORT deposit, periodically sweeping accumulated ORT from the helper to the EOA via withdrawToken().
Adversary-related cluster:
- 0x9bbd94506398a1459f0cd3b2638512627390255e (chainid 56): EOA=true, contract=false. Sender of both seed transactions 0x49bed8.. and 0xa91667.., payer of BNB gas (as shown in tx_metadata, receipts, and balance_diff.prestate.json), and owner encoded in helper contract 0xdD87..'s storage; every call to 0xdD87.. in the txlists has from=0x9bbd.., showing direct control.
- 0xdD87D807774c8aA9D70FC6aF5912C97FaDBF531B (chainid 56): EOA=false, contract=true. Owner-gated helper contract whose decompiled code shows store_a encoding a single owner (0x9bbd..); its methods invest(), withdraw(uint256), approve(), and withdrawToken() can only be called by that owner and are used to drive all observed staking interactions and to sweep ORT to the EOA, making it an adversary-controlled contract within the cluster.
Victim candidates:
- ORT token on BSC (chainid 56), address 0x1d64327C74d6519afeF54E58730aD6fc797f05Ba, is_verified=true
- ORT staking proxy on BSC (chainid 56), address 0x6f40A3d0c89cFfdC8A1af212A019C220A295E9bB, is_verified=false
- ORT staking implementation (StakingPool) on BSC (chainid 56), address 0x26bc1245B8476086E85553E60eE5e3e59FeD9BE0, is_verified=false
Lifecycle stages:
-
Adversary funding and helper deployment
- Txs: 0xbdb193c290dd9bf2c7d37748ad62fc99c090d704a09542b5a6e4be2c222f8a7c (block 24850538, transfer), 0xba96a07bdcdb31eaf7570d4391b3282454982f5fb630b71ae29c82b7997ae25e (block 24850558, contract_deploy)
- Effect: EOA 0x9bbd.. receives BNB funding in block 24850538 and then deploys helper contract 0xdD87.. in block 24850558; decompiled code shows 0xdD87.. storing 0x9bbd.. as its sole owner and exposing approve(), invest(), withdraw(), and withdrawToken() functions. This establishes the adversary-controlled helper used throughout the exploit.
- Evidence: Funding and deployment txs are listed in artifacts/root_cause/data_collector/iter_2/address/56/0x9bbd94../txlist.normal.24800000_24900000.json, and helper decompilation is in artifacts/root_cause/data_collector/iter_1/contract/56/0xdD87D807774c8aA9D70FC6aF5912C97FaDBF531B/decompile/.
-
Helper approval and staking deposit
- Txs: 0x49bed801b9a9432728b1939951acaa8f2e874453d39c7d881a62c2c157aa7613 (block 24850697, transfer)
- Effect: The adversary calls helper.invest() (tx 0x49bed8..) after a prior helper.approve() call (visible in the EOA txlist) has approved ORT spending by proxy 0x6f40..; StakingPool::invest(0,1) is executed via proxy delegatecall, transferring 1 ORT from 0xdD87.. to 0x6f40.. and recording a deposit at index 139 with principal 1 ORT and a misconfigured 6000 ORT reward.
- Evidence: See cast traces and receipts for tx 0x49bed8.. under artifacts/root_cause/data_collector/iter_1/tx/56/0x49bed8../, and the decompiled StakingPool implementation under artifacts/root_cause/data_collector/iter_2/contract/56/0x26bc12../decompile/.
-
Withdraw, reward mint, and periodic sweeping
- Txs: 0xa916674fb8203fac6d78f5f9afc604be468a514aa61ea36c6d6ef26ecfbd0e97 (block 24850710, transfer), 0x3db4f54622b0638d5ac2d5003a513c2ca70e275f3062fe551d4c800767cfb484 (block 24850761, transfer)
- Effect: In tx 0xa91667.., helper.withdraw(139) forwards to the staking proxy, which executes withdraw_amount(139) and Claim_reward(139). The pool returns the 1 ORT principal from 0x6f40.. to 0xdD87.. and mints 6000 ORT to 0xdD87.. via ORT::mint, completing the 1-to-6000 ORT profit cycle for index 139. Shortly afterward, tx 0x3db4f5.. (and subsequent similar txs) call helper.withdrawToken(), whose decompiled logic transfers the full ORT balance of 0xdD87.. to the owner 0x9bbd.., periodically sweeping accumulated ORT rewards from the helper to the EOA.
- Evidence: Withdraw and mint behavior is evidenced by trace.cast.log and tx_receipt.json for 0xa91667.. under artifacts/root_cause/data_collector/iter_1/tx/56/0xa91667../, including ORT Transfer and mint logs and proxy Withdraw/Claim events. The helper.withdrawToken() implementation and its effect of transferring balanceOf(this) to the owner are shown in artifacts/root_cause/data_collector/iter_1/contract/56/0xdD87D8../decompile/0xdD87D8..-decompiled.sol, and EOA txlist entries for withdrawToken() are in artifacts/root_cause/data_collector/iter_2/address/56/0x9bbd94../txlist.normal.24800000_24900000.json.
6. Impact & Losses
- Token ORT: 6000+ per exploited index (e.g., 6000 ORT minted for index 139), aggregating to a large inflationary increase in ORT supply across repeated cycles.
For each 1 ORT deposit processed under the misconfigured staking plan, the adversary mints 6000 new ORT to its helper contract and later sweeps those tokens to its EOA, paying only modest BNB gas. Repeating this cycle across many indices allows the adversary cluster to inflate ORT totalSupply by multiples of 6000 ORT per index with no corresponding economic input, diluting other holders and undermining the token's economic integrity.
7. References
- [1] ORT token source and storage layout — artifacts/root_cause/seed/56/0x1d64327c74d6519afef54e58730ad6fc797f05ba/src/ORT.sol
- [2] Staking implementation decompilation (0x26bc12..) — artifacts/root_cause/data_collector/iter_2/contract/56/0x26bc1245B8476086E85553E60eE5e3e59FeD9BE0/decompile/0x26bc1245B8476086E85553E60eE5e3e59FeD9BE0-decompiled.sol
- [3] Staking proxy decompilation (0x6f40..) — artifacts/root_cause/data_collector/iter_2/contract/56/0x6f40A3d0c89cFfdC8A1af212A019C220A295E9bB/decompile/0x6f40A3d0c89cFfdC8A1af212A019C220A295E9bB-decompiled.sol
- [4] Helper contract decompilation (0xdD87..) — artifacts/root_cause/data_collector/iter_1/contract/56/0xdD87D807774c8aA9D70FC6aF5912C97FaDBF531B/decompile/0xdD87D807774c8aA9D70FC6aF5912C97FaDBF531B-decompiled.sol
- [5] Invest tx trace and receipt (0x49bed8..) — artifacts/root_cause/data_collector/iter_1/tx/56/0x49bed801b9a9432728b1939951acaa8f2e874453d39c7d881a62c2c157aa7613/
- [6] Withdraw/claim tx trace, receipt, and prestate diff (0xa91667..) — artifacts/root_cause/data_collector/iter_1/tx/56/0xa916674fb8203fac6d78f5f9afc604be468a514aa61ea36c6d6ef26ecfbd0e97/
- [7] EOA 0x9bbd.. extended tx history around exploit blocks — artifacts/root_cause/data_collector/iter_2/address/56/0x9bbd94506398a1459f0cd3b2638512627390255e/txlist.normal.24800000_24900000.json