OpenEden Disclosed Report

Oracle Sandwich Attack Enables Risk-Free Arbitrage in T-Bill Vault

Company
Created date
Jul 23 2025

Target

https://github.com/OpenEdenHQ/openeden.vault.audit/tree/d18288e944df21729b18d430b2afec2da99b6287

Vulnerability Details

Summary

Users can exploit the absence of time lock or delay between deposits/withdrawals and T-Bill oracle price updates to extract risk-free profits through sandwich attacks. By monitoring oracle price feeds off-chain, attackers can selectively participate only in profitable price movements while avoiding any losses, creating an unfair advantage over legitimate investors.

Description

The vulnerability stems from the immediate execution of deposits and withdrawals in relation to oracle price updates, allowing sophisticated actors to sandwich oracle updates for guaranteed profits.

Attack Mechanism

  1. Share Calculation: When users call the deposit function, their share amount is calculated using the shares formula

  2. Price Dependency: The T-Bill/USDC rate is determined by the tbillRateFormula, which fetches tbillUsdPrice from the tbillPriceOracle that update Price every day according to the natspac

  3. Sandwich Strategy: Attackers can:

    • Monitor oracle price update in mempool
    • front run price updates
    • Redeem shares immediately after the price update
    • Skip participation entirely when price updates are negative

Proof Of Conecpt

  • Paste the following PoC in: "deposit/withdraw tbill/usdc 1:1"

  • Run it via: npx hardhat test --grep "Oracle Update can be sandwitched"

    it ("Oracle Update can be sandwitched",async function () {
      // Fetch the current prices

      const tbillPrice= await tbillOracle.latestAnswer();
      console.log ("tbillPrice:",tbillPrice); // 1:1 tbill/USD

      // investor1 observed that there is an update
      // He made a deposit
      await vaultV4.connect(investor1).deposit(_100k,investor1.address);
      const inv1shares= await vaultV4.balanceOf(investor1.address);
      const amountInvested = _100k;
      console.log("inv1shares",inv1shares);

      // previewing converting user shares to assets before update
      const inv1SharesToAssets=await vaultV4.previewRedeem(inv1shares);
      console.log("inv1sharesToAssets",inv1SharesToAssets)

      // Updating the price to 1:1.2 USD
      const NewtbillOraclePrice = BigNumber.from("100999999"); // to fit in the 1% max deviation
      await tbillOracle.updatePrice(NewtbillOraclePrice);
      const newPrice = await tbillOracle.latestAnswer();
      console.log("newPrice",newPrice);

      // previewing shares to assets after update
      inv1SharesToAssets=await vaultV4.previewRedeem(inv1shares);
      console.log("inv1sharesToAssets",inv1SharesToAssets)

      // Investor1 tries to redeem his shares
      // Prepare redemption contract
      await usycTokenIns.connect(usycTreasuryAccount).approve(usycRedemptionIns.address, _10M);
      const investor1Balance=await vaultV4.balanceOf(investor1.address);

      // Investor1 redeems their shares
      const investor1RedeemTx = await vaultV4.connect(investor1).redeemIns(investor1Balance, investor1.address);
      const investor1Receipt = await investor1RedeemTx.wait();

      const investor1RedeemEvent = investor1Receipt.events?.find(e => e.event === 'ProcessWithdraw');

      if (investor1RedeemEvent) {
        // args[2] = total assets before fee deduction
        // args[4] = actual assets transferred to user (after fee deduction)
        // args[8] = total fee deducted

        const investor1AssetsReceived = investor1RedeemEvent.args[4]; // _assetsToUser
        const investor1FeesPaid = investor1RedeemEvent.args[8];       // _totalFee


        console.log(`Investor1 (non-partner) received assets: ${investor1AssetsReceived}`);
        console.log(`Investor1 total fees paid: ${investor1FeesPaid}`);

        const profit =  investor1AssetsReceived-amountInvested;
        console.log(`Investor1 profit: ${profit}`); // 900$ on 100k deposit approx 1% freeRisk Money
        }
      });


Impact

  • Risk-Free Returns: Attackers capture ~1% profit per positive oracle update with zero downside risk
  • Scalable Attack: Profits scale linearly with available capital (e.g., $1M investment (if under max cap) → ~$10k profit per update)
  • Frequency: With daily oracle updates and selective participation, attackers can achieve outsized annual returns

Mitigation

  1. Deposit/Withdraw Lock before oracle Price Update
  2. Use TWAP instead of fixed price to use avg price at time interval

Validation steps

Step 1: MemPool Monitoring

  • The attacker monitors the T-Bill price oracle off-chain or watches the mempool for upcoming price updates. They identify that the oracle price is about to increase from 1.0 to approximately 1.01 (a 1% appreciation).

Step 2: Front-Run oracleUpdate with a Deposit

  • Before the oracle price update executes, the attacker quickly deposits $100,000 into the vault. At this moment, the T-Bill price is still at the old rate (1.0), so the attacker receives shares calculated at the lower, pre-update valuation.

Step 3: Oracle Price Update

  • The oracle price update transaction executes, updating the T-Bill price from 1.0 to 1.01. This 1% increase should theoretically benefit all existing vault participants proportionally.

Step 4: Immediate Withdrawal

  • Immediately after the price update, the attacker redeems all their shares. Since the vault now values T-Bills at the higher price (1.01), the attacker's shares are worth more than what they originally paid.

Step 5: Risk-Free Profit The attacker receives approximately $100,900 back (after fees) on their $100,000 deposit - capturing nearly the entire 1% price appreciation as pure profit, despite holding the position for only seconds.

Attachments

hidden
CommentsReport History
Comments on this report are hidden
Details
Statedisclosed
Severity
Medium
Bounty$375
Visibilitypartially
VulnerabilityTransaction-Ordering Dependence (TOD) / Front Running
Participants (4)