Audit sheet
Scored by the DreamPad team against the rubric on the right, from the five review rounds below. Every finding, its fix and its regression test are listed on this page.
- Findings
- 56 / 56 fixed
- Tests
- 315
- Rounds
- 5
- Updated
- 2026-09-16
How the score is computed
Fixed rubric, updated only when a milestone lands. Each row says what earns the missing points.
| Part | Points | Basis |
|---|---|---|
| Every finding closed | 45 / 45 | 56 of 56 findings fixed across 5 rounds, the exploit proofs kept as regression tests |
| Adversarial test suite | 20 / 20 | 315 Foundry tests against real Uniswap V2, V3 and V4 bytecode, including every attack proof from the audit rounds |
| Static analysis | 15 / 15 | Slither reports nothing above informational on the current contracts |
| Keys and operations | 20 / 20 | the mainnet factories and oracles on Ethereum and Robinhood Chain are owned by a 2-of-2 Safe, which is also the treasury; a deploy script that refuses a live chain without a Safe as owner and treasury, a clean secret sweep of the whole git history, a nonce Content Security Policy and a browser key that locks itself |
| Total | 100 / 100 | A from 90, B+ from 80, B from 70, C from 55. |
Scope: every contract in src/, the deployment scripts, and the web app in web/src. Method: a full manual read of the contracts, two independent adversarial passes over the contracts and one over the web app, every finding re-verified against the code, and a regression test for each contract fix (Foundry suite: 315 tests after round 5, the pair-asset guard and instant pools, real Uniswap V2, V3 and V4 bytecode). This is an internal review by the team that wrote the code. It is not a substitute for an independent audit before mainnet.
Severity: Critical = funds of other users can be taken. High = funds can be taken under a plausible precondition, or a coin can be bricked cheaply. Medium = value leaks, griefing or privilege abuse with bounded damage. Low / Info = hardening.
internal audit
| # | Severity | Where | Finding | Fix | Status |
|---|---|---|---|---|---|
| 1 | Critical | DreamToken._update, BondingCurve.buy | A curve buy could deliver tokens to any address, including a pre-created Uniswap pair. An attacker could pre-mint LP in the graduation pair (tokens from the curve plus their own WETH), own ~90% of the pool after graduation, and withdraw most of the raise. | Pre-graduation the curve may not deliver into a pool of the coin (token0()/token1() check), the adapter, the vault or the token. Only the curve pulls tokens back. The curve announces graduation (startGraduation) so the adapter hand-off stays allowed. Test: test_setAmmPairOnlyAcceptsRealPools, graduation suites. | Fixed |
| 2 | High | UniswapV3Adapter._ensurePool | Anyone can create and initialise the V3 pool before graduation at any price without holding a token. The adapter seeded at that price, handing the coin's liquidity to whoever set it. | The adapter pulls the pool to the curve's closing price first: on an empty pool the swap moves the price for free; against pre-placed liquidity it trades at most half of one side (buying cheap or selling dear) and seeds with what is left. Leftovers are computed from balances. Tolerance 0.1% on sqrt price. Tests: test_preInitialisedPoolCannotBlockGraduation, test_mispricedPoolWithLiquidityIsPulledTowardTheCurvePrice. | Fixed |
| 3 | High | BondingCurve.sellFor, DreamPadFactory.setZap | sellFor trusted factory.zap() live. An owner (or a stolen owner key) could point it at a contract that sells every approved holder's curve tokens. | Each curve snapshots the zap at launch; sellFor only honours that address. Tokens get a permissionless syncZap() to re-apply exclusions for a new zap. | Fixed |
| 4 | High | DreamToken.setTaxSplit | The holder / creator split was changeable after launch, contradicting the launch page, so a creator could move 90% of every fee to themselves. | Removed. There is no setter for taxes or the split. Test: test_splitIsFixedForever. | Fixed |
| 5 | High | DreamZap refund leg, venue choice | The pair-asset refund of a sell-out buy was sold back with no minimum, and venues were chosen by raw in-range liquidity, so a dust or one-tick pool could capture the route and the refund. An empty V2 pair also broke every ETH trade for that pair asset. | Venues are chosen by quoted output for the actual amount; empty V2 pairs are not venues; the refund leg must return within 5% of the rate the same transaction bought at, else the buy reverts. Previews use the same selection. | Fixed |
| 6 | Medium | DreamToken.transferCreator | The new creator was not excluded from holder rewards, so a launch wallet could hand the role to a second wallet and earn both shares. | Exclusions follow the role: the new creator is excluded from rewards and inherits the tax / max-wallet exemptions; every past creator stays excluded. Tests: test_transferCreatorMovesExclusions, test_creatorCanNeverEarnHolderRewards. | Fixed |
| 7 | Medium | DreamToken.setExcludedFromRewards, setMinHoldForRewards | The creator could re-include themselves, exclude every large holder, or set a minimum hold above everyone's balance but one, steering all holder rewards to one wallet. | Creator wallets (past or present) can never be included; only contracts can be excluded; minimum hold capped at 0.1% of supply. | Fixed |
| 8 | Medium | DreamToken.setAmmPair | Any address could be flagged as a pool (taxing its transfers and stripping its rewards), and un-flagging any address re-included protected ones. | Flagging requires a real pool of the coin; un-flagging requires a flagged pool. | Fixed |
| 9 | Medium | DreamPadFactory.refreshQuoteAsset, registerQuoteAsset | Permissionless re-sizing from a spot price could sandwich a launch into a wrong raise target; owner-disabled assets could be re-registered. | Refresh once an hour, 25% per step; owner-disabled assets stay disabled. Tests: test_refreshFollowsPriceSlowly, test_ownerDisabledAssetCannotBeReregistered. | Fixed |
| 10 | Medium | UniswapV3Oracle | Spot prices from a single pool; a shallow registered V4 pool (with any hook) displaced the real market for pricing and routing. | 10-minute TWAP when the pool keeps history; V4 registration only for hookless pools or template hooks, above minLiquidity; the deepest V4 pool always wins. Precision fixed for cheap 18-decimal assets against 6-decimal stables. Test: test_shallowRegisteredPoolCannotDisplaceTheRealMarket. | Fixed |
| 11 | Medium | LiquidityVault.harvest, LiquidityVaultV3.harvest, DreamToken._swapBack | Floor-less sales of the token side could be sandwiched, and they are triggered by anyone or at predictable moments. | Per-sale size caps: 0.5% of the pair's token reserve (V2 harvest and swap-back) or a 1% sqrt-price move (V3 harvest); the remainder waits for the next call. | Fixed |
| 12 | Medium | UniswapV3Adapter.uniswapV3MintCallback | The callback trusted the pool address from calldata, so anyone could drain tokens parked on the adapter. | Callback honoured only for the pool the adapter is mid-call with. | Fixed |
| 13 | Low | DreamPadFactory.setLaunchesPaused | Owner-only. | Treasury (admin wallet) only, with a switch on the admin page. releaseTicker accepts owner or treasury. | Fixed |
| 14 | Low | DreamPadFactory._deployCurve | Curves launched through the zap recorded the zap as creator. | The real creator is passed through. | Fixed |
| 15 | Low | DreamToken.forceSwapBack | A creator who disabled automatic swaps could hold holder rewards hostage. | Anyone may trigger a swap-back once the pot reached the threshold. | Fixed |
| 16 | Low | RewardsDistributor.processBatch | A recipient the reward token refuses (USDC blocklist) stalled the batch forever. | Refused transfers are skipped. | Fixed |
| 17 | Low | stuck funds | Stray transfers to the curve, ETH sent to the token or distributor, and factory balance refunds. | Only the curve pulls tokens; ETH enters only via WETH unwraps; refunds are delta-based. | Fixed |
| 18 | Low | UniswapV3Adapter sqrt price | Overflow for a tiny low-decimal quote side. | Shorter shift fallback. | Fixed |
| # | Severity | Where | Finding | Fix | Status |
|---|---|---|---|---|---|
| W1 | High | metadata.ts, TokenLogo | Untyped on-chain metadata (an image that is a number, an object description) crashed the React tree for every visitor of the explore page. | Metadata is validated and capped field by field; only data:, ipfs:// and http(s):// sources load; remote metadata is bounded in size and time; an app-level error boundary contains any remaining render error. | Fixed |
| W2 | Medium | /api/upload | Unauthenticated proxy to the operator's Pinata account. | Same-origin only, 1 MB cap with early content-length check, magic-byte sniffing (no SVG), per-address rate limit, upstream timeout, pins tagged for cleanup. | Fixed |
| W3 | Medium | headers | No frame protection on an app that holds a hot key and fires one-click transactions. | frame-ancestors 'none', X-Frame-Options: DENY, object-src 'none', base-uri 'self', nosniff, referrer and permissions policies. | Fixed |
| W4 | Low | config.ts | Hidden, unconfigured networks were reachable by URL and persisted, pointing browsers at localhost RPCs. | Only shown networks resolve from URLs or saved preferences. | Fixed |
| W5 | Low | fast wallet reset | Reset only checked the balance on the current network. | Reset requires exporting the key first and typing RESET, and warns that the key spans every network. | Fixed |
| W6 | Low | launch form | No warning for community-registered pair assets. | Unverified pair assets are flagged with what can go wrong. | Fixed |
stricter pass
Method: property-based invariants (test/Invariants.t.sol: curve solvency, reward backing, supply conservation, creator never earns holder rewards, split fixed, 1,920 random calls per property), oracle TWAP and precision tests, two independent proof-of-concept attack passes with runnable Foundry exploits (test/Attack.t.sol, test/Attack2.t.sol, test/AttackV3.t.sol, now kept as regression tests), and a second manual read. Suite after round 2: 190 tests; after round 3: 241. Attempted and defended in this round (each has a passing test or a written argument in the attack files): curve reserve accounting under mixed buys and sells; distributor solvency under exclusion, re-inclusion and minimum-hold churn with a full claim-out; creator wallets holding large bags; protocol reward drains; tax or split mutation; pre-graduation pool seeding through a curve buy; arbitrary AMM-pair flagging; V3 pools pre-initialised far above or below the curve price (empty); a 24-decimal pair asset through the whole curve and graduation; registry spam (the list is never iterated on-chain); reward magnitude overflow; zap fund theft through callbacks or sweeps.
V3 graduation against deep pre-placed quote-only liquidity (test_H6c, test_H6d). With the pool pre-initialised at a token price 16× the curve's and deep quote liquidity in between, the adapter's alignment budget (half of the token side) cannot reach the target, so the pool opens above the curve price. The PoC shows the attacker loses about 13 ETH doing it: the adapter sells the coin's tokens to them at 16× and the leftover quote is paid to holders through the revenue split, curve buyers can sell into that liquidity at the inflated price, and the vault's full-range position is intact. Opening a pool mispriced at the attacker's expense is accepted; a looped alignment is a possible refinement.
Sandwich residue. With the average-price floor, a front-run inside the 0.3% (V2) / 1%-of-sqrt (V3) tolerance still costs the attacker two pool fees on a trade larger than the sale, which the PoC shows as a loss at every size on a 0.3% pool. On a pool with lower fees the residue would be bounded by the tolerance times the sale (0.3% of at most 0.5% of the reserve).
Creator-exempted MEV bot. A creator may exempt any address from tax (a CEX, a vesting contract), including a bot of their own. With the floor above, what such a bot can take from swap-backs is the same bounded residue; it cannot take the pot.
| # | Severity | Where | Finding | Fix | Status |
|---|---|---|---|---|---|
| 19 | Medium | LiquidityVault.harvest, DreamToken._swapBack, LiquidityVaultV3.harvest | The round-1 size cap (finding 11) bounded each sale but not the sandwich: with no output floor, a caller could dump, trigger the permissionless sale, and buy back. On a 0% tax coin the PoC netted about 5% of every harvest, repeatable. | Every token-side sale now runs at most once per ten-minute window and is floored at the pool's time-weighted average price over that window (V2: the pair's own cumulative price, exact x·y=k fair output, 0.3% tolerance; V3: swap limit 1% below the average sqrt price from the pool's observations, which the adapter switches on at graduation). A depressed price defers the sale (TokenSaleDeferred / SwapBackDeferred) instead of selling into it. PoC re-run: the best sandwich now loses money at every size. Tests: test_harvestCannotBeSandwichedForProfit, test_tokenSideWaitsWhenThePriceWasJustPushedDown, test_tokenSideIsSoldAtMostOncePerWindow, test_harvestHoldsTokenFeesWhenThePriceWasJustPushedDown, test_harvestSellsNoTokenFeesWithoutPriceHistory, test_H2_harvestSandwichNeverPays. | Fixed |
| 20 | Medium | DreamPadFactory.registerQuoteAsset | Round 1 (finding 9) rate-limited refreshes but the first sizing was still unbounded and, for assets priced from a V2 pair or a V4 pool, spot. A one-block dump of a shallow pool set a raise target 50× off, and refreshes could only walk it back 25% an hour. | Community registration is two-step: the first sizing leaves the asset pending; refreshQuoteAsset at least an hour later must size it within 25% of the first before launches open (QuoteAssetActivated); a disagreeing sizing replaces the first and restarts the hour. Owner-configured assets are unaffected. Tests: test_aPushedFirstPriceDoesNotActivate, test_pendingAssetCannotBeReregisteredEarlyOrLaunchedOn, test_H1_pushedFirstSizingNeverOpensLaunches. | Fixed |
| 21 | Medium | DreamZap.buyCurve, launchWithETH | The sell-out refund was measured as the zap's whole balance of the pair asset, so one unit of USDC (or any pair asset) sent to the zap became a "refund": every fee-only ETH-paid launch on that asset reverted (division by zero in the refund-rate check), and a larger donation was gifted to the next buyer. | Refunds are measured as the balance change within the call; nothing bought this call means nothing is a refund. Test: test_strayPairAssetOnTheZapCannotBrickBuys. | Fixed |
| 22 | Low | CurveMath.grossForNet, BondingCurve._buy | The gross-for-net rounding could exceed the true minimum by one unit, so a final buy whose amount already covered the last tokens computed a refund larger than the amount sent and reverted (underflow). One-wei window, but a wrong revert on the curve's most important buy. | Exact closed form for the smallest covering gross, plus a clamp in the curve. Fuzz test: testFuzz_grossForNetIsTheSmallestCoveringGross. | Fixed |
| 23 | Low | script/Deploy.s.sol | The mainnet script never checked that the factory landed on the address the adapter and vault were bound to; a nonce that drifted (a resumed broadcast) would have silently produced a wired-to-nothing deployment on a one-shot mainnet deploy. | The script now reverts unless the adapter's bound factory is the deployed factory. | Fixed |
final pre-mainnet pass
Method: three more independent adversarial passes with runnable proofs (the round-2 code: test/Attack3.t.sol; the rest of the protocol end to end: test/Attack4.t.sol; the web app), Slither over src/ (86 raw results, each read), the invariant suite at 18,000 random calls per property, fuzz tests for the floor maths (test/V2Twap.t.sol), and a manual re-read of everything round 2 changed. Attempted and defended in this round (each with a passing test in the attack files): distributor accounting under raising and lowering the minimum hold mid-life, refused and reverting recipients in batch and ETH claims, reentrancy through the ETH claim, pre-graduation tokens reaching any address other than the curve (EOAs, fake pools that claim token0() == this, a V4 PoolManager), the final curve buy when the remaining cost is below the minimum buy, exact-cost final buys leaving no dust, the zap's inner swap guarded by the outer minimum, a re-pointed zap against existing curves, ticker normalisation and reservation front-running, onGraduated spoofing, protocol-reward batch authorisation, the pause switch's scope, implementation initialisers and clone re-initialisation, TaxVault, FactoryBound and vault registration authorisation, creator transfers to contracts, V4 registration without a StateView, non-8-decimal and negative feeds.
V3 route selection by liquidity for the ETH / pair-asset leg (bestPool) can be won by a concentrated one-tick pool at another fee tier. Preview and execution use the same pool, and the single-range estimate overstates output across tick boundaries, so the app's minimum makes such a trade revert rather than lose money. Same class as the accepted "spot venues" note below; the coin's own leg no longer uses this lookup.
Ticker display spoofing. A symbol with a trailing control character is a distinct ticker key; it cannot take another coin's reservation but can look like it on screen. The app renders symbols as text and the explore page shows the address; a normalisation of control characters is a possible refinement.
An asset whose only market the attacker owns. The two-step activation makes a pushed price expensive to hold on a real market; on a pool with a single owner and no arbitrage a bogus price costs nothing to hold, so such an asset activates at whatever size its owner chooses (test_attackerControlledPoolActivatesAtChosenSize). This is the permissionless model: a creator chooses their pair asset, the launch form flags assets outside the stock lists as unverified, and the owner can disable one for good. A minimum external liquidity or holder count is a possible refinement.
Griefing the swap-back window. Someone can push the price more than the tolerance below the average right before each window closes, deferring the token-side sale again and again. It costs them two pool fees on a trade larger than the deferred sale every ten minutes and gains them nothing; holder rewards from the quote side keep flowing, and the token side sells as soon as they stop.
| # | Severity | Where | Finding | Fix | Status |
|---|---|---|---|---|---|
| 24 | Medium | DreamZap.buyPool, sellPool | After graduation the ETH / pair-asset leg was hard-wired to one venue (the V2 router, or V3 on V3 chains) while the previews used every venue. A coin paired with a V4-only asset (a Pons coin on Robinhood) or with a stock whose ETH market is V3 (every tokenized stock on Ethereum) showed a healthy quote and reverted on the trade once it graduated. No funds at risk; the advertised ETH in / ETH out was simply broken for those pairs after graduation. | Both legs use the same selection as the curve paths: ETH / pair asset through the best of V2, V3 and V4; pair asset / coin on the coin's own graduation pool (recorded by the token), never a lookup by liquidity that a one-tick pool at another fee tier could win. On Ethereum the zap is now also given the V3 factory. Tests: test_postGraduationBuyPoolWorksForV4PairedCoin, test_postGraduationSellPoolWorksForV4PairedCoin. | Fixed |
| 25 | Low | UniswapV3Oracle.priceUsd | The Chainlink branch checked the answer's sign but not its age; a feed that stopped updating kept pricing WETH. | Answers older than feedMaxStaleness (owner-set, one day) are not a price. Test: test_oraclePriceUsdRejectsAStaleFeed. | Fixed |
| 26 | Low | V2Twap.fairOut, DreamToken._swapBackTerms | The fair-output maths could overflow (revert) when the average price no longer fitted the pool's k, which needs a price collapse of more than 2^32 inside one window; the revert would have surfaced inside a user's sell. A token whose graduation could not record a window would never have started one. | An unreachable floor (the sale is deferred) instead of a revert; the tolerance maths through mulDiv; the first swap-back attempt starts the window. Fuzz tests: testFuzz_fairOutNeverReverts, testFuzz_fairOutAtSpotMatchesTheRouterFormula, testFuzz_fairOutRisesWithTheAveragePrice. | Fixed |
| 27 | Info | RewardsDistributor._tryClaim, DreamToken.transferCreator | State written after an external call (Slither). Not exploitable: every claim entry point is non-reentrant and the distributor is our own contract. | Checks-effects-interactions order; a refused batch transfer is unbooked. | Fixed |
| # | Severity | Where | Finding | Fix | Status |
|---|---|---|---|---|---|
| W7 | Medium | useProfile.ts | The profile page scanned every block since the first launch on the network, six filters at a time, for any address anyone typed in. On Robinhood's block times that is tens of thousands of log requests per visit, enough to exhaust the shared RPC for every visitor. | 90 days by default, older history on request. | Fixed |
| W8 | Medium | /api/upload | The per-address limiter keyed on the client-controlled x-forwarded-for header, a flood reset everyone's budget, a body without Content-Length was buffered before the size check, and the visitor's address was written into the pin's metadata at Pinata. | Platform-set client address only, budgets expire instead of resetting, Content-Length required, no address in pin metadata. | Fixed |
| W9 | Low | metadata.ts | Remote metadata JSON was fetched by every visitor's browser from any URL a creator put on-chain (a tracking beacon), and read whole before the size check; SVG data URIs were accepted as logos while the upload route refuses SVG. | Remote metadata only from IPFS through the gateway, no redirects, read no further than the cap; raster data URIs only, capped at 48 KB. | Fixed |
| W10 | Info | dependencies | Advisories in transitive packages of the wallet libraries, none reachable from the app's code paths. | Non-breaking updates applied; the remaining ones are in WalletConnect's server-side WebSocket and CDP packages that are aliased out of the bundle. | Fixed |
buyback-and-burn feature
Scope: the new BuybackVault (one per factory), V3Twap, and the changes that route a launch's buyback share to it (DreamToken: a fourth bucket in _splitRevenue, a pre-graduation rule letting the vault transfer to the dead address only, fixed exemptions; DreamPadFactory: buybackShareBps and burnCapBps in the launch parameters, setBuybackVault). Method: the feature's own suite (test/Buyback.t.sol, 10 tests including a sandwich sweep, plus the 12 proof tests of the pass kept as regressions), an independent adversarial pass with runnable proofs (test/Attack5.t.sol), and a manual read against the threat model of rounds 1 to 3. Design decisions that follow from those rounds:
The trigger is permissionless, so every buy is bounded like the fee sales. Once per ten-minute window; on the curve at most 0.5% of the virtual quote (a round trip pays the 2% curve fee twice, a 1% price move cannot pay for that); on a V2 pool at most 0.5% of the quote reserve, floored at the pair's time-weighted average price since the previous run with a 0.3% tolerance; on a V3 pool a swap limited to 1% above the pool's average sqrt price. A pushed-up price defers the buy. The feature's sandwich sweep shows a loss for the attacker at every size.
The share comes out of the creator's part, never the holders'. The launch form shows all four buckets; the contract checks that holders + creator + buyback = 100% of the non-platform part.
Nothing but bought coins leave the vault, and only to the dead address or the creator. withdraw moves only held (coins bought after the burn cap), only to the creator, and only after graduation because the token's pre-graduation rule allows the vault a single destination.
Per-coin ledgers. The vault holds many pair assets for many coins in one balance; pending is credited only through deposit, which pulls the exact amount from a launched coin, and a run spends only that coin's pending. Curve fees that flow straight back during a run (_depositedDuringRun) are separated from a sell-out refund.
Exemptions are fixed. The creator cannot tax, cap or re-include the vault; a wallet that buys on the curve can name the vault as recipient, which only strands (effectively burns) its own coins. Attempted and defended (tests in test/Attack5.t.sol): curve sandwiches at every size (the 2% curve fee twice beats the move), deferral griefing (two pool fees per window for nothing), per-coin ledgers across coins and through a sell-out refund with fees flowing back mid-run, burn and withdrawal bounds, replacing the vault (existing coins keep theirs, a hostile vault cannot deposit), share rounding, configure and deposit authorisation, callback pinning.
| # | Severity | Where | Finding | Fix | Status |
|---|---|---|---|---|---|
| 28 | Medium | V3Twap, BuybackVault._buyOnV3, LiquidityVaultV3 | A V3 pool can be created and initialised before graduation by anyone, at any price, with an observation aged as long as they like. That observation was the pool's "oldest" and so the whole price history the first buyback after graduation trusted; with no size cap on the V3 path the run spent all pending in one swap far past the price limit (PoC: all 0.37 ETH pending spent, a 451 bps sqrt move against a 100 bps limit, a sandwich taking 43% of the spend). The V3 harvest shared the library. | Price history starts at the coin's graduation (notBefore): observations older than the coin's own market are ignored, and the first five minutes after graduation are never enough history. V3 buys are capped at 0.2% of the pool's quote balance; the V3 limits are 0.5% on sqrt price (about 1% on price, under the 2% a round trip through a 1% pool costs). Test: test_F2_preInitialisedV3PoolDoesNotSeedTheTwapOrUncapTheFirstRun. | Fixed |
| 29 | Low | BuybackVault._v2Terms, LiquidityVault._saleTerms, DreamToken._swapBackTerms | The round-2 reasoning had the inequality backwards: a trade of 0.5% of a reserve moves the price about 1%, more than a V2 round trip costs, so pushing the price, holding it for one window (the average then equals the pushed price) and trading back paid on 0% tax coins: about 45% of a buyback's spend at the largest pushes. The cap was also computed on the reserve *after* the push, which the push itself inflates. | Every V2 sale and buy is sized at 0.2% of the reserve as it was when the window started (the smaller of then and now), a move of about 0.4%, under the round-trip cost; the PoC sweep now loses at every push from 1% to 100% of the reserve. Tests: test_F3_v2HeldWindowSandwichNeverPays, the harvest and swap-back suites. | Fixed |
| 30 | Low | DreamToken.setExcludedFromMaxWallet | The tax and reward exemptions of the plumbing were protected, the max-wallet ones were not: a creator could revoke the dead address's (or a vault's) exemption and make every burn, and every harvest, revert until the max-wallet period ended. | The token, curve, vaults, adapter, zap and dead address are protected. Test: test_F1_creatorCannotRevokeTheDeadAddressOrVaultExemptions. | Fixed |
| 31 | Info | BuybackVault.withdraw, stray coins | A creator withdrawing held coins to the dead address was not booked as a burn; coins parked in the vault by a curve buy naming it as recipient were stranded outside the ledger. | Withdrawals to the dead address count as burned; a permissionless burnStray burns and books coins that reached the vault outside a buyback. | Fixed |
full-codebase pass before mainnet
Scope: every contract, the web app, the API routes, the deploy scripts, the runbooks, the git history and the live Vercel project. Method: three independent adversarial passes with runnable proofs (test/Attack6.t.sol: token, curve, factory, zap, 22 tests; test/Attack7.t.sol: vaults, buyback, rewards, oracles, 7 tests; a web and operations pass that ran the app with hostile metadata under Playwright), Slither, npm audit, and a secret sweep of every commit, dangling object and untracked file. Every proof that found something is kept as a regression that now asserts the fix.
Key material. No private key, mnemonic, RPC key, Pinata JWT or Vercel token has ever been committed on any branch or dangling commit. The only 64-hex strings in the history are Anvil's two public developer keys in script/DeployLocal.s.sol. cache/, broadcast/, .env* and now every *.log are ignored; server secrets are read only inside the two API route handlers; nothing in a client module touches them. The treasury is an address the factory reports, held by a Safe on mainnet per the runbook; the deployer keeps no role once the Safe accepts ownership. Attempted and defended (tests in test/Attack6.t.sol and test/Attack7.t.sol): ETH-refund re-entrancy on the final buy and on unwrapped sells, pre-funded pair griefing, the max wallet against graduation, the dead address and the adapter, the final-buy refund maths and a CurveMath fuzz, sellFor and zap allowance abuse, swap-back floor DoS, creator cap tightening and role transfers, treasury and owner key powers (a table of every power is in the pass report), zap cross-venue routing leaving nothing behind, the vault and zap as max-wallet bypasses, buyback ledger inflation and fake coins, a buyback run that graduates the curve, spoofing or reverting onGraduated, the V3 held-window sandwich (a loss at every size), V3 run bounds, burnStray against a creator's held coins, and, on the web, stored XSS through every metadata field, SVG script execution, prototype pollution, SSRF through metadata, fast-key exfiltration (661 requests, none carried it), chain and contract confusion through crafted URLs, clickjacking, and docs search reflection.
| # | Severity | Where | Finding | Fix | Status |
|---|---|---|---|---|---|
| 32 | High | BuybackVault.burnStray | Took any address. Passing a pair asset (WETH, USDC) burned the vault's entire balance of it, every coin's pending buyback share, and every later run reverted. Permissionless, free, repeatable per deposit. | Only a coin the factory configured can be named (registered). Test: test_F1_burnStrayBurnsEveryCoinsPendingBuybackQuote. | Fixed |
| 33 | High | DreamPadFactory._launch | The launch parameter minHoldForRewards bypassed the 0.1% cap that the creator's setter has (finding 7). A minimum nobody could meet parked every holder reward; the creator then lowered it and a wallet of their own took the whole carry with two dust buys (0.089 ETH of other buyers' fees in the proof). | The cap applies at launch; the form refuses it too. Test: test_F2_launchTimeMinHoldLetsTheCreatorSweepEveryHolderReward. | Fixed |
| 34 | Medium | BuybackVault._observe | The V2 buy cap and TWAP floor were anchored to the reserve and price at the first run, chosen by whoever called it, and a deferred run re-anchored again. A push above 100% of the reserve, held for one window, paid (+0.0102 ETH on a 0.047 ETH spend). | The coin seeds the vault's first window at graduation (onGraduated, best effort from DreamToken.graduate) and a deferred run never moves the anchor. Tests: test_v2FirstRunSnapshotInflationStealsPending, test_v2FirstRunSnapshotIsAttackerControlled, test_v2WindowSeededAtGraduationAndKeptThroughDeferrals. | Fixed |
| 35 | Medium | DreamPadFactory.registerQuoteAsset / refreshQuoteAsset, UniswapV3Oracle | The two-step community activation sampled two instants an hour apart; for an asset priced from a V2 pair (spot) an attacker pushed the pair at both instants and unwound in the same block. A real asset activated 16x too small ($1,884 raises instead of $30,000) for 0.09 ETH, with nothing held. | The oracle reports the V2 pair it prices an asset from (v2Source); the factory records that pair's cumulative price at every sizing and sizes from the average since (at least an hour). A pending asset activates only when the instant agrees with the average and the average agrees with the previous sizing; an active asset is re-sized from the average, bounded to 25% a step. A push now has to be held for the whole hour against arbitrage. Test: test_F3_twoFlashPushesActivateARealAssetAtABogusSize. | Fixed |
| 36 | Low | DreamToken.setExcludedFromRewards | The zap, adapter, distributor and tax vault were not protected. A creator re-included the zap, which holds a seller's coins for the length of sellPool; a swap-back inside that call paid it holder rewards, and the next zap caller swept them (0.0126 ETH from one sell). | They are protected, as are AMM pairs. Test: test_F4_creatorCanReincludeTheZapAndLeakHolderRewardsThroughIt. | Fixed |
| 37 | Low | DreamToken.setExcludedFromTax | The zap was not protected: one creator call removed the sell tax for every sell through the app's ETH-out route while the page still showed it. | The zap, adapter, pair and AMM pairs are protected. Test: test_N18_creatorCanExemptTheZapAndRemoveTheSellTaxForEveryone. | Fixed |
| 38 | Info | DreamPadFactory._launch | A max wallet below what the minimum buy purchases at the opening price locked everyone but the creator out of the curve. | The factory refuses the combination (InvalidMaxWallet). Test: test_N14_capBelowMinBuyLocksOutsidersOutButNeverLocksTheCurve. | Fixed |
| 39 | Medium | Web: share-card renderer | The server rasterised creator SVG logos (the client refuses them); a 13 KB stacked-filter SVG took 18.7 s of CPU per request, cache-bustable, on a public route. | Raster logos only, rendered at most once an hour per coin. | Fixed |
| 40 | Medium | Web: fast-trading key | The key sat in plain text in localStorage and in the shared React context, with no script CSP: one XSS, one bad extension, and every fast wallet on the site was drained. No XSS was found; this is defence in depth. | A per-request nonce Content Security Policy from middleware (scripts only from the app's own bundles; connections only to the configured RPCs, wallet relays, IPFS and analytics), the key removed from the context (the export action reads it explicitly), and an inactivity lock after an hour. | Fixed |
| 41 | Medium | script/Deploy.s.sol, UniswapV3Oracle | OWNER defaulted to the deployer and nothing checked that the admin roles were contracts; the oracle's ownership transfer was single-step, so a mistyped OWNER lost its admin. | On a live chain the script refuses an OWNER or TREASURY that is not a contract, or a deployer that is the treasury (ALLOW_EOA_ADMIN=true for rehearsals); the oracle is Ownable2Step; PRIVATE_KEY is optional so a hardware wallet can broadcast. | Fixed |
| 42 | Low | Web: zap trust | The app routed every ETH trade through whatever factory.zap() said at that moment and granted it max approvals, so a lost owner key could re-point users' trades with setZap. | The zap is pinned per network (NEXT_PUBLIC_ZAP_ADDRESS_*); the app refuses to trade through any other and says so. | Fixed |
| 43 | Low | Web: upload route | The same-origin check passed when Origin was absent; the rate-limit key collapsed to one bucket off Vercel. | A request with neither Origin nor Sec-Fetch-Site is refused; no trusted client address means no upload. | Fixed |
| 44 | Low | Web: link normalisation | https://good.com@evil.example/ rendered as a "good.com" link; IP literals and any host passed for social links. | No userinfo hosts, no IP literals, X and Telegram links only on their own domains. | Fixed |
| 45 | Low | Ops: log hygiene | Runbook-generated deploy logs were not ignored; cache/ keeps the full RPC URL (with its key) of a deploy. | *.log ignored; the runbook writes logs outside the checkout and says to delete cache/ after a deploy. | Fixed |
| 46 | Low | Web: dependencies | Two high and 23 moderate transitive advisories (ws, postcss, decode-uri-component). | Overrides; npm audit --omit=dev reports none. | Fixed |
Accepted risks and design notes
- Unlimited approvals in the trade panels are deliberate (one approval per token, as requested). Spenders are the curve, the zap and the Uniswap router read from the factory, never from the URL.
- Spot venues in the zap are still Uniswap pools; a deep one-tick position can win a route and make a trade fail its slippage check (a revert, never a loss). Users see the same estimate and execution.
- Ticker reservation can be front-run by a bundler who pays the launch fee first; the treasury can release such a ticker. A commit-reveal scheme was judged not worth the launch UX cost.
- First eligible holder receives the carried rewards accumulated while nobody was eligible (a creator's excluded initial buy). Small, and it favours the first outside buyer.
- Pair assets are third-party tokens. A pausable or blocklisting token can freeze a coin's rewards and trading. Community registrations go through
PairAssetGuard, which refuses a transfer fee (a real probe transfer), a standardpaused()getter and the usual blocklist getters, and launches on a paused asset are refused; a token that hides such controls behind non-standard names still passes, so the app keeps warning for community-registered assets. Owner-listed assets (the stablecoin, the issuers' stocks) have these controls by regulation and skip the guard. - Uniswap V2 protocol fee (
feeTo, currently off on mainnet) would dilute the V2 vault's harvest measure by one sixth if ever enabled; the vault would then need thekLastformula. - Oracle falls back to spot where a pool keeps no history, and V2 pairs are always spot. It sizes raises and the $2 launch fee only; nothing settles against it.
- Fast trading keys are stored unencrypted in the browser by design of the feature; the app says so and offers export. Since round 5 the page runs under a nonce Content Security Policy, the key is not exposed through the app's React context, and fast mode locks after an hour without a trade.
- Community pair assets priced only from a Uniswap V4 pool are still sized from spot readings (V4 pools keep no price history the oracle can average). The owner can disable such an asset; assets with a V2 pair or a V3 pool are sized from a time-weighted average.
- The buyback vault's graduation hook is best effort. If it ever failed, that coin's first V2 window would again open at the first run, bounded by the size caps (round 5, N16).