Reproduced Exploit
Omo — anyone can redeem another user's shares and take the assets
1. The victim deposits 1000e18 assets and receives 1000e18 shares, then approves the router for those shares. 2. The attacker calls redeem(vaultId, 1000e18, attacker, victim) without being the owner. 3. The router transferFroms the victim's shares and releases the underlying to the attacker. 4. Har…
Chain
Other
Category
access-control
Date
Jan 2025
Source
AuditVault
EVM Playground
Source-level debugger — step opcodes and Solidity in sync
The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Balances (native / ERC-20 / NFT), Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.
Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 53319-h-01-anyone-can-redeem-from-users-and-take-the-funds-pashov. Standalone Foundry PoC and full write-up: 53319-h-01-anyone-can-redeem-from-users-and-take-the-funds-pashov_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/access-control/missing-owner-check · vuln/access-control/missing-auth · vuln/logic/missing-check
Reproduction: a self-contained Foundry PoC that compiles and runs with only
forge-std— no fork, no RPC. Full trace: output.txt. PoC: test/53319-h-01-anyone-can-redeem-from-users-and-take-the-funds-pashov_exp.sol.
AuditVault taxonomy: lang/solidity · sector/token · has/github · severity/high · genome: access-roles · direct-drain · mev/frontrun
Key info#
| Impact | HIGH — any caller redeems a victim's vault shares and the attacker's asset balance increases by the victim's full 1000e18 |
| Protocol | Omo — OmoRouter.redeem |
| Vulnerable code | IOmoVault(vault).transferFrom(owner, address(this), shares) |
| Bug class | owner is a parameter. The router spends the allowance the user granted it, then sends the assets to a caller-chosen receiver |
| Finding | Pashov Audit Group — Omo, 2025-01-25 · #53319 (H-01) |
| Report | Omo-security-review_2025-01-25 |
| Source | AuditVault |
| Status | Audit finding, reproduced as a local synthetic |
| Compiler | solc 0.8.27, cancun, via_ir |
The report's redeem pulls shares from owner after that user has approved the router, which is the normal step before a user redeems. The caller does not have to be owner, and receiver is arbitrary. The diff in the report swaps the minus and plus lines; the description and the quoted body agree that the live code uses owner.
TL;DR#
- The victim deposits
1000e18assets and receives1000e18shares, then approves the router for those shares. - The attacker calls
redeem(vaultId, 1000e18, attacker, victim)without being the owner. - The router
transferFroms the victim's shares andreleases the underlying to the attacker. - Harm: attacker balance goes from
0to1000000000000000000000. Victim shares go from that amount to0. The vault's asset balance is0. - Control: a router that sets
owner = msg.senderreverts when a stranger calls it. The victim calling it receives the1000e18assets back.
The vulnerable code#
Quoted from the report and kept as the executed line:
function redeem(uint256 vaultId, uint256 shares, address receiver, address owner) external returns (uint256 assets) {
address vault = vaultOf[vaultId];
// Transfer shares from owner to router first
// @> VULN: spends the router's allowance on `owner`; the caller does not have to be owner
IOmoVault(vault).transferFrom(owner, address(this), shares);
assets = IOmoVault(vault).release(shares, receiver);
}
The control drops the owner argument:
function redeem(uint256 vaultId, uint256 shares, address receiver) external returns (uint256 assets) {
address owner = msg.sender;
address vault = vaultOf[vaultId];
IOmoVault(vault).transferFrom(owner, address(this), shares);
assets = IOmoVault(vault).release(shares, receiver);
}
- function redeem(uint256 vaultId, uint256 shares, address receiver, address owner)
+ function redeem(uint256 vaultId, uint256 shares, address receiver)
// Transfer shares from owner to router first
- IOmoVault(vault).transferFrom(owner, address(this), shares);
+ IOmoVault(vault).transferFrom(msg.sender, address(this), shares);
Root cause#
Approving the router is required for the user to redeem through it. redeem treats that approval as usable by every caller, because owner and receiver are both arguments. The shares move to the router and the assets move to receiver. A searcher who sees the approval in the mempool can take the position before the user does.
Preconditions#
- The victim holds vault shares. Here,
1000e18shares against1000e18assets, 1:1. - The victim has approved the router for those shares. That approval is what the report says the owner sets.
- The attacker has no allowance of their own and starts with
0assets.
Attack walkthrough#
Numbers from output.txt:
- Victim shares before:
1000000000000000000000. - Attacker assets before:
0. redeemreturns1000000000000000000000.- Attacker assets after:
1000000000000000000000. - Victim shares after:
0.
The difference attackerAfter - attackerBefore equals the victim's deposit. The control test uses the same deposit against OmoRouterFixed: a stranger's redeem reverts and the shares stay put; the victim's own call returns the 1000e18 assets to the victim.
Diagrams#
Impact#
The victim loses the assets behind every share they approved to the router. In the PoC that is the entire 1000e18 deposit: the attacker is paid that amount and the vault is emptied. The victim cannot redeem afterwards because their share balance is 0. Any later approval of the router is stealable the same way, including by a transaction ordered in front of the user's own redeem.
Remediation#
Redeem only for msg.sender, or require msg.sender == owner before transferFrom. Do not let the caller choose both the share owner and the asset receiver.
Sources#
- AuditVault finding: https://github.com/Auditware/AuditVault/blob/main/findings/53319-h-01-anyone-can-redeem-from-users-and-take-the-funds-pashov.md
- Pashov report (2025-01-25): https://github.com/pashov/audits/blob/master/team/md/Omo-security-review_2025-01-25.md
OmoRouter.redeem'stransferFrom(owner, address(this), shares)line is the snippet in that report. The audited Omo repository commit was not published with the finding.
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 53319-h-01-anyone-can-redeem-from-users-and-take-the-funds-pashov_exp (evm-hack-registry mirror).
- AuditVault finding: 53319-h-01-anyone-can-redeem-from-users-and-take-the-funds-pashov.
Alerts & third-party analyses
These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.