ycdeal3 Exploit: $387K Lost to Missing Caller Authorization in RWAVault Withdraw
What Happened
On April 28, 2026, the ycdeal3 RWAVault on Ethereum mainnet was drained of 387,763.999994 USDC in a single transaction. The root cause was a missing caller-authorization check in the vault's withdraw function. The function accepts an arbitrary owner parameter and a separate receiver parameter, but never verifies that the caller is authorized to act on behalf of owner. The attacker called withdraw with eight victim addresses as owner and their own contract as receiver, burning each victim's shares and redirecting the underlying USDC to themselves. The exploited contract was 0xb9c7c84a1aa0dd40b5b38aae815ad0cdd2e5f88a, the attacker contract was 0x50c140c2f705fa9d0bd0f4f253bacf4087588d17, and the attack transaction was 0x6b04344d5627df59d3bc645e7454f4605a90272852a91e435e370376643353b3.
The attacker emitted an on-chain Log event during execution stating: "root cause: no check on the relationship between receiver and owner."
How the Attack Worked
The ERC-4626 vault standard defines withdraw(uint256 assets, address receiver, address owner) to support a common pattern: a depositor approves a third party to manage their position, and that third party calls withdraw on the depositor's behalf. To make this safe, OpenZeppelin's base _withdraw implementation contains the following enforcement:
solidity
if (caller != owner) { _spendAllowance(owner, caller, shares); }
In plain terms, if the caller is not the owner, the call only succeeds if the owner has previously granted the caller an ERC-20-style allowance covering the share amount. This is the standard caller-authorization invariant for ERC-4626 vaults.
The ycdeal3 RWAVault overrode withdraw and redeem to add custom interest-claim and accounting logic, and in the override, the _spendAllowance check was dropped. A grep for _spendAllowance across the project's source returns zero matches. The allowance-consumption helper is never invoked anywhere in the codebase.
Under the override, the exploit path for each victim is:
The function calls _claimRemainingInterest(victim), emitting an InterestClaimed event and advancing the victim's interest accounting.
The function decrements _depositInfos[victim].principal and _depositInfos[victim].shares.
The function calls _burn(victim, shares) to destroy the victim's vault shares.
The function calls IERC20(asset()).safeTransfer(attacker_contract, assets), sending the underlying USDC to the attacker-controlled receiver.
No allowance was ever granted by any victim. No authorization check was ever performed. The attacker batched the calls into a single transaction, draining eight separate depositor positions in sequence, including a Safe smart account holding 100,000 vault shares. After the drain, the attacker contract swapped 5,000 USDC for 2.196 ETH on Uniswap V2 and forwarded the remaining 387,763.999994 USDC to the controlling EOA at 0x7137804200a073f616d92e87007f1f100100b56a.
The identical defect existed in the sibling redeem(uint256 shares, address receiver, address owner), providing a second drain path that bypasses the assets-denominated entry point entirely.
Would Olympix Have Caught It
We ran BugPocer, Olympix's internal audit agent, against the ycdeal3 RWAVault source after the incident. BugPocer identified both vulnerabilities and produced a working proof of concept for each.
The two confirmed High-severity findings:
missing_caller_authorization_on_withdraw flagged that RWAVault.withdraw accepts an arbitrary owner parameter, mutates _depositInfos[owner], burns owner's shares, and transfers the underlying asset to a caller-controlled receiver, all without verifying msg.sender == owner and without calling _spendAllowance(owner, msg.sender, shares). The finding referenced the OpenZeppelin _withdraw base implementation as the canonical pattern the override bypasses, and explicitly noted that a grep for _spendAllowance across the project's source returns zero matches.
missing_access_control (redeem) flagged the identical defect in the sibling redeem function, including the same missing authorization check before _burn(owner, shares) and the same safeTransfer to an attacker-chosen recipient.
A third Medium-severity finding, forced_interest_claim_griefing, was also surfaced. It flagged that _claimRemainingInterest(owner) executes before any authorization check and persists state mutations even when the outer function reverts. The on-chain transaction confirms this finding in practice: eight InterestClaimed events were emitted, one per victim, forcing each victim's interest claim at the attacker's chosen moment alongside the principal drain.
Both High findings were initially classified as unverified because BugPocer's harness could not automatically satisfy the vault's phase preconditions (Collecting → Active → Matured) and withdrawalStartTime requirements. The BugPocer-generated PoC walks the vault through the phase transitions, sets withdrawalStartTime, and reproduces the drain in two tests: test_attacker_drains_victim_via_withdraw and test_attacker_drains_victim_via_redeem. Both promote the findings from unverified to confirmed true positives.
The attacker's own on-chain message ("root cause: no check on the relationship between receiver and owner") is a near-verbatim restatement of what BugPocer flagged. The analysis was retrospective in this case, but the defect class is exactly the type BugPocer is built to surface pre-deployment: an override that silently weakens an inherited safety invariant, with no allowance-consumption helper anywhere in the project's own code to compensate.
Audits are necessary but insufficient. They catch what a human reviewer thinks to look for during a fixed engagement window. An override that drops a single inherited safety check is the kind of regression that survives audit review, especially when the override adds new logic that draws attention away from what was removed. Deterministic, PoC-backed analysis running continuously against every commit is what closes that gap.
Takeaway
The ycdeal3 vault was a textbook case of an override silently weakening a base contract's safety invariant. The OpenZeppelin _withdraw implementation enforced caller authorization. The RWAVault override did not. Nothing in the project's own code reintroduced the check, and nothing in its test suite caught the regression. Continuous, deterministic verification that runs against every commit and produces executable proofs of concept is the structural answer to a class of bug that point-in-time review will keep missing.
Ship code that doesn't end up in a post-mortem like this one. BugPocer runs continuous, PoC-backed analysis against every commit and surfaces the kind of inherited-invariant regressions that audits miss. Book a free demo!
What’s a Rich Text element?
The rich text element allows you to create and format headings, paragraphs, blockquotes, images, and video all in one place instead of having to add and format them individually. Just double-click and easily create content.
A rich text element can be used with static or dynamic content. For static content, just drop it into any page and begin editing. For dynamic content, add a rich text field to any collection and then connect a rich text element to that field in the settings panel. Voila!
Headings, paragraphs, blockquotes, figures, images, and figure captions can all be styled after a class is added to the rich text element using the "When inside of" nested selector system.
Follow-up: Conduct a follow-up review to ensure that the remediation steps were effective and that the smart contract is now secure.
Follow-up: Conduct a follow-up review to ensure that the remediation steps were effective and that the smart contract is now secure.
In Brief
Remitano suffered a $2.7M loss due to a private key compromise.
GAMBL’s recommendation system was exploited.
DAppSocial lost $530K due to a logic vulnerability.
Rocketswap’s private keys were inadvertently deployed on the server.
Hacks
Hacks Analysis
Huobi | Amount Lost: $8M
On September 24th, the Huobi Global exploit on the Ethereum Mainnet resulted in a $8 million loss due to the compromise of private keys. The attacker executed the attack in a single transaction by sending 4,999 ETH to a malicious contract. The attacker then created a second malicious contract and transferred 1,001 ETH to this new contract. Huobi has since confirmed that they have identified the attacker and has extended an offer of a 5% white hat bounty reward if the funds are returned to the exchange.