This is everything I tested on the vault contract, version 3.0.1, before freezing it for launch: the review layers, the test suite, and a series of 16 attacks. Each attack transaction links to HashScan, so you can check it on the chain yourself.
This is my own testing, not an audit. An independent, professional audit comes before the vault ever handles real money. The background is in my story.
Of the 16 attacks, 13 held on the frozen v3.0.1 code. One held on the previous version, on code that didn't change, and was not repeated. Two are known trade-offs, described under known limits. No internal check broke in any attack.
All attacks ran on Hedera testnet, against vaults deployed from the same frozen v3.0.1 code (tag testnet-v3.0.1, commit 74b45c6). The attack vaults were set up to be broken on purpose; the vault you can try is C.
| Vault | Contract | Role |
|---|---|---|
| Vault B | 0.0.10764329 | v2.0.7.1, used for ATT-14 |
| Vault C | 0.0.10974199 | the testnet vault you can try |
| Vault D | 0.0.10975125 | attack vault, wound down by ATT-21 |
| Vault E | 0.0.11022200 | attack vault for ATT-13 and ATT-16 |
| Layer | What it covers | Outcome |
|---|---|---|
| Dependency scan | Aikido: dependencies and infrastructure | Two dependency CVEs fixed; contract clean |
| Static analysis | Slither | 121 findings, each triaged; no real vulnerability |
| Self-review | Manual review, proven with mutation testing | Three issues (M1, M2, M3) found and fixed |
| AI contract reviews | Gemini on HbarVault, and separately on the pricing and swap libraries | Rail-snapshot isolation confirmed, M1/M2 corrections applied; fee math matches Uniswap V3 exactly; nothing Critical or High |
| Mathematical verification | Halmos, symbolic proofs of key properties | Three properties proven; two left undecided by solver limits, covered by fuzzing and on-chain runs |
| 48-hour live run | The dead-man switch on the real chain | M2 confirmed and verified on-chain |
An attacker agent came at the vault the way real attackers have come at vaults over the past ten years. Each attack checked seven internal invariants before and after. Open an attack to read what it tried, why it was stopped, and its transactions; each one opens on HashScan. Two attacks needed no transaction: they were read-only checks against the contract.
The attack. A stranger's wallet tries all 16 functions that only the owner, the operator or a depositor themselves may call: opening and closing positions, changing settings, cancelling someone's deposit.
Why it held. Every one of those functions checks who is calling. All 16 calls were refused (NotOwner, NotOperator, NotEntryOwner).
Recorded result: PASS. Vault D.
The attack. The owner, me, tries to use its own legitimate powers to get at participants' money: withdrawing the operational reserve, pointing the fee at itself, and setting the deposit cap to 1 to trap people in.
Why it held. The reserve is a separate gas pot, so withdrawing it touched nothing of the participants. Changing the fee recipient created no shares, because a fee only arises on real profit at withdrawal. The deposit cap only limits new deposits: a withdrawal went through while the cap was 1. Vault value and total shares stayed the same.
Recorded result: PASS — owner power bounded, NAV-for-users unchanged. Vault D.
The attack. Clogging the deposit queue so other people can't get through: dust deposits of 1 tinybar, empty deposits, cancelling someone else's entry, and pointing at a queue position that doesn't exist.
Why it held. There is a minimum deposit, an empty deposit is refused, only the person who queued an entry can cancel it, and an invalid position is rejected. All four calls were refused.
Recorded result: PASS. Vault D.
The attack. Re-entrancy, the pattern behind some of the largest hacks: a hostile contract receives a refund and, during that payout, calls back into the vault to cancel, claim or withdraw again, hoping to be paid twice.
Why it held. Every payout function is locked while it runs. All three call-backs were rejected, and the contract received exactly one payout of 5 HBAR.
Recorded result: PASS — nonReentrant, single payout. Vault D.
The attack. One participant tries to reach another participant's money: cancel their deposit, withdraw more shares than they own, or claim someone else's payout.
Why it held. Shares and payouts are kept per wallet. You can only cancel your own entry, withdraw your own shares and claim your own balance. All five calls were refused.
Recorded result: PASS. Vault D.
The attack. The other way round: does the emergency exit really open when the bot goes silent, and can an ordinary participant use it without me?
Why it held. Before the 48 hours were up, the emergency functions were refused. After 48 hours without a heartbeat, a participant who isn't the owner could start closing positions herself, without a fee. Checked with a read-only call, so nothing was changed.
Recorded result: PASS — emergency exit opens for non-owners, proven non-destructively. Vault D.
No transaction: a read-only call against the contract, nothing was broadcast.
The attack. A free bet: queue a deposit, watch for an hour whether the price moves your way, and cancel if it doesn't. The other participants would pay for that option.
Why it held. A deposit can't be cancelled during its first hour. The immediate cancel was refused (CancelNotYetAllowed).
Recorded result: PASS — DEPOSIT_CANCEL_DELAY enforced. Vault D.
The attack. Dodging the 15% fee by pushing the pool price just before your withdrawal is processed, so your profit looks smaller than it is.
Why it held. The vault only processes the queue when the live price is close to its 30-minute average. With the price pushed 253 ticks away (the limit is 200), processing was refused. A manipulated pool makes the withdrawal wait; it doesn't give it a better price.
Recorded result: PASS — processQueue reverts TwapSpotDeviationTooHigh. Vault E.
The attack. Manipulating the pool of the reward token so the vault sells its SAUCE rewards at a bad price.
Why it held. A reward swap must return at least 97% of the averaged price, and the pool price must be within its band. It held live on v2.0.7.1, and that code is byte-identical in v3.0.1.
Recorded result: PASS-INHERITED (see §4.3). Vault B (v2.0.7.1).
The attack. After everyone has left, a newcomer deposits into the nearly empty vault and collects the rewards that were still being released to the people before them.
How it is contained. The contract doesn't prevent this. My operations do: no fresh deposits into an emptied vault while rewards are still being released.
Recorded result: FINDING-BY-DESIGN (see §4.2). Vault E.
The attack. Forcing withdrawals the vault can't cover, to cause short payouts or a stuck queue.
Why it held. You can only withdraw your own shares, so an attacker's withdrawal is small compared with what the vault holds free. The contract also refuses to process a withdrawal it can't pay in full. Three full withdrawals were processed cleanly.
Recorded result: PASS — withdrawals capped at own shares, wallet covers. Vault D.
The attack. Feeding the vault an old or false snapshot of its positions, so it prices deposits wrongly.
Why it held. Anyone may refresh the snapshot, but it can only record the live position amounts, and it is wiped on every position change. Pricing always reads the live positions.
Recorded result: PASS — snapshot cleared on position change, price always live. Vault D.
The attack. Calling the function that collects the trading fees the positions earned, to grab them or send them elsewhere.
Why it held. Only the operator may call it. The call was refused (NotOperator).
Recorded result: PASS — onlyOperator. Vault D.
The attack. Driving up the number of positions until one processing step needs more work than Hedera allows, so the vault gets stuck.
Why it held. Only the operator can open positions, with a hard limit of 8. A ninth was refused, and each step stays well under Hedera's limit (14 of 50 child records).
Recorded result: PASS — cap of 8 positions, 9th reverts TooManyPositions; processQueue(50)@8 = 1 child record. Vault D.
No transaction: a read-only call against the contract, nothing was broadcast.
The attack. With the bot gone for 48 hours plus seven days, can a stranger wind the whole vault down, and can they profit from doing it?
Why it held. Anyone can wind it down, but it only opens the exit. Nothing was drained, no one's shares changed, and the caller received exactly her own share and paid the gas.
Recorded result: PASS (see §4.1). Vault D.
The attack. Someone steals the operator key and, during a sustained stablecoin de-peg, swaps the vault's HBAR into USDC at a bad rate.
How it is contained. This is a known limit, not fully blocked. The daily swap cap in the contract bounds the damage, a short spike is refused, the bot halts swaps during a de-peg, and the monitor raises an alarm. Four swaps went through, the fifth was refused.
Recorded result: BOUNDED-LOSS (see §4.4). Vault D.
After 48 hours without the bot plus seven days of grace, a wallet that wasn't mine called the wind-down. It only opened the exit: no positions were drained, no one's shares changed, and the caller paid only gas (−0.202 HBAR). Her share of the payout was exactly her 17.24% pro-rata, the same formula as everyone. 11 of 11 checks green.
During a sustained stablecoin de-peg, someone holding the operator key could swap HBAR to USDC at a bad rate. Measured on vault D: four swaps went through and rotated 83.8% of the daily cap; a fifth was refused by the contract. A short price spike is caught; only a de-peg that lasts at least 30 minutes gets through.
Bound: damage per day ≤ de-peg × daily-cap fraction × vault value, and never beyond the amount rotated. Three mitigations: the daily cap in the contract, a de-peg halt in the bot, and the independent monitor.
If everyone leaves and a new depositor comes in while rewards are still vesting, that newcomer inherits those rewards. On vault E a 10 HBAR deposit inherited about 14.98 HBAR. The contract doesn't prevent this; an operational rule does: no fresh deposits into an emptied vault while rewards remain.
Held live on v2.0.7.1 (vault B). The attacked code is byte-identical in v3.0.1, and repeating it on testnet isn't possible: it needs about 40,000 SAUCE through the pool, and only about 263 exist there. So it counts as carried over, not as one of the 13.
Each transaction can also be read straight from the Hedera mirror node: testnet.mirrornode.hedera.com/api/v1/transactions/<id> or /api/v1/contracts/results/<0x-hash>.
AI agent · 奈緒