What we tried to steal
Five ways to take money out of a fund you manage, each written as a test that actually runs against a fork of Robinhood Chain mainnet, using the real NVDA pool and the real Chainlink feed. All five are refused. The names below are the tests: they are in the repository and they run in about twelve seconds.
This is not an audit. It is the set of attacks we could think of, run against real liquidity rather than a mock. An attack nobody thought of is not on this page.
The attacks
The manager calls withdraw on money that is not theirs
test_Attack_ManagerCannotWithdrawRefused. There is no path that pays anyone but a share holder, in proportion to their shares.
The manager mints a token, then makes the fund buy it with everything
test_Attack_BuyOwnWorthlessTokenRefused. Both sides of a trade must be on the list the factory was deployed with, and that list cannot be added to.
The manager hands the fund a contract to call
test_Attack_NoArbitraryCallPathRefused. No function takes a target or calldata, so the argument does not exist to pass.
Somebody who is not the manager places a trade
test_Attack_OutsiderCannotTradeRefused.
The manager promises no minimum and takes a deliberately terrible fill, collecting the difference on the other side
test_Attack_ZeroSlippageSelfSandwichRefused on the price the trade actually got. This is the one a slippage limit alone cannot stop, because the manager sets the limit.
An ordinary trade, then a backer leaves
test_HonestTradeAndExitPassed. 10,000 USDG in, 5,000 traded into 23.355 NVDA, fund worth 9,997.05 afterwards. A cost of 0.03%.
The rules, one question each
| Question | Answer | Test |
|---|---|---|
| Can a manager take any cut of the profit they like? | No. The factory refuses anything above 30%. | test_FactoryRefusesAGreedyFee |
| Can they charge any yearly fee they like? | No. Capped at 2%. | test_FactoryRefusesAGreedyAnnualFee |
| Can they trade before putting their own money in? | No. | test_ManagerMustBeInvested |
| Are they paid while the fund is down? | No. | test_NoFeeOnALoss |
| Does a late backer pay a fee on profit earned before they arrived? | No. The high-water mark is held per backer. | test_LateDepositorPaysNoFeeOnSomebodyElsesProfit |
| Can somebody leave while the feed is frozen? | Yes, in kind, and the manager is not paid. | test_ExitWorksWhileTheFeedIsFrozen |
| Can a manager raise a fee after you are in? | No. There is no function that raises one. | test_TermsOnlyEverEase |
| Does shortening a lockup free people already in it? | Yes. It is measured from each deposit rather than stamped as a deadline. | test_ShorteningTheLockupReleasesPeopleAlreadyIn |
| Can giving up an asset trap what the fund already holds of it? | No. A retired asset can still be sold. | test_RetiringAnAssetStopsBuyingAndStillAllowsSelling |
| Does taking the performance fee create new shares? | No, and it used to. See below. | test_TakingTheFeeMintsNothing |
A bug this found
The performance fee was being minted rather than moved. The fee is supposed to come out of the shares a leaving backer is redeeming and be handed to the manager. The helper that credited the manager also increased the total share count, so every fee taken created a second copy of itself and quietly diluted everybody who stayed.
It was invisible in ordinary use and invisible to every test that only checked the manager got paid. What caught it was the fork test asserting that after the last backer leaves, the only shares still in existence are the ones the manager earned.
// before: this bumped the supply, whether shares were being
// created or only changing hands
function _credit(address who, uint256 shares, uint256 price) private {
...
totalShares += shares;
}
// after: the callers say which it is
_credit(msg.sender, shares, price);
totalShares += shares; // a deposit creates shares
_credit(manager, feeShares, price); // the fee only changes handsPinned by test_TakingTheFeeMintsNothing, which checks the arithmetic directly rather than waiting for a fork to disagree.
What is not covered
- No third-party audit. Nobody outside this project has reviewed the contract.
- The venue is trusted. A fund routes through one Uniswap V3 router and reads Chainlink. If a feed reports a wrong price confidently, the fund believes it.
- The manager can still be bad at their job. None of this stops somebody losing your money honestly, which is the ordinary way money is lost.
The Risk page is the longer version of this list.