Repository navigation
CHIP-0062: The Forge — Weighted N-Asset AMM - #217
awizardxch wants to merge 21 commits into
Conversation
Proposes a weighted constant-function market maker in which one singleton holds any number of assets, each in its own reserve coin, at weights fixed at pool creation. Built on CHIP-0050's action layer; authorizations are CHIP-0025 messages whose receiver coin id the pool derives rather than accepts. Informational / Puzzle. Requires CHIP-0025 and CHIP-0050. Supporting material — the full specification, architecture, a written CLVM pass over every leaf, security notes, and a comparison against publicly documented AMM failures — is referenced from the public forge-puzzles repository, alongside the implementation it describes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
This CHIP is now a |
|
@awizardxch Thanks for creating this CHIP. I have updated the file name and added myself as a reviewer. Otherwise, I haven't made any changes. You may want to update the formatting to remove the mid-paragraph new line characters, as well as the non-standard U+2014 characters. |
greimela
left a comment
There was a problem hiding this comment.
Daybreak Blue security review
These findings come from a ChatGPT Daybreak Blue review of PR head 29203bd and Forge implementation f8fbcaa.
| Severity | Finding |
|---|---|
| P0 | Genesis can authorize more LP tokens than pool state records |
| P1 | Successive singleton generations can backfill oracle time |
| P1 | The final LP position cannot redeem |
| P1 | The protocol-fee units disagree |
| P1 | Reserve receiver derivation trusts solution data |
The first two issues were reproduced with spend bundles accepted by consensus validation. The terminal-LP and reserve-substitution cases were tested against the compiled puzzles.
| | A substituted coin satisfying an authorization | Derived receiver ids; CHIP-0025 mode binds sender puzzle and receiver coin at consensus | | ||
| | Value extracted by chaining actions in one spend | The invariant holds per action; suites value the actor's closing position at pre-sequence prices | | ||
| | A negative or reversed trade amount, reversing the fee | Sign assertions in the leaf, and an invariant unsatisfiable for such inputs | | ||
| | LP minted or melted without the pool's consent | The TAIL requires a message from the pool and a CAT parent; the melt inner's only behaviour is to destroy its whole amount | |
There was a problem hiding this comment.
P0: Bind genesis issuance to a single LP eve
Impact: A pool creator or keyless router can mint LP tokens that the pool state does not record, wait for users to deposit, then redeem the hidden supply for reserve assets.
Attack path
- The TAIL genesis branch accepts the singleton launcher coin announcement instead of a pool message.
- That announcement does not commit to the LP eve coin making the assertion.
- Multiple independently funded eves can reuse the announcement in one bundle.
Reproduction: A complete registered-pool bundle passed consensus validation. Pool state recorded 5,000,000 LP, while two eves minted 10,000,000 LP in total.
Required change: Bind genesis authorization to exactly one derived LP eve coin ID. Add a regression test that rejects duplicate eves.
There was a problem hiding this comment.
Addressed in revision 2. The registry's register leaf now announces
sha256(launcher_id + tree_hash([pool_puzzle_hash, 1, [total_lp, eve_coin_id]]))
so the announcement names the eve coin. The TAIL's genesis branch asserts that announcement using its own coin id, taken from the CAT truths rather than from its solution, so a second eve would have to assert an announcement that was never made.
contracts/v12/puzzles/forge_registry_register.rue, contracts/v12/puzzles/forge_lp_cat_tail.rue.
Regression coverage is in two places. _test_v12_genesis.py covers the duplicate eve directly. _test_v11_v12_review_findings.py builds your attack against the previous revision, expects it to be ACCEPTED, then builds it against this one and expects it to be REFUSED -- a finding that cannot be demonstrated against the old build fails the suite as loudly as one this build still allows, so the test cannot quietly stop testing anything.
| | LP minted or melted without the pool's consent | The TAIL requires a message from the pool and a CAT parent; the melt inner's only behaviour is to destroy its whole amount | | ||
| | An LP token used against a pool of a different revision | The TAIL asserts the protocol version | | ||
| | Two pools claiming the same market | The sorted registry | | ||
| | Oracle manipulation within one spend | The accumulator advances on pre-spend prices, with the height pinned across every action in a bundle | |
There was a problem hiding this comment.
P1: Prevent cross-generation oracle backfilling
Impact: An attacker can make the oracle report elapsed blocks at a manipulated price even though no block elapsed. A downstream contract that releases assets based on this TWAP can be drained.
Attack path
- Spend the pool at declared height
H - 31and perform a swap. - In the same bundle, spend the ephemeral successor at declared height
Hand observe the new price. - The same-height guard only covers leaves within one action-layer spend, so the cross-generation pair passes.
Reproduction: The compiled bundle passed consensus validation and recorded 31 blocks at the manipulated price at one actual height. Spot moved from 1, while the claimed 32-block TWAP became 3.9004.
Required change: Bind elapsed time to actual coin birth heights across singleton generations. If the puzzle cannot enforce that relationship, remove the oracle from this proposal.
There was a problem hiding this comment.
Addressed in revision 2, by binding to the coin's birth height rather than removing the oracle. The prologue now takes birth, emits ASSERT_MY_BIRTH_HEIGHT, asserts h >= birth, and accumulates over h - birth instead of over the difference between two solver-chosen declared heights. An ephemeral successor spent in the same bundle has birth equal to that block, so it accumulates nothing, which is what closes your two-generation pair.
contracts/v12/puzzles/forge_action_common.rue::prologue.
Because the claim is about what consensus will accept, the test is too: _test_v12_consensus_timelocks.py runs chia.consensus.check_time_locks -- the function the mempool itself calls -- over the bundles, so a false birth height is judged the way a node judges it rather than the way our own code does.
| |---|---| | ||
| | `swap` | One reserve rises, one falls. Requires a positive input and a positive claimed output, distinct in-range asset indices, and an exact invariant match. The protocol and DAO slices are taken from the output and accrued in state. | | ||
| | `add` | Deposits to one or more reserves, minting LP. Requires a positive LP delta, non-negative deposits, and at least one positive deposit. | | ||
| | `remove` | Burns LP and pays a proportional share of every reserve. Requires the burn to be positive and strictly less than total LP, so the pool outlives every withdrawal. | |
There was a problem hiding this comment.
P1: Provide a redeemable terminal state
Impact: The final LP holder cannot withdraw their share of the reserves. Another holder can redeem all but the victim's final LP unit and leave that share permanently trapped.
Cause: The rule burn < total_lp forbids burning the remaining LP supply, while the registry accepts any positive supply without requiring a minimum relationship to the reserves.
Reproduction: A compiled pool registered successfully with reserves of 10,000,000 and 20,000,000 and total_lp = 1. Its only LP unit could not be burned.
Required change: Add a terminal close path. Alternatively, define a bounded minimum-liquidity amount, lock it deliberately at genesis, and enforce that invariant during registration.
There was a problem hiding this comment.
Addressed in revision 2 by the second option you named. MIN_LOCKED_LP is 1,000, burned at genesis; remove asserts burn <= total_lp - MIN_LOCKED_LP; and registration refuses a genesis supply below the floor, so the invariant cannot be skipped by minting a pool under it.
contracts/v12/puzzles/forge_action_common.rue, forge_action_remove.rue, forge_registry_common.rue.
The tests pin both edges: burning everything above the floor is accepted, one unit more is refused, and a pool registering below the floor is refused. The last holder can now take out their whole position, because the locked thousandth was never theirs -- it was burned before anyone deposited.
|
|
||
| **Configuration** is curried and therefore immutable for the life of a pool: | ||
| asset ids in canonical order, the weight of each, the trade fee in basis points, | ||
| the protocol fee in parts per million, the LP TAIL hash, and the DAO recipient's |
There was a problem hiding this comment.
P1: Use one protocol-fee unit
Impact: A wallet following the CHIP can authorize a fee 100 times larger than intended, diverting the difference to the protocol recipient.
| Source | Stated unit | Denominator |
|---|---|---|
| This CHIP | Parts per million | 1,000,000 |
| Referenced Forge specification and code | Basis points | 10,000 |
For example, a value of 5 means 5 ppm under this text but 5 bps in the puzzle.
Required change: Choose one unit, denominator, and maximum. State them normatively and use the same names in the implementation.
There was a problem hiding this comment.
Addressed. Basis points throughout. The CHIP now states it normatively in Specification: the protocol fee is protocol_fee_bps, denominator 10,000, at most 100, and the reference implementation uses the same name and the same denominator. Parts per million appears nowhere in the document.
The unit was only ever wrong in the prose; thank you for catching that it would have led a wallet to authorize a hundredfold fee.
| ### Authorization | ||
|
|
||
| Every reserve release and every LP mint or melt is a CHIP-0025 `SendMessage` with | ||
| mode `SENDER_PUZZLE | RECEIVER_COIN`. The receiver's coin id MUST be derived by |
There was a problem hiding this comment.
P1: Commit the canonical reserve parent
Impact: An attacker can replace the expected reserve successor and strand the canonical reserve, breaking the proposal's anti-substitution guarantee. This is not a free drain by itself because the attacker must provide replacement value.
Cause: The reference finalizer accepts reserve_parent_ids from its solution and uses those values in coinid(...). The pool does not independently commit to or authenticate them.
Reproduction: An attacker-created reserve with the same puzzle and amount satisfied the message and became the successor while the canonical reserve remained unspent.
Required change: Commit the reserve parent or full coin ID in pool state, or prove its lineage before deriving the receiver.
There was a problem hiding this comment.
Addressed in revision 2 by the first option you named. Reserve parent ids are an eighth field of pool state, written by the finalizer from the coins it has just created, and the finalizer's solution no longer has a field to name them -- so there is nothing left to substitute rather than a check that a substitution must fail.
contracts/v12/puzzles/forge_multi_reserve_finalizer.rue.
_test_v11_v12_review_findings.py accepts your decoy against the previous revision and refuses it against this one with MESSAGE_NOT_SENT_OR_RECEIVED, and separately asserts that the solution has no reserve-parent field at all.
…mpty actions Editor's asks: paragraphs are single lines, the 25 em-dashes are spaced hyphens as in CHIP-0050. Review P1 (greimela): the protocol fee is in basis points, denominator 10,000, matching the reference implementation -- the text had said parts per million. Review (cursor-bot): the action layer refuses an empty action list; the CHIP now says so. Design changes for the remaining findings follow once the revised puzzles are live on testnet11. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
P0, genesis can authorize more LP than state records: the launcher's announcement names the eve coin id and the TAIL asserts it with its own. P1, cross-generation oracle backfill: elapsed time is h minus the pool coin's consensus-asserted birth height. P1, the final LP position cannot redeem: MIN_LOCKED_LP at genesis, enforced at registration and in remove. P1, reserve receiver derivation trusts solution data: reserve parent ids live in state, written by the finalizer. Security table and a revision history updated accordingly. The reference implementation's revised puzzles are described in the linked repository once deployed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…counts Housekeeping against the Draft review; no design change. The Additional Assets list named three documents the reference repository no longer carries. When the shipping revision replaced the retired one, that revision's specification, architecture and CLVM pass were withdrawn from the public repository -- deliberately, since publishing working exploit mechanics for retired code helps an attacker practising against the live one and helps a reviewer not at all. The list now names the one specification that exists, and every link in the document has been checked to resolve. Comments-URI pointed at pull request 192 rather than 217. The deployment is thirty-three pools rather than twenty, and every action a user can take -- swap, deposit, withdrawal, multi-hop route and split route -- has now settled on chain through a wallet-signed Offer handed to the keyless router, which was not yet true when revision 2 was written. Also finishes the Editor's two formatting asks, which revision 1 had applied to the prose but not to everything since added: the remaining mid-paragraph line breaks are joined, and the two U+2212 MINUS SIGN characters are plain hyphens. The only non-ASCII character left is the product symbol inside the fenced invariant formula. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
@greimela -- thank you for the review, and particularly for reproducing the first two with accepted bundles. That is what made them impossible to argue with. All five findings are addressed in the reference implementation, which has moved from V11.1 to V12 (protocol 13). Each reply above names the puzzle and the tests. The pattern throughout was to remove the thing that could be substituted rather than to add a check that substitution must fail: the genesis announcement names the eve, elapsed time comes from a consensus-asserted birth height, the reserve parents moved into state and the solution field is gone. Two notes on process:
@danieljperry -- both formatting asks are done: mid-paragraph line breaks are gone and there are no U+2014 characters. The only non-ASCII character left is the product symbol inside the fenced invariant formula, which I have kept as notation; happy to spell it out if you would rather it were ASCII. |
…nd who holds the locked minimum Two corrections from an automated review of revision 3. Both are text; neither changes the design or the reference implementation. The oracle paragraph described the interval as `h - birth` without stating the `h >= birth` assertion that keeps it non-negative, and said a successor created and spent in the same block accumulates nothing. The second part was wrong in a way worth correcting precisely: such a successor cannot be spent in that block at all. ASSERT_HEIGHT_ABSOLUTE is checked against the previous transaction block, so the largest legal `h` is that height, while a coin created in the block being made is dated one higher, and `h >= birth` then has no solution. Same outcome for the cross-generation attack, different mechanism, and the difference matters to anyone implementing from this text: without the assertion the subtraction really would go negative and take the accumulator with it. The `remove` row claimed the locked minimum is the creator's and that every later holder can redeem every unit they hold. The first half was wrong about the reference implementation and the second only follows from the first being handled. The floor is on total supply, and LP is a transferable CAT, so if the minimum were retained it would be a holding that can never be burned and its eventual holder would be stranded -- the case the rule exists to prevent. The pool cannot enforce this, because the puzzle sees a total and a burn and never who holds which unit. So the genesis mint placing the minimum beyond recovery is now normative, with the reasoning, and the reference implementation's burn address is named as how it does that. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
30f100b to
59f3b25
Compare
|
Updated audit report from Opus and Fable. Forge / CHIP-0062 — V12 Re-Audit & Cross-Validation vs. #217Target: Executive summaryV12 should not be characterized as "the CHIP-0062 review findings are closed." Three of PR-217's five human findings are genuinely and solidly fixed, cross-confirmed by two independent review processes using different methodologies. But: the fix claimed for a fourth (terminal-LP / Of our original V11 findings: C-1 (CRITICAL) and L-1 (LOW) are cleanly closed. M-2 (MEDIUM-HIGH) is closed against its original attack shape but the fix introduces a new HIGH-severity regression (below). M-3, M-4, L-2 (root cause), L-3, L-5, L-6, L-7, I-1 are all still open, mostly untouched because V12 only targeted the five specific shapes PR-217 tested. M-4 is escalated from MEDIUM to HIGH with an executed full-reserve drain. Several new findings were surfaced. 1. Findings that are genuinely closed — cross-confirmedC-1 → PR Finding 1 (genesis double-mint) — CLOSED
Confirmed by two unrelated mechanisms, independently:
M-2 → PR Finding 2 (same-bundle oracle backfill) — CLOSED against its original shape
L-1 → PR Finding 5 (reserve receiver from solution) — CLOSED
PR Finding 4 (fee-unit ppm/bps mismatch) — verified in-puzzle, not just text
PR Finding 6 (empty action list) — verified
2. PR Finding 3/8 (terminal LP / MIN_LOCKED_LP) — the author's rebuttal to CNI is not accurateThis is a genuine gap our own re-audit initially missed and only caught during cross-validation. PR-217's Cursor bot (round 2, 2026-09-14) flagged that the CHIP text wrongly implied On inspection of the actual reference implementation, this claim does not hold:
Consequence: the creator holds 1,000 transferable-but-effectively-unredeemable LP units by default — exactly the scenario the bot's Finding 8 warned about, live in the reference implementation, not merely a text error as the author told CNI. 3. Finding 7 vs. our own oracle finding — the most important reconciliation in this reportPR-217's Cursor bot (round 2) claimed That conclusion is wrong, for a mechanism nobody in the PR thread raised. Both oracle-team agents (independently, with the Opus agent pinning the exact root cause) found:
Verified by direct execution:
For a TWAP consumer, a reading that can be driven to 0 or ∞ is the same category of failure as a reading that can be inflated — this is a regression of the same underlying finding, not a separate new issue, and it survived precisely because PR-217's testing (and the CHIP's own revision-4 text, "a claimed h can understate that interval by at most the window") never considered the zero case. Severity: MEDIUM–HIGH for any oracle consumer (matches the original M-2 ladder); no direct Forge fund-loss path, same caveat as M-2 always carried, since no Forge leaf prices off its own oracle. 4. Findings PR-217 never tested — ranked by real-world severityPR-217's five human findings all exercise a single, internally-consistent pool configuration. None varies config across leaves, none touches registry admission/squatting, none touches M-4 (unregistered pool, cross-leaf config mismatch) — STILL OPEN, escalated MEDIUM → HIGHNothing on the pool coin itself, and nothing in the action layer ( Two independent full drains, both accepted by real chia_rs, reproduced by both genesis-team agents:
This specifically defeats the project's own MIN_LOCKED_LP burn is unimplemented — MEDIUM (§2 above; new finding from cross-validation)Registry
|
…erify the burn A second independent review of the reference implementation tested configuration and registry admission rather than the single well-formed pool the first review exercised. Three of its findings change what this document requires. An implementation must bind its whole leaf set to one configuration at the coin level. The action layer proves a leaf's membership of the merkle root and not the leaves' agreement with each other, so a pool assembled from leaves of differing configurations releases its reserves against an asset of the attacker's choosing. The binding also makes the leaf set legible: the hashes sit in the finalizer's curry, so a verifier reads them off the coin. The oracle must credit the interval between a spend's claimed height and its inclusion, at the price in force during it. Revision 4's claim that a claimed height 'can understate that interval by at most the window' was wrong in the case where it understates it to nothing, which is free and permanent. Registration must verify the genesis burn rather than only require it in prose, which it can do because the genesis supply passes through a settlement whose payments are announced. Alongside: the genesis supply must exceed the locked minimum rather than merely reach it; the protocol fee recipient and the oracle parameters are pinned by the registry rather than chosen by the registrant; and both oracle parameters are bounded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
@trgarrett - thank you, and particularly for testing the thing the first review did not: a pool's configuration, and the registry's admission rules. Three of these are real, one is the worst finding this project has had, and all three are fixed in a new revision rather than patched into the old one. Below is the disposition of every item, including the two I could not reproduce. Revision 5 is now on this PR, and the reference implementation has moved from V12 (protocol 13) to V13 (protocol 14). V12's testnet liquidity has been withdrawn and its sources removed from the public repository, as V11's were. The three that forced a major incrementM-4, cross-leaf configuration mismatch. Correct, and correctly ranked. The action layer proves each leaf is a member of the merkle root; nothing in it says the six leaves agree, and I had been reading "the registry recomputes the root from one config" as a property of the coin when it is a property of registration. Reproduced here at 99.80% of both reserves. V13 binds the leaf set at the coin level: the finalizer is curried with the configuration hash, the six leaf module hashes and the observe leaf's slot hash, recomputes the six-leaf root from them, and asserts it equals the root the action layer carries, on every spend. Worth being precise about what that buys, because my first write-up overstated it. It cannot stop an unrelated puzzle existing and is not meant to. What it does is make the leaf set legible: the hashes sit in the curry in the clear, so a verifier reads them off the coin and compares them with the published set. Five honest leaves and one poisoned no longer have a valid root, and a pool that rewrites the finalizer no longer matches the published hashes, so the mismatch is visible before anyone deposits. The registry performs exactly that comparison against its own pinned hashes. The discarded oracle interval. Correct, and the more interesting of the two oracle findings. Your framing is the one that makes it obvious: it is not an illegal negative interval, it is a legal zero one, and it is free. V13 keeps the spot price it last credited in state and credits two intervals: the previous state over The unburned floor. You are right, and the part that matters is that my reply to CNI described the deploy script as though it were the implementation. V13 does not rely on either driver. The genesis mint lands at The registry findingsM-3 / L-3. I have not added the three values to the key, because they are not market parameters and the key describes a market. The registry pins them instead: Deregistration and expiry: deliberately not added, and I would rather argue the position than quietly skip it. A registered pool cannot die, because the floor keeps it alive, and a thinly funded pool is a live market anyone can deepen, so "the key is taken" means "this market exists". That claim is worth nothing unless the curve actually accepts the deposit, so it is now a test rather than an argument: a pool registered at the smallest thing the registry admits, reserves The boundary. Correct. Registration now requires M-1. Correct. When the protocol and DAO recipients are equal the leaf now pays one coin of both fees. Pinned by a case that reproduces the V12 The two I could not reproduceI-1, L-2, L-6 and L-7. Both accurate. The observation slot is written for a consumer to spend with its own message and is never spent by a leaf, which is by design rather than oversight; the live path is the announcement, and the specification now says so instead of leaving it to be discovered. L-7's collisions fail the composer's own bundle rather than anyone else's, so they are a composition rule; I checked the router's lanes and none of them compose a pair that can collide. What revision 5 changesThe configuration binding, the exact oracle and the verified genesis burn are normative. Alongside them: the genesis supply must exceed the floor rather than reach it, the protocol fee recipient and the oracle parameters are pinned by the registry, and both oracle parameters are bounded. The "understate by at most the window" sentence is gone; it was wrong in exactly the case you found. Redeployed and exercisedtestnet11, 32 pools: 32 deposits, 29 swaps, 25 collects, 6 withdrawals, 4 observations, a multi-hop route, and swap, deposit and withdrawal settled through the real offer lane - the path a browser uses, with the router holding no key. 364 discoverability checks against the chain, including that every pool's floor is burned and unspendable. Every offline suite passes. Two notes on processYour cross-validation step is what caught the unburned floor, and it caught it because it compared the implementation against what I had told CNI rather than against the specification. That is a check neither review process would have made alone. And the finding I could not reproduce was as useful as the three I could, because it sent me to build the mutant rather than argue from the source. If the other two rebuttals above are wrong, that is the way I would like to find out. Still no external audit, and I would not want this advanced on the strength of the implementation alone. |
…chitecture and CLVM pass Housekeeping, no design change. The Additional Assets list named the previous revision's specification. That document was withdrawn from the reference repository when this revision replaced it, so the link resolved to nothing -- a dead link in a document under review, and the same failure revision 3 was written to correct when the V11 documents were withdrawn and the list pointing at them was not updated. It now names the current specification. The architecture description and the written CLVM pass have been brought to this revision and are listed alongside it, restoring the three-document set the list carried before those two were withdrawn with the revision they described. Both are published in the reference repository, and each link was checked against it before this commit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Re-tested V13 against every item in your reply, adversarially, against the actual shipping source rather than the prose — chia_rs-executed where a bundle would settle it. Seven of nine hold up, two of them more solidly than you argued (the config-root binding is a hard rejection, not merely legible; the oracle fix is exact, not approximate — I rebuilt your own on-chain numbers independently and they match to the block). Two don't hold, and I want to be as precise about those as you were about the others. The two that don't holdRegistry squatting — the dilution defense has a cheaper counter. Your I don't have a preferred fix to push on you, but the shape is: I-1, I think I see the gap in "no positive mint satisfies the lower bound": a negative slot does push Everything elseConfig-root binding: closes both shapes I sent, and closes them as a hard The oracle fix is exact — I reconstructed your on-chain example as a real three-spend bundle sequence independently and got 182 / 5 / 261, same as you. I went looking for a residual (whether deferred credit could still get mis-weighted within an interval, whether a same-block manipulate-then-observe could sneak a reading in) and didn't find one. Genesis burn, the three pinned registry fields, the Two things unrelated to any specific finding, in case they're useful: Still no external audit, on my side either — this was six adversarial passes plus this one, not a security firm. |
|
We will hold a public Zoom discussion of CHIP-62 on September 24 at:
See the #chips channel of our Discord for more details. |
… load-bearing Two corrections, no design change. The Feasibility section said the puzzle uses CHIP-0050's upstream action layer and finalizer with no changes to either; the action layer is upstream and unchanged, the pool's finalizer is this proposal's own multi-reserve finalizer, and the text now says so. And the author's reply to the fourth review said the add leaf's non-negative-deposit assertion was redundant; it is load-bearing, and the reference suite now pins it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Two corrections to things we published, both wrong, posted together. Neither changes the reference implementation; both change what we claimed about it. Revision 7 of the CHIP is on this PR with the text. 1. I-1 reproduces. We said it did not.@trgarrett - you were right, and our reply was wrong. Our earlier response, and We rebuilt your vector independently from the parameters you gave, without running your script: on Your arithmetic is exactly right, and the reason we missed it is the useful part. Our search ran through a Python mirror whose wrapper refuses negative deposits before the bracket is reached. We tested the guard we were trying to test, through a mirror carrying the same guard, and read its refusal as the bracket's. We had also stayed in the region where the positive slot is too small to overcome the negative slot's drag - We did not need the serialized bundle you offered - the reproduction matched to the unit - but thank you for offering it. Errata are on the two documents that made the claim. 2. The CHIP understated the custom code a reviewer must read.The Feasibility section said the puzzle is "built from CHIP-0050's upstream action layer and finalizer with no changes to either." Half true. The action layer is upstream and unchanged. The pool's finalizer is not: it is Forge's own multi-reserve finalizer - curried with the reserve hashes, the configuration hash and the six leaf module hashes, sending one CHIP-0025 message per reserve on every spend, and asserting that the action layer's merkle root is the six-leaf root of exactly that configuration. It is where M-4 was closed. The upstream finalizer is vendored unchanged and serves only the registry singleton. Vendoring the upstream finalizer does not make the pool's finalizer upstream code, and a reviewer reading the CHIP would have looked in the wrong place for the custom logic. Revision 7 says so, and names the finalizer as the code to read first. Flagged by the same review and carried into the next revision rather than corrected here: the integrity suite recompiles only the upstream pins and curve exports, not Forge's leaves and finalizer, and passes when |
In response to the fourth independent review of the reference implementation (trgarrett, 2026-09-15). One change is normative, and it is the most serious finding this proposal has had. Registration proved the launcher's authorization of a configuration without proving that the coins the configuration describes were ever made. A registry that rebuilds the pool's puzzle hash from reserve parents named in the solution, and reads the reserve amounts from that same solution, admits a pool whose reserves were never funded. Nothing is stolen, which is what makes it easy to miss: the pool simply cannot be spent by anyone, because the finalizer derives every reserve coin id from those parents and the first real interaction fails message pairing. The market's key is gone permanently for one creation fee, and the dilution defence the previous text relied on cannot be executed either. Specification now requires a conforming registry to observe something that exists only if the reserve does, with the reference implementation's reserve launcher given as the construction: register takes each reserve's GRANDparent, derives P_i = coinid(grandparent_i, launcher_hash_i, reserves[i]), and asserts a coin announcement from P_i. The ASCII-prefix requirement is stated, because the CAT layer refuses an inner coin announcement beginning 0xcb and a bare tree hash bricks one creation in 256. The three cheaper alternatives are recorded as refused so they are not re-attempted. Two further requirements are stated that the text had left to the implementation. A leaf that takes value binds its settlement by PARENT AND AMOUNT and asserts the derived coin, rather than asserting an announcement that says nothing about how much the coin holds. And the section says plainly what that does NOT guarantee: an assertion is not consumed, conservation is a property of the whole bundle, and a network fee is slack -- observed on chain, not reasoned about. The locked minimum is one unit. It exists so a pool is never spendable-but-empty, and a larger floor only strands liquidity: a thousand units stranded a median 2.21% of each pool against about 0.002% at one. Test Cases gains the rule that a mutation verdict of "survived" means UNREACHED, never redundant -- the method error behind revision 7's correction, written down as guidance rather than as an apology. The reference implementation has moved to V14 (protocol 15). V13's testnet liquidity was withdrawn and its sources removed from the public repository, as V12's and V11's were, and Additional Assets names the V14 documents.
The proposal describes three mechanisms that are hard to hold in prose, and the
last two are what revision 8 made normative. Each section now carries a figure
beside the description rather than instead of it.
Coin layout one pool spend, and the path that does NOT exist: a leaf emits a
tagged condition, the finalizer routes it and derives the reserve's
receiver id from committed state. A leaf never addresses a reserve.
Authorization the settlement binding against the shape it replaces, side by side.
Naming the coin id binds the asset and the id and says nothing about
the amount; deriving the id from a parent and an amount binds both.
Registry the reserve proof: one lineage per reserve inside the registration
bundle, the derivation register performs, and the announcement only
a coin running the launcher can make.
Hand-authored SVG under assets/chip-0062, following CHIP-0021's convention rather
than a rendering service, so the figures diff like text and need no network fetch.
Each carries its own opaque ground and explicit colours, so it reads on a light or
a dark page, and an aria-label stating the claim for anyone who cannot see it.
The Additional Assets note is updated: it offered to copy assets into this
repository once a number was assigned, and the figures now are here.
Two corrections from an automated review of revision 8, both to the text rather than the design. The reference implementation is unchanged by either: each construction below is what it already does, and what the CHIP failed to say. 1. The settlement rule omitted the puzzle hash. A coin id is coinid(parent, puzzle_hash, amount). Revision 8 said a leaf is given the settlement's parent and amount and "derives the coin id from them", which is not implementable -- it names two of the three inputs and leaves an implementer to guess the third. It is not a guess and it is not a solution field: the settlement puzzle hash is determined by the asset the leaf is crediting, the offer settlement puzzle for a native-XCH input and that puzzle inside the asset's CAT wrapper for a CAT, and the leaf already knows which asset it credits because the index is its own. That it is derived rather than supplied is the whole point of the rule, so the rule now says it. 2. The reserve rule left the announcement's message unspecified, and this is the better catch of the two. Deriving P_i binds WHICH coin announced. It does not bind WHAT that coin did. A launcher built to revision 8's text could create a coin at some other puzzle hash, announce it truthfully, and satisfy an assertion that only checks that A coin was created -- leaving a pool whose reserves the finalizer will never recognise, which occupies the market key exactly as an unfunded reserve did. That is the finding R-1 closed, reopened one level down. A conforming launcher's announcement MUST commit to the puzzle hash it created, the amount, and the pool's launcher id; and the registry MUST build the expected message from the reserve puzzle hash it derives from the pool's own configuration, never from a value in the solution. Both halves are now normative. assets/chip-0062/reserve-proof.svg had the same gap: it drew the announcement as the ASCII tag with the payload elided. It now shows the tree hash over (created_puzzle_hash, amount, launcher_id), and the register box says the registry builds that message from the reserve hash it derives itself.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 7305762. Configure here.
|
@trgarrett - both of the two that didn't hold are closed, and you were right about both. Revisions 8 and 9 are on this PR and the reference implementation has moved from V13 (protocol 14) to V14 (protocol 15). V13's testnet liquidity has been withdrawn and its sources removed from the public repository, as V12's and V11's were. Registry squatting - closed, and closed the shape you namedYou wrote: "register needs to observe something that only exists if the reserves are real - an announcement from each reserve coin, the way genesis now proves the LP burn actually happened rather than merely being authorized." That is exactly what V14 does, and it is worth saying that the fix came from your sentence rather than from us finding a better one. V14 adds a seventh puzzle. then asserts a coin announcement from Two details you will want: The ASCII prefix on the launcher's message is load-bearing. The CAT layer refuses an inner coin announcement whose first byte is its own ring marker Three cheaper shapes were tried against consensus first and all three failed, which is why the launcher exists rather than something simpler. A puzzle announcement binds the puzzle and not the announcing coin, and accepted a funded reserve under a wrong parent. A coin announcement from an arbitrary coin binds the coin and not the deed. And spending the genesis eve to prove the reserve cannot be built at all: Refused by the live node, not by our validator. The V13 construction - a registration that pays the fee and mints the genesis LP exactly as an honest one does, but creates no reserve launcher - pushed at the live V14 registry on testnet11 comes back Your units correction on the I-1 - answered in revision 7, and the method is now in the CHIPNothing to add to the reply above beyond this: the rule that produced the error is now written into the CHIP itself rather than living in our notes. Test Cases now says a mutation verdict of "survived" means unreached, never redundant, and that a conforming test method SHOULD require either a probe that reaches the line or a written argument for why none can. Our own runs report UNREACHED and fail unless every unreached line is argued in The two loose items
The cause is line endings. Ten
Two things we got wrong ourselves since your reviewNeither was found by a reviewer, and both are corrections to what we claimed rather than to the puzzles. "Two swaps naming one settlement are refused by conservation" was too strong. True only of a bundle with nothing spare in it. An assertion is not consumed, so both leaves' The locked minimum is one unit in V14, not a thousand. The floor exists so a pool is never spendable-but-empty, and one unit does that. A thousand stranded a median 2.21% of each pool's liquidity, and 4.91% at worst, for no property the puzzle gains. Also in revision 8A leaf that takes value now binds its settlement by parent and amount and asserts the derived coin, rather than asserting an announcement that says nothing about how much the coin holds. On V13 a swap naming a one-mojo settlement as its 250,000 input is accepted when another coin supplies the value; the live node refuses it on V14 with An automated review of revision 8, since this was draftedCursor's bot raised two, and both are worth having: each is valid against the text and wrong about the implementation, which is the same shape as the fifth review. Revision 9 fixes the text; no puzzle changes. The settlement rule omitted the puzzle hash. A coin id is The reserve rule left the announcement's message unspecified, and this one is the better catch. Deriving We built that construction rather than asserting it does not workBoth probes run the same creation lane an honest pool takes, with one value in a launcher's solution changed. A probe that builds its own bundle proves something about the probe.
The live registration is fully funded - the fee, the eve, the LP backing and both launchers are all paid for - so the node is judging the construction and not a shortfall. Nothing was spent, because nothing confirmed. The MEDIUM finding gets the arithmetic it asks for in the same run: one parent and one amount produce two different coin ids for two different assets, and the id derived for the CAT is the real coin's id to the byte. So the id is not derived from two values. It is derived from three, and the third is fixed by the asset rather than supplied alongside it. @danieljperry - noted on the September 24 call, and I will be there. Still no external audit on our side either. Four rounds of review have now found real defects in this code, one a demonstrated drain and one a market key lost forever, which is an argument for more review rather than for confidence. |
The hand-authored SVGs did not look like anything else this project publishes. These are Mermaid, rendered locally and committed as SVG, which is how the Forge workflow atlas draws the same material -- so the CHIP's figures and the project's documents now read as one set rather than two. Two of the five are the atlas's own diagrams rather than new drawings: the pool lifecycle and a trade end to end, taken verbatim and changed in exactly one way. Each gains a DASHED BOUNDARY marking where this proposal's normative surface stops. A CHIP reader needs that line: the lifecycle's deployment index and resync are the reference implementation's bookkeeping, and in the trade the wallet and the router are one implementation's choice, while the span inside the boundary is the part a competing implementation must match. The other three are specific to this proposal and have no atlas equivalent: one pool spend including the path that does NOT exist, the settlement binding against the shape it replaces, and the reserve proof at registration. Each figure ships as the Mermaid source beside the rendered SVG. A figure that diffs like text can be regenerated rather than redrawn, and a reviewer can read what it claims without trusting the picture. The SVGs are self-contained: no font, no script, no external reference of any kind, and each carries its own background so it reads whatever the page around it does. Rendered with mermaid 11 against the same theme variables the Forge pages use.
Two of the five were unreadable where it counts. GitHub renders a document at about 500px wide, and at that width settlement-binding came out 500x69 and pool-spend 500x89 -- a strip of unreadable text rather than a diagram. Measured, not guessed: the rendered width of the image element on the blob page, against its natural size. settlement-binding was laying its six nodes out in ONE row, so it was eight times wider than tall. Each subgraph needs an explicit `direction TB`; I had stripped those while trying the chart the other way round, and without them the steps do not stack. Restored, and the long single-line labels are broken across lines so a node is not a thousand pixels wide on its own. 1944x268 -> 754x652. pool-spend ran left to right across five nodes. It reads the same top to bottom and fits a column instead of a billboard. 1653x294 -> 658x772. All five now land between 231 and 586 pixels tall in a 500px column, which is a diagram rather than a stripe. Nothing about what they claim has changed.
Mermaid renders labels with htmlLabels off, so it does not decode entities: the two subgraph titles in settlement-binding read "The id is DERIVED — what V14 requires" on the rendered figure. Plain hyphens instead, which is what the rest of this document uses anyway. Caught by looking at the figure at the width it is read at rather than at the size it was drawn.
The reserve rule spoke of "the puzzle hash it created" and of the hash the registry derives as though they were one value. For a CAT reserve they differ by exactly one wrap: the launcher runs as the CAT's inner puzzle, so the hash it is given and announces is the reserve's inner hash, the coin that appears is that hash inside the wrapper, and launcher_hash_i is the launcher's hash inside that same wrapper. Built from the text as written, an implementer could hand the launcher the wrapped hash. The CAT layer would wrap it again, the announcement would still match, registration would succeed, and the reserve would be one the finalizer never recognises -- the permanently occupied market key revision 8 exists to prevent, reached another way. Registry now tabulates all three hashes for both reserve kinds and states that failure, and the Registry figure names each one. The reference implementation already keeps them apart: reserve_launcher_full_hash wraps the launcher, reserve_inner_hashes does not wrap the reserve, and the driver hands the launcher r.inner_hash. No puzzle changes; protocol 15 is unaffected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The video from our discussion of this CHIP is now available on Youtube: |
|
This CHIP is now in |
|
A question for Chia Network, raised at the September 24 discussion: should The Forge build revocable-CAT (CHIP-0038) support into CHIP-0062 now, or leave it for a later CHIP? Where it stands. The reference implementation (V14) accepts no revocable coin. A reserve and every settlement must hash to exactly What support would take. Accepting the layer is one curried hash away, but supporting it safely needs three things:
The options as we see them.
A related question either way: is there, or will there be, a canonical way to learn an asset's intended hidden puzzle hash? Wallets read it from the coin today, which is exactly what an imprint takes advantage of. |
In my opinion, you should go with the Later option. Add support for rCATs if/when there is demand. For now, keep the scope as-is. You can always create a new CHIP with this extra support later. That said, this is a decision you'll need to make for your own CHIP. As for your related question, every rCAT coin can have a different hidden puzzle, even if the rCAT itself is identical. If the minter can prove that they minted every coin from the same singleton with the same hidden puzzle, then every coin must have the same hidden puzzle. However, this is a difficult thing to prove, so the only safe assumption is that you can't prove what a coin's hidden puzzle is until that coin is spent. |
… id by coinid, the 0xcb note corrected Tracks V15 (protocol 16) of the reference implementation, cut after a sixth review on 2026-10-07. The registry's creation fee becomes state moved only by a set_fee leaf on a message from a coin at the treasury's puzzle hash; the last two concatenation-derived coin ids use coinid and hashed atoms are stored canonically; the reserve-announcement note claimed the CAT layer refuses any inner announcement beginning 0xcb, which is wrong (33 bytes only); two revisions can be live at once, routed across off chain. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Revision 11 (2026-10-07) is pushed. It lands after the move to Review, because what it records was found by reviews run after the Review request; we would rather the text under Review match the reference implementation than hold a correction back. Why it exists, briefly: A sixth review of the reference implementation ran on 2026-10-07, this time over the published repositories rather than the puzzles alone. Everything it rated above informational was in the off-chain composer and services, and is fixed there. Two findings were against the puzzles, both low and both fail-closed as they stood: two coin ids still derived by hashing a concatenation instead of Since a revision was being cut, it also carries one design change the operator needed: the registry's creation fee moves from a curried constant into registry state, with a The revision also corrects the text itself. Revision 10's implementer note said the CAT layer refuses any inner coin announcement whose first byte is Finally, Backwards Compatibility now records that two revisions can be live at once, each pool spent by its own revision's leaves, since V14 is kept serving beside V15 in the reference implementation and a route across the two is composed off chain. The full disposition and the testnet record are in |
…he September 24 discussion Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ss 0x000...dead Tracks V16 (protocol 17) of the reference implementation. Registry names the burn address; the rule (a puzzle hash with no preimage) is unchanged. Additional Assets gains FORGE_PUZZLE_V16.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Backwards Compatibility still described V14 serving beside V15, and Additional Assets named contracts/v15 as the revision this text describes, where revision 12 tracks V16. Both now name V16 (protocol 17), with V15 and V14 kept live beside it. No design change.

