Findings
17 findings across 11 protocols
Selected work lives on the homepage. Every entry here has a working proof-of-concept or an independently traced root cause against real source and, where possible, forked mainnet state. Severity is reported as found. Including the ones that were not exploitable in practice.
Showing 17 of 17 findings
Ellipsis Finance
BSC1 findinglemon.fun
Robinhood Chain1 findingFlex Finance
Ethereum2 findingsEthereum
Lender withdrawal shortfall enables free-collateral self-deal
When a Lender (Yearn V3 vault) withdrawal exceeds idle liquidity, TroveManager.redeem() kicks a Dutch auction with receiver set to the withdrawing depositor themselves. TokenizedStrategy permanently marks the shortfall as a realized loss for all remaining depositors.
Verified with working PoC
Ethereum
Direct-borrower path shares the same auction self-deal bug
The same auction self-deal root cause is separately reachable from TroveManager.open_trove()/borrow(): _transfer_borrow_tokens() calls the internal _redeem() with its default receiver = msg.sender whenever requested debt exceeds idle liquidity.
Verified — second entry point, same root cause
Hydrex
Ethereum1 findingNoon (USN)
Ethereum2 findingsEthereum
RedeemHandler.redeem() has no USN/collateral ratio check
RedeemHandler.redeem() burns order.usnAmount and pays out order.collateralAmount with no on-chain relationship enforced between the two, for any caller holding BURNER_ROLE.
Verified with working PoC
Ethereum
MinterHandlerV2 uncollateralized self-mint
mint()'s zero-amount guard and 2%-band collateral/usnAmount ratio check are both gated behind order.user != msg.sender, so any address holding MINTER_ROLE that is also a whitelisted user can self-mint an arbitrary usnAmount with collateralAmount = 0.
Verified with working PoC
HyperFX
Ethereum1 findingMonolith Market
Ethereum1 findingRipe Protocol
Base1 findingRamses DLMM
Robinhood Chain1 findingOrvex
Robinhood Chain5 findingsRobinhood Chain
VoterV5 stale reward-index lets a late voter capture other gauges' skipped-epoch rewards
VoterV5._vote()/_reset() never refresh a gauge's reward-index checkpoint - unlike real Velodrome, which updates it on every vote action. The checkpoint only advances via distribute()/killGauge(), which is permissionless and unenforced per-epoch, so a gauge that goes unvoted for N epochs then receives even minimal weight captures the full N-epoch accumulated index delta.
Verified with working PoC
Robinhood Chain
ProtocolToken.sol has no burn() - veORVX early-exit claims permanently revert
VotingEscrowV2_LockLogic._claim() calls token.burn(penaltyAmount) whenever an early exit's penalty is nonzero, but the real deployed ORVX token (ERC20 + ERC20Permit + Ownable2Step only) has no burn() function at all. Any user - no privileged role needed - who locks ORVX in a NON_PERMANENT lock and later tries to exit early, accepting the documented penalty, has the transaction unconditionally revert.
Verified with working PoC
Robinhood Chain
oORVX.exerciseVe() unconditionally reverts - same missing-burnFrom root cause, independent entry point
oORVX's exerciseVe() converts option tokens into a permanent veORVX lock via createLockFor(), which unconditionally calls token.burnFrom() for any PERMANENT lock inside VotingEscrowV2_LockLogic._updateLock(). ORVX has no burnFrom() either, so this documented conversion path is completely non-functional for every holder.
Verified — second entry point, same root cause
Robinhood Chain
bveORVX.exerciseVe() calls a function selector that doesn't exist on the real oORVX contract
bveORVX's optionToken is statically typed as the 5-argument OptionTokenV3.exerciseVe(uint256,uint256,address,uint256,uint256) (selector 0xa9f6ee33), but the contract actually deployed at that address only implements the simpler 2-argument exerciseVe(uint256,address) (selector 0x9130325d) - two entirely different functions as far as the EVM is concerned.
Verified with working PoC
Robinhood Chain
MinterUpgradeableV3._initialize() one-time latch never actually latches
The guard require(_initializer != address(0), 'already initialized') is satisfied by any nonzero value, including the sentinel it sets on completion - so it never blocks a second call. The governor can re-run the genesis-distribution function at will, silently rewinding whatever decay curve the emission schedule applies.
Verified with working PoC