Audit package - D17 V15.1
Everything a reviewer needs to audit the D17 V15.1 contracts: what is in scope, how to build and test it, how the system is meant to work, what is deliberate, what is already known, and where we would most like a second pair of eyes.
Status: V15.1 was reviewed by the project lead's Solidity reviewer (no blocking issue)
and deployed on Ethereum mainnet on 2026-11-12 (factory 0xB46eb48f982b63B879E00592880D12d1741fDB02, 2% fee), with the source code of its 8 contracts
verified on Etherscan (addresses in release/deployments/mainnet.json). A formal
professional audit is still welcome; findings go into V16.
1. Scope
The production source is contracts/contracts/ (12 contracts, 2 interface files and one
library). Its exact bytes are fixed by contracts/SHA256SUMS.txt; please quote the commit
you reviewed in the report.
| File | nSLOC | Role |
|---|---|---|
D17Launch.sol |
708 | The sale engine: rounds, refunds, finalization, settlement accounting, pool funding |
D17Factory.sol |
299 | Registry and validation of every launch configuration; mainnet guards |
D17Locker.sol |
284 | Per-participant escrow: WETH in, refunds, settlement, withdrawals |
D17Token.sol |
250 | The launch token (ERC-20), transfer gate until trading opens |
D17LiquidityVault.sol |
198 | Creates the official Uniswap V2 pair, holds the LP forever, creator vesting |
D17LaunchFactory.sol |
155 | Deploys token, launch and vault for one launch |
interfaces/ID17.sol |
113 | Shared interfaces |
D17FeeSplitter.sol |
68 | Ownerless fee recipient with fixed shares |
D17FeeConfig.sol |
50 | Protocol fee rate and recipient for future launches (renounced at deployment) |
D17LockerFactory.sol |
46 | Creates lockers; batch settlement of absent lockers |
D17LaunchDeployer.sol |
43 | Deploys D17Launch (keeps D17LaunchFactory under the size limit) |
D17TokenFactory.sol |
41 | Deploys D17Token |
D17LiquidityVaultFactory.sol |
41 | Deploys D17LiquidityVault |
interfaces/ID17LaunchFactory.sol |
40 | Launch factory interface |
lib/D17SafeTransfer.sol |
23 | Safe ERC-20 transfers |
| Total | about 2,360 |
Out of scope: the test contracts in contracts/contracts/test/, the deployment and
tooling scripts in contracts/scripts/, the web app (apps/web) and the API (apps/api).
External dependencies are assumed honest: canonical WETH9 and the Uniswap V2 factory,
router and pair.
Compiler: solc 0.8.24, viaIR: true, optimizer enabled with runs: 1, EVM version
shanghai, bytecodeHash: "none" (contracts/hardhat.config.js). The exact standard
JSON input used for the release is release/solc-input.json.
2. Build and test
Requirements: Node 22.13+ or 24+, npm. From the repository root:
npm ci
npm run compile -w @d17/contracts
npm run test:contracts # every contract suite below, a few minutes
npm run test:fuzz -w @d17/contracts # stateful fuzz, about 15 minutes per 8 campaigns
Suite (contracts/test/) |
What it covers |
|---|---|
local-e2e.mjs |
Full lifecycle: wiring, configuration bounds, five rounds, refunds, failed launches, finalization, settlement (owner, third party), pool creation, token conservation, adversarial reverts |
hardening-e2e.mjs |
The V15 hardening rules: anchor griefing, the final-round floor, the effective LP allocation, the complete-funding gate |
mainnet-floors-e2e.mjs |
Chain id 1 rules: 24 h notice, 1 h windows, frozen fee config required |
fee-splitter-e2e.mjs |
Splitter constructor rules and exact distribution |
v15-1-regressions-e2e.mjs |
The three V15.1 fixes, the all-settled gate, batch settlement, its gas margin and the per-transaction gas cap |
failed-refund-remediation.mjs |
Cancelled launches: principal and take-back penalties returned exactly once |
adversarial-callbacks.mjs |
Hostile WETH callbacks and reentrancy around deposits and refunds |
minimum-liquidity.mjs |
Smallest pools against the real Uniswap V2 pair |
rounding-reference-model.mjs |
Mathematical reference model of the per-locker rounding |
fuzz-invariants.mjs |
Seeded stateful fuzzing with invariant checks and adversarial probes |
Several suites run against the real Uniswap V2 bytecode (@uniswap/v2-core). In a
sandbox without access to binaries.soliditylang.org, see "Cloud sandbox" in
docs/MAINTAINERS.md for installing the compiler by hand.
3. How the system works
The complete reference, with a citation for every rule, is CONTRACTS_TECHNICAL.md; the plain-language version is CONTRACTS.md; the changes from the upstream V14 code are in V15_HARDENING.md. In short:
- Creation. A creator calls
D17Factory.createLaunch(config). The factory validates the configuration, snapshots the protocol fee, and has the launch factory deploy the token, the launch and the liquidity vault in one transaction. The whole supply is minted once; the rules are fixed in immutables and arulesHash. - Sale. Five rounds. Participants commit ETH through their own
D17Locker, which wraps it to WETH and keeps it. Round 1 sets the anchor price and must meet a minimum raise and a minimum price, otherwise the launch is cancelled. No later round sells below the anchor; unsold tokens are burned. - Take-backs. After rounds 1 to 4 a window lets participants take their commitment back for a fee (1% after rounds 1-2, the launch's 1-25% after rounds 3-4), paid into the vault for the pool. If the launch is cancelled, everything is returned, fees included.
- Finalization and settlement. Anyone finalizes after round 5. Each locker settles:
sale tokens to the locker, the pool share of its WETH to the vault, the creator's
treasury share and the protocol fee to their recipients. After the claim window anyone
can settle an absent locker (
settleAfterGrace, orsettleAfterGraceBatchfor up to 4). - Pool. Once every final commitment is settled, anyone creates the official Uniswap V2 pair from the vault: all settled WETH plus all take-back fees against the effective LP allocation. The LP tokens stay in the vault forever. Token transfers open with the pool. The creator's token allocation vests over 180 days.
Roles and trust.
| Role | Powers |
|---|---|
| Creator | Chooses the configuration within the factory's bounds; receives the treasury share (at most 10%) and the vested allocation (at most 10% of supply). No function after creation. |
| Participant (locker owner) | Commits, takes back, settles, withdraws; only for their own locker. |
| Anyone | Finalizes, settles absent lockers after the claim window, creates the pool, releases vested creator tokens, burns residual dust, distributes the fee splitter. Gains nothing from it. |
| Fee config owner | Renounced as the last deployment transaction; on chain id 1 the factory refuses to create launches until it is. |
| Deployment keys | Own the factories only until they are pinned and renounced; the factory refuses every launch while any owner key exists. |
4. Intended behaviour (please do not report as a bug unless it can be exploited)
- Permanently locked liquidity. There is no way to withdraw the vault's LP tokens. Once holders have sold, a large share of the pool's WETH stays in the pair forever (about half of the pool's WETH if the pool holds as many tokens as the sale sold). This is the main topic of the next version (V16); ideas are welcome.
- The creator chooses the LP allocation, from 1 token up. A very small one opens the pool far above the average price paid. The web app flags such launches as predatory; a contract rule is planned for V16.
- Round 5 is anchored like rounds 2-4: it never sells below the round-1 price, and what it cannot sell is burned.
- The pool waits for every buyer. Pool creation reverts while a final commitment is unsettled; whoever opens trading pays the gas to settle absent buyers.
- Fees. Protocol fee at most 2% (2% on mainnet, 1% on the Sepolia test suite), charged only on settlement of a successful commitment, frozen per launch. Each fee is rounded down per locker; the pool receives the remainder.
- Late-settlement entry points (
D17Launch.claimLateSettlement,D17LiquidityVault.mintLateLiquidity) andburnFailedLaunchPenaltiesremain in the ABI for compatibility and always revert. - Contract identities end in
V15_1(D17_LAUNCH_V15_1, ...), distinct from V15'sV15_HARDENED. Each suite only accepts lockers and launches it registered itself.
5. Known limitations
- Block timestamps bound every window; mainnet requires every window to last at least 1 h.
- A creator can take part through other wallets on the same terms as everyone else.
- Donations: D17 tokens sent to the vault after the pool opens, or WETH sent to the vault of a cancelled launch, are stuck. The creator allocation of a cancelled launch is locked forever.
createLaunchdeploys three contracts in one transaction. With the largest metadata the contract accepts (an 8 KB logo) it needs more than the 16,777,216 gas cap per transaction (EIP-7825); the web app limits the logo to 4 KB.D17LaunchDeployeris 22,910 bytes of the 24,576-byte limit: any growth ofD17Launchconsumes that margin.- Static analysis: Slither 0.11.6 reports only benign findings (listed in
CONTRACTS_TECHNICAL.mdsection 11). - No professional audit before this one.
6. Already found and fixed
| When | Issue | Fix |
|---|---|---|
| V15 | Factory at the contract size limit | D17LaunchDeployer |
| V15 | Free last-second refunds could grief the anchor | 1% fee on every round 1-2 take-back |
| V15 | Final round sold off at any price | Anchor floor on round 5, unsold burned |
| V15 | Last late settler blocked by rounding (LP_RESERVE_EXCEEDED) |
Cap on late LP releases (late settlement removed in V15.1) |
| V15.1 | Pool creation reverted forever when every locker settled first: per-locker fee rounding put the settled WETH above the aggregate formula, so the pool asked for more LP tokens than reserved; anyone could trigger it with odd wei amounts | Exact per-locker ledger exactCommittedLiquidityWeth |
| V15.1 | Supplies above Uniswap V2's uint112 reserve accepted |
MAX_TOKEN_SUPPLY, SUPPLY_ABOVE_V2_RESERVE |
| V15.1 | Late settlement added liquidity at a moved price and burned the unmatched part | Pool only once every commitment is settled |
| V15.1 | Take-back fees of a cancelled launch were burned | Returned to their payers |
| V15.1 (Sepolia test) | Batch settlement forwarded 1M gas per locker; a settlement costs about 1M on Sepolia, so the batch skipped every buyer | 3M per locker, at most 4 per batch (12.2M, under the EIP-7825 cap) |
7. Where we would most like your attention
- Accounting exactness.
exactCommittedLiquidityWethagainst every path that changes a position (commit, round refund, failed refund, settlement), and the equality checked at pool creation (LIQUIDITY_ACCOUNTING). Any rounding path that could make the pool creation revert again, or leave WETH or tokens behind. - Liveness. Any way to make a successful launch impossible to open (a position that can never settle, a locker that blocks the batch, gas griefing with many small positions), or a cancelled launch impossible to refund.
- Failed-launch refunds. Principal and penalties returned exactly once, including partial withdrawals before the failure; no way to claim another locker's penalty.
- Reentrancy and ordering across the locker, the launch, the vault, WETH and the token (the token transfer gate and the vault-to-pair exception).
- Pool creation. Pre-seeding or donating to the pair before it is created, the
vault's checks on reserves, and the
minLpMintedand deadline arguments. - Deployment and identity. The pin and renounce flow, the mainnet guards in
D17Factory, and the identity checks between lockers, launches and factories. - Fee splitter. Rounding, any token or ETH left behind, and behaviour with non-standard tokens sent to it.
8. Public test deployment
The exact source of this package (with a 1% fee, a 75 / 25 fee split,
fee config renounced; mainnet will use 2%, the only difference) is deployed on Sepolia, start block 12112991, with every contract
verified on Etherscan. Addresses and transactions: release/deployments/sepolia.json.
| Contract | Address |
|---|---|
| D17Factory | 0x260cbEA5Af1669446353d2B80D75a145016C1228 |
| D17FeeConfig | 0xcB917eaAd0DaDB3E483212e4a9452dD1b73d270c |
| D17FeeSplitter | 0x6E6F46d2DdA1E0c70A467148bB43B7b76e59FeF7 |
| D17LaunchFactory | 0xd1730399468d40ef9Dff8f91653D4f83E889Ef4E |
| D17LaunchDeployer | 0xB4bcE65b6C5ecFd0ADeB7DaE32a2B9036B96d556 |
| D17TokenFactory | 0x778e49CB1e24fE4ae31111Ba4283cd5061B57b6f |
| D17LiquidityVaultFactory | 0x9C663fCE0049423b9c53f55993E41b7b41D2CcE1 |
| D17LockerFactory | 0x613F3B7406Eb59385502b123026229B57c2E9D75 |
This suite (2026-11-11) differs from the previous Sepolia suite only by its identity
constants (*_V15_1). The test launches ran on that previous suite: a successful launch
with one buyer left absent and settled by a third party through the batch (the pool then
opened and the 1% fee was split 75 / 25 to the wei), two launches cancelled by their
round-1 floors, and, one suite earlier, a cancelled launch that returned a take-back fee
in full. A cancelled launch
with a take-back fee returned in full ran on the previous V15.1 suite.
9. Reviews of later versions
Every contract change makes a new version with new identity constants, and each version
gets its own review before mainnet: V16 (planned changes to the pool liquidity, see
docs/V16_POOL_LIQUIDITY.md) will need one too. A review of a later version can focus on
the diff from the last reviewed commit, plus the invariants it touches.
10. Reporting
Please share findings privately with the maintainers before any publication (see
SECURITY.md), with the affected contract, the invariant, the call sequence and a minimal
reproduction where possible.