Proposes a weighted constant-function market maker in which one singleton holds any number of assets, each in its own reserve coin, at weights fixed at pool creation. Built on CHIP-0050's action layer; authorizations are CHIP-0025 messages whose receiver coin id the pool derives rather than accepts.
Informational / Puzzle. Requires CHIP-0025 and CHIP-0050.
Supporting material — the full specification, architecture, a written CLVM pass over every leaf, security notes, and a comparison against publicly documented AMM failures — is referenced from the public forge-puzzles repository, alongside the implementation it describes.
Note
Low Risk
Documentation-only CHIP and diagram assets; no consensus, node, or wallet code changes in this repository.
Overview
Adds CHIP-0062 (The Forge: Weighted N-Asset AMM), an informational puzzle CHIP that specifies a CHIP-0050 action-layer AMM where one singleton holds N assets at fixed weights, with leaves for
swap,add,remove,collect,observe, and DAO fee reduction.The document is the normative summary of the design: weighted invariant pricing, derived CHIP-0025 receiver coin ids (not solution-supplied), settlement binding by parent and amount, a sorted on-chain registry with reserve-launcher proofs, oracle height crediting, and coexistence of puzzle revisions (V16 / protocol 17 as the described revision). It points to the forge-puzzles reference implementation and external spec docs rather than embedding puzzle code here.
Also adds
assets/chip-0062/Mermaid sources plus a renderedlifecycle.svg(and references to other figures) so the CHIP renders offline. Revision 13 is editorial only: Backwards Compatibility and Additional Assets now consistently name V16 alongside live V15/V14.Reviewed by Cursor Bugbot for commit 5d24016. Bugbot is set up for automated code reviews on this repo. Configure here.