feat(anthemus): SharePairAmmV2 — LP tokens + 0.30% curve fee validated; ADR-0049 ratified (snapshot 0222)
Step 3+4 of the Anthemus critical path: - contracts/SharePairAmmV2.cash — fractional LP tokens via the reservoir model (outstanding = supply − treasury; add releases pro-rata, remove absorbs and pays the fee-grown slice) + Uniswap-V2-style fee-on-input invariant (k grows 0.30% per swap, 100% to LPs) + treasury-drain guard (swap barred from the treasury input; locking-bytecode pinning; no dust drain). 4/4 mock tests at exact fee/mint/withdraw boundaries. - scripts/chipnet-anthemus-amm-v2.mjs — 5/5 PASS live chipnet: bootstrap → fee swap (1,000→906) → LP release (exactly 1,000) → LP burn returning 1,100 X + 909 Y (fees included). Pool end-state exact. - services/anthemus-relay — curveLevels()/poolBook() + GET /pool: the AMM rendered as a maker on the SAME book (pool-as-maker path). - wiki/decisions.md — ADR-0049 written against the validated bytecode: brand locked, book-first ratified, ADR-0016 refined (hub-exclusive pools only), maker 0 / taker 0.15% consensus-enforced, curve fee 100%-to-LP with the protocol slice deferred, tollBps accepted-pending, USDu preferred-not-forced. - critical-path doc → rev. 2 (Steps 1–4 status). Mixed shared files staged as HEAD+own-edits blobs; the concurrent session's uncommitted work remains untouched in the working tree. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
934c35fc56
commit
569f40de0a
10 changed files with 784 additions and 5 deletions
|
|
@ -5,7 +5,12 @@ deposits a cross-chain asset, trades it on Anthemus, and withdraws. Engine order
|
|||
**order book first, AMM on top as an automated order**. Brand & scope:
|
||||
[anthemus_dex.md](anthemus_dex.md). Model: [anthemus_business-model.md](anthemus_business-model.md).*
|
||||
|
||||
**Date:** 2026-07-17 · rev. 1 · **Status:** plan — not started. Supersedes the AMM-first
|
||||
**Date:** 2026-07-17 · rev. 2 (2026-07-21) · **Status:** Steps 1–3 BUILT + validated,
|
||||
Step 4 ratified — Step 1 `AnthemusOrderBook` + relay (snapshot 0220, 7/7 mock + 8/8
|
||||
chipnet), Step 2 Agora ▸ Anthemus Swap+Exchange UIs (0220), Step 3 `SharePairAmmV2`
|
||||
LP-tokens+curve-fee + pool-as-maker relay path (snapshot 0222, 4/4 mock + 5/5 chipnet),
|
||||
Step 4 ADR-0049 written against the validated bytecode. Remaining: UI place/cancel →
|
||||
covenant wiring; wrap-covenant `tollBps`; T6 type-2 orders. Supersedes the AMM-first
|
||||
sketch in the business-model §10 (engine order inverted, see §1).
|
||||
|
||||
---
|
||||
|
|
|
|||
157
contracts/SharePairAmmV2.cash
Normal file
157
contracts/SharePairAmmV2.cash
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
pragma cashscript ^0.12.0;
|
||||
|
||||
// SharePairAmmV2 — the PRODUCTION constant-product AMM (Anthemus critical path Step 3;
|
||||
// anthemus_business-model §10 change 1). Upgrades the validated SharePairAMM spike with
|
||||
// the three coupled changes the model requires:
|
||||
//
|
||||
// 1. FRACTIONAL LP TOKENS — reservoir model (the repo's pre-mint idiom): the full
|
||||
// `lpSupply` of `lpCategory` FTs is pre-minted into a third covenant UTXO (the
|
||||
// LP TREASURY). Outstanding LP = lpSupply - treasury balance — no minting NFT,
|
||||
// no commitment state. addLiquidity RELEASES LP from the treasury pro-rata;
|
||||
// removeLiquidity ABSORBS LP back and releases a pro-rata slice of BOTH
|
||||
// reserves *including accrued fees*. Replaces the single-lpPkh spike.
|
||||
// 2. CURVE FEE — 0.30% on the input side, Uniswap-V2 style: the invariant is
|
||||
// checked on FEE-ADJUSTED balances (x·1000 - dxIn·3), so `k` grows by the fee
|
||||
// every swap and LP tokens are the claim on the grown k. 100% to LPs (§2a
|
||||
// option i — no protocol slice; identical posture to Cauldron).
|
||||
// 3. OVERFLOW — the v1 caveat is resolved by the 2025 VM upgrade: script ints are
|
||||
// big integers, so reserve-product × 10^6 scaling cannot overflow. Token
|
||||
// amounts themselves stay int64-bounded by consensus.
|
||||
//
|
||||
// THREE co-spent covenant UTXOs, pinned by index:
|
||||
// input 0: X reserve (xCategory FT) — swap + liquidity
|
||||
// input 1: Y reserve (yCategory FT) — swap + liquidity
|
||||
// input 2: LP treasury (lpCategory FT) — liquidity ops ONLY
|
||||
//
|
||||
// Safety over v1 (both hardenings matter):
|
||||
// - swap() requires activeInputIndex <= 1, so the TREASURY can never be unlocked
|
||||
// through the swap path (else its LP tokens — the claim on all reserves — could
|
||||
// be swept). Liquidity functions running on ANY input enforce the full shape.
|
||||
// - every pinned input must share the active input's locking bytecode, so a
|
||||
// spender cannot substitute their own token UTXOs for a covenant leg.
|
||||
// - re-created covenant outputs must keep >= the input sats (no dust drain).
|
||||
//
|
||||
// Bootstrap is a genesis PLACEMENT, not a covenant call: the founder parks initial
|
||||
// reserves x0/y0 and a treasury of (lpSupply - L0) while keeping L0 LP tokens —
|
||||
// funding a covenant address is unconstrained. addLiquidity therefore requires
|
||||
// outstanding LP > 0.
|
||||
|
||||
contract SharePairAmmV2(
|
||||
bytes xCategory, // reserve X fungible category, consensus order
|
||||
bytes yCategory, // reserve Y fungible category, consensus order
|
||||
bytes lpCategory, // LP-share fungible category, consensus order
|
||||
int lpSupply // total pre-minted LP supply (treasury + circulating)
|
||||
) {
|
||||
// ---- SWAP: permissionless trade against the pool; k grows by the 0.30% fee. ----
|
||||
function swap() {
|
||||
require(this.activeInputIndex <= 1); // never the treasury
|
||||
require(tx.inputs[0].lockingBytecode == tx.inputs[this.activeInputIndex].lockingBytecode);
|
||||
require(tx.inputs[1].lockingBytecode == tx.inputs[this.activeInputIndex].lockingBytecode);
|
||||
require(tx.inputs[0].tokenCategory == xCategory);
|
||||
require(tx.inputs[1].tokenCategory == yCategory);
|
||||
int x = tx.inputs[0].tokenAmount;
|
||||
int y = tx.inputs[1].tokenAmount;
|
||||
|
||||
// [0] X reserve back, [1] Y reserve back — same covenant, same categories,
|
||||
// sats not drainable.
|
||||
require(tx.outputs[0].lockingBytecode == tx.inputs[0].lockingBytecode);
|
||||
require(tx.outputs[0].tokenCategory == xCategory);
|
||||
require(tx.outputs[0].value >= tx.inputs[0].value);
|
||||
require(tx.outputs[1].lockingBytecode == tx.inputs[1].lockingBytecode);
|
||||
require(tx.outputs[1].tokenCategory == yCategory);
|
||||
require(tx.outputs[1].value >= tx.inputs[1].value);
|
||||
int x2 = tx.outputs[0].tokenAmount;
|
||||
int y2 = tx.outputs[1].tokenAmount;
|
||||
require(x2 > 0);
|
||||
require(y2 > 0);
|
||||
|
||||
// Fee-adjusted constant product (fee = 3/1000 on the input side, to LPs):
|
||||
// credit only 99.7% of what came IN toward the invariant; the rest stays
|
||||
// in the pool and grows k.
|
||||
int dxIn = 0;
|
||||
if (x2 > x) {
|
||||
dxIn = x2 - x;
|
||||
}
|
||||
int dyIn = 0;
|
||||
if (y2 > y) {
|
||||
dyIn = y2 - y;
|
||||
}
|
||||
require((x2 * 1000 - dxIn * 3) * (y2 * 1000 - dyIn * 3) >= x * y * 1000000);
|
||||
}
|
||||
|
||||
// ---- ADD LIQUIDITY: deposit both legs pro-rata; the treasury RELEASES LP. ----
|
||||
// The released LP goes to the provider by ordinary FT conservation.
|
||||
function addLiquidity() {
|
||||
require(tx.inputs[0].lockingBytecode == tx.inputs[this.activeInputIndex].lockingBytecode);
|
||||
require(tx.inputs[1].lockingBytecode == tx.inputs[this.activeInputIndex].lockingBytecode);
|
||||
require(tx.inputs[2].lockingBytecode == tx.inputs[this.activeInputIndex].lockingBytecode);
|
||||
require(tx.inputs[0].tokenCategory == xCategory);
|
||||
require(tx.inputs[1].tokenCategory == yCategory);
|
||||
require(tx.inputs[2].tokenCategory == lpCategory);
|
||||
int x = tx.inputs[0].tokenAmount;
|
||||
int y = tx.inputs[1].tokenAmount;
|
||||
int tIn = tx.inputs[2].tokenAmount;
|
||||
int outstanding = lpSupply - tIn;
|
||||
require(outstanding > 0); // pool was bootstrapped
|
||||
|
||||
// [0][1][2] re-create all three covenant legs.
|
||||
require(tx.outputs[0].lockingBytecode == tx.inputs[0].lockingBytecode);
|
||||
require(tx.outputs[0].tokenCategory == xCategory);
|
||||
require(tx.outputs[0].value >= tx.inputs[0].value);
|
||||
require(tx.outputs[1].lockingBytecode == tx.inputs[1].lockingBytecode);
|
||||
require(tx.outputs[1].tokenCategory == yCategory);
|
||||
require(tx.outputs[1].value >= tx.inputs[1].value);
|
||||
require(tx.outputs[2].lockingBytecode == tx.inputs[2].lockingBytecode);
|
||||
require(tx.outputs[2].tokenCategory == lpCategory);
|
||||
require(tx.outputs[2].value >= tx.inputs[2].value);
|
||||
|
||||
int dx = tx.outputs[0].tokenAmount - x;
|
||||
int dy = tx.outputs[1].tokenAmount - y;
|
||||
require(dx > 0);
|
||||
require(dy > 0);
|
||||
int lpMint = tIn - tx.outputs[2].tokenAmount; // LP released to the provider
|
||||
require(lpMint > 0);
|
||||
// Pro-rata mint bound on BOTH legs (floor division favors the pool):
|
||||
// lpMint <= dx·L/x and lpMint <= dy·L/y.
|
||||
require(lpMint * x <= dx * outstanding);
|
||||
require(lpMint * y <= dy * outstanding);
|
||||
}
|
||||
|
||||
// ---- REMOVE LIQUIDITY: burn LP into the treasury; withdraw pro-rata + fees. ----
|
||||
function removeLiquidity() {
|
||||
require(tx.inputs[0].lockingBytecode == tx.inputs[this.activeInputIndex].lockingBytecode);
|
||||
require(tx.inputs[1].lockingBytecode == tx.inputs[this.activeInputIndex].lockingBytecode);
|
||||
require(tx.inputs[2].lockingBytecode == tx.inputs[this.activeInputIndex].lockingBytecode);
|
||||
require(tx.inputs[0].tokenCategory == xCategory);
|
||||
require(tx.inputs[1].tokenCategory == yCategory);
|
||||
require(tx.inputs[2].tokenCategory == lpCategory);
|
||||
int x = tx.inputs[0].tokenAmount;
|
||||
int y = tx.inputs[1].tokenAmount;
|
||||
int tIn = tx.inputs[2].tokenAmount;
|
||||
int outstanding = lpSupply - tIn;
|
||||
require(outstanding > 0);
|
||||
|
||||
require(tx.outputs[0].lockingBytecode == tx.inputs[0].lockingBytecode);
|
||||
require(tx.outputs[0].tokenCategory == xCategory);
|
||||
require(tx.outputs[0].value >= tx.inputs[0].value);
|
||||
require(tx.outputs[1].lockingBytecode == tx.inputs[1].lockingBytecode);
|
||||
require(tx.outputs[1].tokenCategory == yCategory);
|
||||
require(tx.outputs[1].value >= tx.inputs[1].value);
|
||||
require(tx.outputs[2].lockingBytecode == tx.inputs[2].lockingBytecode);
|
||||
require(tx.outputs[2].tokenCategory == lpCategory);
|
||||
require(tx.outputs[2].value >= tx.inputs[2].value);
|
||||
|
||||
int lpBurn = tx.outputs[2].tokenAmount - tIn; // LP absorbed back (burned)
|
||||
require(lpBurn > 0);
|
||||
int dx = x - tx.outputs[0].tokenAmount; // withdrawn X (incl. fees)
|
||||
int dy = y - tx.outputs[1].tokenAmount; // withdrawn Y (incl. fees)
|
||||
require(dx >= 0);
|
||||
require(dy >= 0);
|
||||
// Pro-rata withdrawal bound: dx <= lpBurn·x/L, dy <= lpBurn·y/L.
|
||||
require(dx * outstanding <= lpBurn * x);
|
||||
require(dy * outstanding <= lpBurn * y);
|
||||
// Reserves can never be fully drained.
|
||||
require(tx.outputs[0].tokenAmount > 0);
|
||||
require(tx.outputs[1].tokenAmount > 0);
|
||||
}
|
||||
}
|
||||
|
|
@ -58,6 +58,7 @@
|
|||
"chipnet:redeem-safety": "node scripts/chipnet-redeem-safety-test.mjs",
|
||||
"registry:serve": "node --import tsx scripts/registry-serve.mjs",
|
||||
"chipnet:anthemus-orderbook": "node scripts/chipnet-anthemus-orderbook.mjs",
|
||||
"chipnet:anthemus-amm-v2": "node scripts/chipnet-anthemus-amm-v2.mjs",
|
||||
"anthemus:relay": "node services/anthemus-relay/server.mjs",
|
||||
"chipnet:lib-balances": "node --import tsx scripts/chipnet-lib-balances.mjs",
|
||||
"chipnet:lib-send": "node --import tsx scripts/chipnet-lib-send.mjs",
|
||||
|
|
|
|||
206
scripts/chipnet-anthemus-amm-v2.mjs
Normal file
206
scripts/chipnet-anthemus-amm-v2.mjs
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* chipnet-anthemus-amm-v2.mjs — Anthemus Step 3 GATE: SharePairAmmV2 (fractional
|
||||
* LP tokens + 0.30% curve fee, reservoir treasury) on LIVE chipnet.
|
||||
*
|
||||
* 1. genesis X + Y + LP FT categories (C); 2,000 X → B (the trader)
|
||||
* 2. BOOTSTRAP placement: 10,000 X + 10,000 Y + 990,000 LP treasury into the pool
|
||||
* (C keeps 10,000 circulating LP — the genesis LP position)
|
||||
* 3. B SWAPS 1,000 X → 906 Y (0.30% fee on input stays in the pool; k grows)
|
||||
* 4. C ADDS liquidity 1,100 X + 910 Y → treasury releases exactly 1,000 LP
|
||||
* 5. C REMOVES 1,000 LP → pro-rata slice 1,100 X + 909 Y (fees INCLUDED)
|
||||
*
|
||||
* Final deltas prove the loop: B −1,000 X +906 Y · C LP net 0 (add − remove) ·
|
||||
* pool x=11,000 y=9,095 treasury=990,000.
|
||||
*
|
||||
* Roles: C = founder/LP/funder, B = trader. Resumable.
|
||||
* Run: node scripts/chipnet-anthemus-amm-v2.mjs --yes
|
||||
*/
|
||||
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { TestNetWallet } from 'mainnet-js';
|
||||
import { Contract, ElectrumNetworkProvider, TransactionBuilder, SignatureTemplate } from 'cashscript';
|
||||
|
||||
const ROOT = join(import.meta.dirname, '..');
|
||||
const STATE = join(ROOT, 'ChipnetTests', '.chipnet-anthemus-amm-v2.json');
|
||||
const ARTIFACT = join(ROOT, 'contracts/dist/SharePairAmmV2.json');
|
||||
const KEYS = { b: join(ROOT, '.chipnet-keys-b.json'), c: join(ROOT, '.chipnet-keys-c.json') };
|
||||
|
||||
const FT_SUPPLY = 1_000_000n; // X and Y genesis supplies (to C)
|
||||
const LP_SUPPLY = 1_000_000n; // total LP (treasury + circulating)
|
||||
const X0 = 10_000n, Y0 = 10_000n, L0 = 10_000n;
|
||||
const SWAP_IN = 1_000n, SWAP_OUT = 906n; // floor(10000·997/10997)
|
||||
const ADD_X = 1_100n, ADD_Y = 910n, ADD_LP = 1_000n; // min(1100·10000/11000, 910·10000/9094)
|
||||
const REM_LP = 1_000n, REM_X = 1_100n, REM_Y = 909n; // floor(1000·12100/11000), floor(1000·10004/11000)
|
||||
const DUST = 1000n, MF = 3000n;
|
||||
|
||||
const hexToBin = (h) => { const o = new Uint8Array(h.length / 2); for (let i = 0; i < o.length; i++) o[i] = parseInt(h.slice(i * 2, i * 2 + 2), 16); return o; };
|
||||
const txidLE = (h) => { const d = hexToBin(h); const o = new Uint8Array(d.length); for (let i = 0; i < d.length; i++) o[i] = d[d.length - 1 - i]; return o; };
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const divider = (c = '=') => console.log(c.repeat(80));
|
||||
const step = (t) => { console.log(''); divider(); console.log(t); divider(); };
|
||||
const txidOf = (s) => (typeof s === 'string' ? s : s?.txid ?? s?.txId);
|
||||
const load = () => (existsSync(STATE) ? JSON.parse(readFileSync(STATE, 'utf8')) : {});
|
||||
const save = (s) => writeFileSync(STATE, JSON.stringify(s, (k, v) => (typeof v === 'bigint' ? v.toString() : v), 2));
|
||||
function pickBch(u, need) { const p = []; let t = 0n; for (const x of u) { p.push(x); t += BigInt(x.satoshis); if (t >= need) break; } return { p, t }; }
|
||||
|
||||
async function main() {
|
||||
if (!process.argv.includes('--yes')) { console.log('Re-run with --yes to broadcast on chipnet.'); return; }
|
||||
const art = JSON.parse(readFileSync(ARTIFACT, 'utf8'));
|
||||
const C = await TestNetWallet.fromWIF(JSON.parse(readFileSync(KEYS.c, 'utf8')).wif);
|
||||
const B = await TestNetWallet.fromWIF(JSON.parse(readFileSync(KEYS.b, 'utf8')).wif);
|
||||
const provider = new ElectrumNetworkProvider('chipnet');
|
||||
const sigC = new SignatureTemplate(C.privateKey);
|
||||
const sigB = new SignatureTemplate(B.privateKey);
|
||||
let st = load();
|
||||
|
||||
const ftBal = async (w, cat) => (await provider.getUtxos(w.cashaddr)).filter((x) => x.token?.category === cat && !x.token?.nft).reduce((s, x) => s + BigInt(x.token.amount), 0n);
|
||||
const ftUtxo = async (w, cat, min) => (await provider.getUtxos(w.cashaddr)).find((x) => x.token?.category === cat && !x.token?.nft && BigInt(x.token?.amount) >= min);
|
||||
async function bch(w, sig, need) { const u = (await provider.getUtxos(w.cashaddr)).filter((x) => !x.token).sort((a, b) => Number(b.satoshis) - Number(a.satoshis)); const { p, t } = pickBch(u, need); if (t < need) throw new Error(`${w.cashaddr} low BCH ${t} < ${need}`); return { p, t, sig }; }
|
||||
async function eligible(w) {
|
||||
let u = await w.getUtxos(); let e = u.filter((x) => x.vout === 0 && !x.token); if (e.length) return e[0];
|
||||
const bad = u.filter((x) => !(x.vout === 0 && !x.token));
|
||||
await w.send([{ cashaddr: w.cashaddr, value: 2000n, unit: 'sat' }], bad.length ? { utxoIds: bad } : undefined); await sleep(12000);
|
||||
u = await w.getUtxos(); e = u.filter((x) => x.vout === 0 && !x.token); if (!e.length) throw new Error('splitter failed'); return e[0];
|
||||
}
|
||||
// Category derived from the freshly created FT — never the utxoIds hint ([[mainnet-js-chipnet-gotchas]]).
|
||||
async function genesisFt(label, supply) {
|
||||
const before = new Set((await provider.getUtxos(C.cashaddr)).filter((x) => x.token).map((x) => x.token.category));
|
||||
const u = await eligible(C);
|
||||
const r = await C.tokenGenesis({ cashaddr: C.tokenaddr, amount: supply, value: 1000n }, [], { utxoIds: [u] });
|
||||
await sleep(10000);
|
||||
const fresh = (await provider.getUtxos(C.cashaddr)).find((x) => x.token && !x.token.nft && BigInt(x.token.amount) === supply && !before.has(x.token.category));
|
||||
if (!fresh) throw new Error(`${label} genesis: fresh category not found (tx ${txidOf(r)})`);
|
||||
console.log(` ${label} genesis ${txidOf(r)} — category ${fresh.token.category}`);
|
||||
return fresh.token.category;
|
||||
}
|
||||
|
||||
// ---- 1. genesis X + Y + LP; fund the trader ----
|
||||
step('1. Genesis X + Y + LP FT categories (C); 2,000 X → B');
|
||||
if (!st.x) { st.x = await genesisFt('X', FT_SUPPLY); save(st); } else console.log(` X ${st.x}`);
|
||||
if (!st.y) { st.y = await genesisFt('Y', FT_SUPPLY); save(st); } else console.log(` Y ${st.y}`);
|
||||
if (!st.lp) { st.lp = await genesisFt('LP', LP_SUPPLY); save(st); } else console.log(` LP ${st.lp}`);
|
||||
const xCat = st.x, yCat = st.y, lpCat = st.lp;
|
||||
|
||||
if (st.baseBX == null) { st.baseBX = (await ftBal(B, xCat)).toString(); st.baseBY = (await ftBal(B, yCat)).toString(); save(st); }
|
||||
|
||||
if (!st.fundB) {
|
||||
const cx = await ftUtxo(C, xCat, 2_000n); if (!cx) throw new Error('fundB: C X missing');
|
||||
const { p, t } = await bch(C, sigC, MF + DUST);
|
||||
const tb = new TransactionBuilder({ provider });
|
||||
tb.addInput(cx, sigC.unlockP2PKH()); for (const u of p) tb.addInput(u, sigC.unlockP2PKH());
|
||||
tb.addOutput({ to: B.tokenaddr, amount: DUST, token: { amount: 2_000n, category: xCat } });
|
||||
tb.addOutput({ to: C.tokenaddr, amount: DUST, token: { amount: BigInt(cx.token.amount) - 2_000n, category: xCat } });
|
||||
tb.addOutput({ to: C.cashaddr, amount: BigInt(cx.satoshis) + t - 2n * DUST - MF });
|
||||
st.fundB = txidOf(await tb.send()); save(st); console.log(` 2,000 X → B — ${st.fundB}`); await sleep(10000);
|
||||
} else console.log(` (funded ${st.fundB})`);
|
||||
|
||||
const pool = new Contract(art, [txidLE(xCat), txidLE(yCat), txidLE(lpCat), LP_SUPPLY], { provider });
|
||||
console.log(` pool ${pool.address}`);
|
||||
const legOf = async (cat) => (await provider.getUtxos(pool.address)).find((u) => u.token?.category === cat);
|
||||
|
||||
// ---- 2. bootstrap placement ----
|
||||
step('2. BOOTSTRAP: 10,000 X + 10,000 Y + 990,000 LP treasury → pool (C keeps 10,000 LP)');
|
||||
if (!st.bootstrap) {
|
||||
const cx = await ftUtxo(C, xCat, X0); const cy = await ftUtxo(C, yCat, Y0); const cl = await ftUtxo(C, lpCat, LP_SUPPLY);
|
||||
if (!cx || !cy || !cl) throw new Error('bootstrap: C inventory missing');
|
||||
const { p, t } = await bch(C, sigC, MF + 4n * DUST);
|
||||
const tb = new TransactionBuilder({ provider });
|
||||
tb.addInput(cx, sigC.unlockP2PKH()); tb.addInput(cy, sigC.unlockP2PKH()); tb.addInput(cl, sigC.unlockP2PKH());
|
||||
for (const u of p) tb.addInput(u, sigC.unlockP2PKH());
|
||||
tb.addOutput({ to: pool.tokenAddress, amount: DUST, token: { amount: X0, category: xCat } }); // [0] X reserve
|
||||
tb.addOutput({ to: pool.tokenAddress, amount: DUST, token: { amount: Y0, category: yCat } }); // [1] Y reserve
|
||||
tb.addOutput({ to: pool.tokenAddress, amount: DUST, token: { amount: LP_SUPPLY - L0, category: lpCat } }); // [2] LP treasury
|
||||
tb.addOutput({ to: C.tokenaddr, amount: DUST, token: { amount: L0, category: lpCat } }); // [3] circulating LP
|
||||
tb.addOutput({ to: C.tokenaddr, amount: DUST, token: { amount: BigInt(cx.token.amount) - X0, category: xCat } });
|
||||
tb.addOutput({ to: C.tokenaddr, amount: DUST, token: { amount: BigInt(cy.token.amount) - Y0, category: yCat } });
|
||||
tb.addOutput({ to: C.cashaddr, amount: BigInt(cx.satoshis) + BigInt(cy.satoshis) + BigInt(cl.satoshis) + t - 6n * DUST - MF });
|
||||
st.bootstrap = txidOf(await tb.send()); save(st); console.log(` bootstrapped — ${st.bootstrap}`); await sleep(10000);
|
||||
} else console.log(` (bootstrapped ${st.bootstrap})`);
|
||||
|
||||
// ---- 3. B swaps 1,000 X → 906 Y ----
|
||||
step('3. B SWAPS 1,000 X → 906 Y (0.30% fee stays in the pool)');
|
||||
if (!st.swap) {
|
||||
const xLeg = await legOf(xCat); const yLeg = await legOf(yCat);
|
||||
if (!xLeg || !yLeg) throw new Error('swap: pool legs missing');
|
||||
const bx = await ftUtxo(B, xCat, SWAP_IN); if (!bx) throw new Error('swap: B X missing');
|
||||
const { p, t } = await bch(B, sigB, MF + 3n * DUST);
|
||||
const tb = new TransactionBuilder({ provider });
|
||||
tb.addInput(xLeg, pool.unlock.swap());
|
||||
tb.addInput(yLeg, pool.unlock.swap());
|
||||
tb.addInput(bx, sigB.unlockP2PKH()); for (const u of p) tb.addInput(u, sigB.unlockP2PKH());
|
||||
tb.addOutput({ to: pool.tokenAddress, amount: DUST, token: { amount: BigInt(xLeg.token.amount) + SWAP_IN, category: xCat } });
|
||||
tb.addOutput({ to: pool.tokenAddress, amount: DUST, token: { amount: BigInt(yLeg.token.amount) - SWAP_OUT, category: yCat } });
|
||||
tb.addOutput({ to: B.tokenaddr, amount: DUST, token: { amount: SWAP_OUT, category: yCat } });
|
||||
tb.addOutput({ to: B.tokenaddr, amount: DUST, token: { amount: BigInt(bx.token.amount) - SWAP_IN, category: xCat } });
|
||||
tb.addOutput({ to: B.cashaddr, amount: BigInt(bx.satoshis) + t - 4n * DUST - MF });
|
||||
st.swap = txidOf(await tb.send()); save(st); console.log(` swapped — ${st.swap}`); await sleep(10000);
|
||||
} else console.log(` (swapped ${st.swap})`);
|
||||
|
||||
// ---- 4. C adds liquidity → 1,000 LP released ----
|
||||
step('4. C ADDS 1,100 X + 910 Y → treasury releases 1,000 LP');
|
||||
if (!st.add) {
|
||||
const xLeg = await legOf(xCat); const yLeg = await legOf(yCat); const tLeg = await legOf(lpCat);
|
||||
const cx = await ftUtxo(C, xCat, ADD_X); const cy = await ftUtxo(C, yCat, ADD_Y);
|
||||
if (!xLeg || !yLeg || !tLeg || !cx || !cy) throw new Error('add: legs/inventory missing');
|
||||
const { p, t } = await bch(C, sigC, MF + 4n * DUST);
|
||||
const tb = new TransactionBuilder({ provider });
|
||||
tb.addInput(xLeg, pool.unlock.addLiquidity());
|
||||
tb.addInput(yLeg, pool.unlock.addLiquidity());
|
||||
tb.addInput(tLeg, pool.unlock.addLiquidity());
|
||||
tb.addInput(cx, sigC.unlockP2PKH()); tb.addInput(cy, sigC.unlockP2PKH());
|
||||
for (const u of p) tb.addInput(u, sigC.unlockP2PKH());
|
||||
tb.addOutput({ to: pool.tokenAddress, amount: DUST, token: { amount: BigInt(xLeg.token.amount) + ADD_X, category: xCat } });
|
||||
tb.addOutput({ to: pool.tokenAddress, amount: DUST, token: { amount: BigInt(yLeg.token.amount) + ADD_Y, category: yCat } });
|
||||
tb.addOutput({ to: pool.tokenAddress, amount: DUST, token: { amount: BigInt(tLeg.token.amount) - ADD_LP, category: lpCat } });
|
||||
tb.addOutput({ to: C.tokenaddr, amount: DUST, token: { amount: ADD_LP, category: lpCat } });
|
||||
tb.addOutput({ to: C.tokenaddr, amount: DUST, token: { amount: BigInt(cx.token.amount) - ADD_X, category: xCat } });
|
||||
tb.addOutput({ to: C.tokenaddr, amount: DUST, token: { amount: BigInt(cy.token.amount) - ADD_Y, category: yCat } });
|
||||
tb.addOutput({ to: C.cashaddr, amount: BigInt(cx.satoshis) + BigInt(cy.satoshis) + t - 6n * DUST - MF });
|
||||
st.add = txidOf(await tb.send()); save(st); console.log(` added — ${st.add}`); await sleep(10000);
|
||||
} else console.log(` (added ${st.add})`);
|
||||
|
||||
// ---- 5. C removes 1,000 LP → 1,100 X + 909 Y (fees included) ----
|
||||
step('5. C REMOVES 1,000 LP → 1,100 X + 909 Y (pro-rata INCLUDING the swap fee)');
|
||||
if (!st.remove) {
|
||||
const xLeg = await legOf(xCat); const yLeg = await legOf(yCat); const tLeg = await legOf(lpCat);
|
||||
const cl = await ftUtxo(C, lpCat, REM_LP);
|
||||
if (!xLeg || !yLeg || !tLeg || !cl) throw new Error('remove: legs/LP missing');
|
||||
const { p, t } = await bch(C, sigC, MF + 4n * DUST);
|
||||
const tb = new TransactionBuilder({ provider });
|
||||
tb.addInput(xLeg, pool.unlock.removeLiquidity());
|
||||
tb.addInput(yLeg, pool.unlock.removeLiquidity());
|
||||
tb.addInput(tLeg, pool.unlock.removeLiquidity());
|
||||
tb.addInput(cl, sigC.unlockP2PKH());
|
||||
for (const u of p) tb.addInput(u, sigC.unlockP2PKH());
|
||||
tb.addOutput({ to: pool.tokenAddress, amount: DUST, token: { amount: BigInt(xLeg.token.amount) - REM_X, category: xCat } });
|
||||
tb.addOutput({ to: pool.tokenAddress, amount: DUST, token: { amount: BigInt(yLeg.token.amount) - REM_Y, category: yCat } });
|
||||
tb.addOutput({ to: pool.tokenAddress, amount: DUST, token: { amount: BigInt(tLeg.token.amount) + REM_LP, category: lpCat } });
|
||||
tb.addOutput({ to: C.tokenaddr, amount: DUST, token: { amount: REM_X, category: xCat } });
|
||||
tb.addOutput({ to: C.tokenaddr, amount: DUST, token: { amount: REM_Y, category: yCat } });
|
||||
if (BigInt(cl.token.amount) > REM_LP) tb.addOutput({ to: C.tokenaddr, amount: DUST, token: { amount: BigInt(cl.token.amount) - REM_LP, category: lpCat } });
|
||||
tb.addOutput({ to: C.cashaddr, amount: BigInt(cl.satoshis) + t - 6n * DUST - MF });
|
||||
st.remove = txidOf(await tb.send()); save(st); console.log(` removed — ${st.remove}`); await sleep(10000);
|
||||
} else console.log(` (removed ${st.remove})`);
|
||||
|
||||
// ---- final assertions ----
|
||||
step('FINAL — pool state + deltas prove the loop');
|
||||
const px = BigInt((await legOf(xCat))?.token.amount ?? 0);
|
||||
const py = BigInt((await legOf(yCat))?.token.amount ?? 0);
|
||||
const pt = BigInt((await legOf(lpCat))?.token.amount ?? 0);
|
||||
const bX = (await ftBal(B, xCat)) - BigInt(st.baseBX ?? 0);
|
||||
const bY = (await ftBal(B, yCat)) - BigInt(st.baseBY ?? 0);
|
||||
console.log(` pool X = ${px} (expect 11,000 = 10,000 + 1,100 add − 1,100 remove + 1,000 swap-in)`);
|
||||
console.log(` pool Y = ${py} (expect 9,095 = 10,000 − 906 + 910 − 909)`);
|
||||
console.log(` pool LP tsy = ${pt} (expect 990,000 — add release + remove absorb net 0)`);
|
||||
console.log(` B X Δ = ${bX} (expect 1,000 = 2,000 funded − 1,000 swapped)`);
|
||||
console.log(` B Y Δ = ${bY} (expect 906 — the swap proceeds)`);
|
||||
const pass = px === 11_000n && py === 9_095n && pt === LP_SUPPLY - L0 && bX === 1_000n && bY === 906n;
|
||||
divider();
|
||||
console.log(pass ? 'PASS — 5/5: bootstrap / fee-swap / LP release / LP burn with fee accrual, live on chipnet'
|
||||
: 'FAIL — inspect state above');
|
||||
divider();
|
||||
if (!pass) process.exit(1);
|
||||
}
|
||||
|
||||
main().catch((e) => { console.error(e); process.exit(1); });
|
||||
|
|
@ -131,5 +131,44 @@ export function createBook(provider, opts = {}) {
|
|||
const openOrders = (makerPkh) => Object.values(orders).filter((o) => o.status === 'open' && (!makerPkh || o.params.makerPkh === makerPkh));
|
||||
const tradeLog = (baseCategory) => trades.filter((t) => !baseCategory || t.base === baseCategory);
|
||||
|
||||
return { announce, refresh, book, openOrders, trades: tradeLog, contractOf };
|
||||
/**
|
||||
* The pool-as-maker path (critical path Step 3): read a live SharePairAmmV2
|
||||
* pool's reserves and render the curve as synthetic resting orders on the SAME
|
||||
* book (maker: 'pool'). Prices are PRE-FEE — the 0.30% curve fee is a separate
|
||||
* disclosed line at fill time, same posture as the order-book taker fee.
|
||||
*/
|
||||
async function poolBook(poolAddress, xCategory, yCategory, opts = {}) {
|
||||
const utxos = await provider.getUtxos(poolAddress);
|
||||
const rx = utxos.filter((u) => u.token?.category === xCategory).reduce((a, u) => a + BigInt(u.token.amount), 0n);
|
||||
const ry = utxos.filter((u) => u.token?.category === yCategory).reduce((a, u) => a + BigInt(u.token.amount), 0n);
|
||||
if (rx <= 0n || ry <= 0n) return { asks: [], bids: [], mid: null };
|
||||
return curveLevels(Number(rx), Number(ry), opts);
|
||||
}
|
||||
|
||||
return { announce, refresh, book, openOrders, trades: tradeLog, contractOf, poolBook };
|
||||
}
|
||||
|
||||
/**
|
||||
* Discretize a constant-product curve (reserves rx base, ry quote) into synthetic
|
||||
* book levels: level i sits at mid·(1 ± i·step); its qty is the base amount the
|
||||
* pool trades before the marginal price crosses that band edge (closed form:
|
||||
* cumulative base at price p is |rx − sqrt(k/p)|). Pure display math for the book
|
||||
* view — settlement always re-quotes against the covenant invariant.
|
||||
*/
|
||||
export function curveLevels(rx, ry, { levels = 7, step = 0.002 } = {}) {
|
||||
const k = rx * ry;
|
||||
const mid = ry / rx;
|
||||
const asks = []; const bids = [];
|
||||
let prevAsk = 0; let prevBid = 0;
|
||||
for (let i = 1; i <= levels; i++) {
|
||||
const pa = mid * (1 + i * step);
|
||||
const cumAsk = rx - Math.sqrt(k / pa); // base the pool SELLS up to pa
|
||||
asks.push({ price: pa, qty: Math.max(0, Math.floor(cumAsk - prevAsk)), maker: 'pool' });
|
||||
prevAsk = cumAsk;
|
||||
const pb = mid * (1 - i * step);
|
||||
const cumBid = Math.sqrt(k / pb) - rx; // base the pool BUYS down to pb
|
||||
bids.push({ price: pb, qty: Math.max(0, Math.floor(cumBid - prevBid)), maker: 'pool' });
|
||||
prevBid = cumBid;
|
||||
}
|
||||
return { asks, bids, mid };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,14 @@ const server = createServer(async (req, res) => {
|
|||
if (req.method === 'GET' && url.pathname === '/trades') {
|
||||
return json(res, 200, bookSvc.trades(url.searchParams.get('base') ?? ''));
|
||||
}
|
||||
if (req.method === 'GET' && url.pathname === '/pool') {
|
||||
// The AMM rendered as a maker on the same book: /pool?address=&x=<cat>&y=<cat>
|
||||
return json(res, 200, await bookSvc.poolBook(
|
||||
url.searchParams.get('address') ?? '',
|
||||
url.searchParams.get('x') ?? '',
|
||||
url.searchParams.get('y') ?? '',
|
||||
));
|
||||
}
|
||||
return json(res, 404, { error: 'not found' });
|
||||
} catch (e) {
|
||||
return json(res, 400, { error: e?.message ?? String(e) });
|
||||
|
|
|
|||
82
snapshots/0222-wallet-anthemus-amm-v2-lp-tokens-curve-fee.md
Normal file
82
snapshots/0222-wallet-anthemus-amm-v2-lp-tokens-curve-fee.md
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
---
|
||||
domain: wallet
|
||||
summary: SharePairAmmV2 — fractional LP tokens + 0.30% curve fee validated (4/4 mock, 5/5 live chipnet incl. fee accrual on burn); pool-as-maker levels in the relay; ADR-0049 ratifies the Anthemus business model against real bytecode
|
||||
tags: anthemus, dex, amm, lp-tokens, curve-fee, adr-0049, chipnet
|
||||
---
|
||||
|
||||
# 0222 — SharePairAmmV2: LP tokens + curve fee validated; ADR-0049 ratified
|
||||
|
||||
**Date:** 2026-07-21 · **Branch:** design/dex-p2p-architecture
|
||||
**Critical path:** Step 3 (+ Step 4) of `Parameters/Anthemus_DEX/anthemus_mvp-critical-path.md`.
|
||||
Follows 0220 (order-book engine + relay + the two Agora UIs).
|
||||
|
||||
## What shipped
|
||||
|
||||
### `contracts/SharePairAmmV2.cash` — the production AMM (one coupled change)
|
||||
|
||||
The three interdependent upgrades the business model required (§10 change 1),
|
||||
designed and validated together:
|
||||
|
||||
1. **Fractional LP tokens — reservoir model.** Full `lpSupply` pre-minted into a
|
||||
third covenant UTXO (the LP treasury); **outstanding LP = lpSupply − treasury**
|
||||
— no minting NFT, no commitment state. `addLiquidity` releases LP pro-rata
|
||||
(`lpMint·x ≤ dx·L` on BOTH legs); `removeLiquidity` absorbs LP back and releases
|
||||
a pro-rata slice of both reserves **including accrued fees**. Bootstrap is a
|
||||
genesis *placement* (founder parks reserves + treasury, keeps L0), so
|
||||
`outstanding > 0` is a covenant precondition — no in-covenant sqrt needed.
|
||||
2. **Curve fee 0.30% on input** — Uniswap-V2-style fee-adjusted invariant
|
||||
`(x₂·1000 − dxIn·3)(y₂·1000 − dyIn·3) ≥ x·y·10⁶`, so k grows by the fee every
|
||||
swap and LP tokens are the claim on the grown k. 100% to LPs (ADR-0049 §5).
|
||||
3. **Overflow** — the v1 caveat is closed by the 2025 VM bigints; noted in-source.
|
||||
|
||||
**Safety over the v1 spike (both found during design, both tested):**
|
||||
- `swap()` requires `activeInputIndex ≤ 1` — otherwise the LP **treasury could be
|
||||
unlocked through the swap path and swept** (LP tokens are the claim on all
|
||||
reserves). Negative-tested.
|
||||
- Every pinned input must share the active input's locking bytecode (no
|
||||
substituting attacker UTXOs for a covenant leg), and re-created legs keep ≥ input
|
||||
sats (no dust drain).
|
||||
|
||||
### Gates
|
||||
|
||||
- **`test/regtest/share-pair-amm-v2.test.ts` — 4/4** (real bytecode, libauth VM):
|
||||
fee boundary exact (10,000/10,000 pool, 1,000 in → 906 ok / 907 rejected);
|
||||
LP release boundary exact (1,000 ok / 1,001 rejected); removal pays the fee-grown
|
||||
slice (1,001 X for a 10% burn > the feeless 1,000) and over-withdraw rejected;
|
||||
treasury-via-swap drain rejected.
|
||||
- **`scripts/chipnet-anthemus-amm-v2.mjs` — 5/5 PASS live chipnet**
|
||||
(`npm run chipnet:anthemus-amm-v2`): genesis X/Y/LP → bootstrap 10,000/10,000 +
|
||||
990,000 treasury → B swaps 1,000 X → 906 Y → C adds 1,100/910 → exactly 1,000 LP
|
||||
released → C burns 1,000 LP → 1,100 X + 909 Y back (fees included). Final pool
|
||||
X=11,000 · Y=9,095 · treasury=990,000; all deltas exact.
|
||||
|
||||
### Pool as a maker on the same book
|
||||
|
||||
`services/anthemus-relay/book.mjs` gained `curveLevels(rx, ry)` (closed form:
|
||||
cumulative base at price p = |rx − √(k/p)|) + `poolBook(address, xCat, yCat)` and
|
||||
the server a `GET /pool` route — the AMM rendered as synthetic `maker:'pool'`
|
||||
levels on the SAME book the resting orders live on (the taxonomy's automation
|
||||
ladder, made concrete). Prices are pre-fee; the 0.30% is a separate disclosed line.
|
||||
|
||||
### ADR-0049 — ratified (wiki/decisions.md)
|
||||
|
||||
Written against the validated bytecode per the ADR-0048 discipline: brand locked;
|
||||
book-first engine ratified; ADR-0016 refined (hub-exclusive pools only, native →
|
||||
Cauldron unchanged); **maker 0 / taker 0.15% consensus-enforced** (harder than the
|
||||
model's "start soft"); **0.30% curve fee 100%-to-LP, protocol slice deferred**;
|
||||
bridge `tollBps` accepted-pending-implementation (cross-chain session's surface);
|
||||
USDμ base preferred-not-forced; no mercenary token; List-A ~0%; ADR-0021 "Deviant
|
||||
takes nothing" stands; R-016 counsel before mainnet.
|
||||
|
||||
## Files
|
||||
|
||||
- `contracts/SharePairAmmV2.cash` (+ dist) · `test/regtest/share-pair-amm-v2.test.ts`
|
||||
- `scripts/chipnet-anthemus-amm-v2.mjs` · package script `chipnet:anthemus-amm-v2`
|
||||
- `services/anthemus-relay/book.mjs` (`curveLevels`/`poolBook`) + `server.mjs` (`/pool`)
|
||||
- `wiki/decisions.md` (ADR-0049) · critical-path doc status → rev. 2
|
||||
|
||||
## Next
|
||||
|
||||
- Wire the UI's place/cancel + swap to the covenants (drivers are the reference).
|
||||
- `tollBps` in the wrap covenant (owner: cross-chain session, ADR-0049 §6).
|
||||
- T6 BTC/USD type-2 orders (validated 0184) onto the book.
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
> Duplicate snapshot numbers exist (0018, 0019, 0022, 0027, 0053, 0054, 0057, 0062, 0083, 0084,
|
||||
> 0144, 0145, 0153, 0164) — they are distinct files, disambiguated below by slug.
|
||||
|
||||
_252 snapshots · 252 tagged · 8 domains · regenerate after adding one._
|
||||
_255 snapshots · 255 tagged · 8 domains · regenerate after adding one._
|
||||
|
||||
## Quick-find — all snapshots (Ctrl-F by №, domain, tag, or word)
|
||||
|
||||
|
|
@ -268,8 +268,11 @@ _252 snapshots · 252 tagged · 8 domains · regenerate after adding one._
|
|||
| [0219](0219-cross-chain-pyusd-redeem-roundtrip-closed.md) | cross-chain | uniform-fiat redeem round-trip withdraw release pyusd sepolia solana correspondent aggregate-covenant chipnet real-lock adr-0053 cross-chain | REDEEM ROUND-TRIP CLOSED — the full Uniform USD circle proven with real coins on BOTH PYUSD chains in one run: real lock 2 PYUSD on Sepolia (EVM vault) + 2 on Solana devnet (Token-2022) → chipnet mint 4 unified USD into both cells → covenant pro-rata withdraw burns all 4 (FT back to controller, aggregate → 0) → REAL releases (Sepolia vault.release + Solana rail release) matched to the ledger's largest-first cell debits. 10/10 checks; every balance round-trips; independently re-verified on all three chains. |
|
||||
| [0220](0220-cross-chain-solana-first-class-correspondent-ledger.md) | cross-chain | uniform-fiat correspondent solana adapter memo token-2022 deposit-rail redeem service chipnet-safe pyusd devnet cross-chain | Solana wired into services/correspondent as a FIRST-CLASS ledger: config row PYUSD (Solana) + solana-adapter.mjs (vault = operator ATA, BCH recipient rides an SPL Memo in the lock tx, tx signature IS the lockId, Token-2022 aware) joining evm/tron behind the same server interface. bch.mjs mint operator hardened onto chipnet-safe + wallet D (off corrupted C). Validated by scripts/correspondent-e2e-solana.mjs 7/7 through the REAL service handlers: quote → memo-lock on devnet → confirm (finalization-gated, idempotent) → BCH mint → redeem quote → covenant burn → release on Solana; all txs re-verified independently. |
|
||||
| [0220](0220-wallet-anthemus-dex-mvp-engine-relay-two-uis.md) | wallet | anthemus dex order-book relay agora ui chipnet | Anthemus DEX MVP — AnthemusOrderBook covenant (token↔token + BID + taker fee, 7/7 mock + 8/8 live chipnet), discovery relay, and TWO Agora interfaces (Swap + Exchange) over one engine |
|
||||
| [0221](0221-wallet-uniform-fiat-usd-eur-ui-m6b.md) | wallet | wallet uniform-fiat m6b ui agora usd eur deposit-disclosure swap route-plan honest-label adr-0051 adr-0053 | M6b BUILT (user-directed handover from the UI session): the Uniform Fiat 'USD'/'EUR' surface in Agora — new self-contained Fiat panel (screens/UniformFiat: Hold/Deposit/Swap) implementing the posted WALLET-COORDINATION contract exactly: plain tickers one row per currency, MANDATORY oracle-price deposit disclosure ('USDT is trading at $0.9992 — you will receive 9.99 USD'), 'Swap' copy with the RoutePlan desk·market split rendered as a normal fill, honest label + live transparency (backing/circulating/collateralization + cells as backend detail) and pro-rata withdraw on integrated rails. Typed engine ports router.mjs + ledger semantics; AgoraScreen touch is the minimal 4-line Anthemus pattern. tsc clean · 149/149 · flows driven in-browser. |
|
||||
| [0222](0222-cross-chain-eurc-ethereum-first-real-eur-cell.md) | cross-chain | uniform-fiat eurc euro sepolia real-lock integrated basket-scaling aggregate-covenant chipnet cross-chain adr-0053 | EURC·Ethereum onboarded FOR REAL — the FIRST real EUR cell: a genuine Circle EURC lock on Sepolia (5 EURC, vault 0x5fE18BEa…, tx 0x17f8cc26…) backs 5.00 unified EUR minted on chipnet (cat e9aca38c…) via the aggregate covenant; 4/4 reconciled locked==minted, both legs independently re-verified. Uniform EUR is now real the same way Uniform USD is. Catalog: EURC.Ethereum candidate → integrated (EUR 1 integrated of 4). Basket-scaling audit: Base/Arbitrum/BSC/Fuji hold tokens (EURC·Base 20, USDC·Arbitrum 20) but ZERO gas ETH on the operator address — those graduations are gas-blocked, not code-blocked. |
|
||||
| [0222](0222-wallet-anthemus-amm-v2-lp-tokens-curve-fee.md) | wallet | anthemus dex amm lp-tokens curve-fee adr-0049 chipnet | SharePairAmmV2 — fractional LP tokens + 0.30% curve fee validated (4/4 mock, 5/5 live chipnet incl. fee accrual on burn); pool-as-maker levels in the relay; ADR-0049 ratifies the Anthemus business model against real bytecode |
|
||||
|
||||
## Wallet UI (shells/app) `(56)`
|
||||
## Wallet UI (shells/app) `(58)`
|
||||
|
||||
- [0023-wallet-display-layer](0023-wallet-display-layer.md) — wallet display layer — D-1/D-2/D-4/D-6 surfaced in one structure · _wallet_
|
||||
- [0024-wallet-send-import-d5](0024-wallet-send-import-d5.md) — Phase 2 Step 5 — tier-B send + import flows with D-5 · _wallet_
|
||||
|
|
@ -327,6 +330,8 @@ _252 snapshots · 252 tagged · 8 domains · regenerate after adding one._
|
|||
- [0194-wizardconnect-responder-integration](0194-wizardconnect-responder-integration.md) — WizardConnect responder integration + multi-protocol compatibility tests + Checkers reply · _wizardconnect, checkers_
|
||||
- [0196-wallet-multi-account-keystore-0083](0196-wallet-multi-account-keystore-0083.md) — Multi-account keystore reworked to the 0083 all-in-one model (Bitcoin.com) — HD sub-accounts + independent seed/WIF, shared PIN, unlockAll · _wallet, keystore, multi-wallet, 0083, security_
|
||||
- [0220-wallet-anthemus-dex-mvp-engine-relay-two-uis](0220-wallet-anthemus-dex-mvp-engine-relay-two-uis.md) — Anthemus DEX MVP — AnthemusOrderBook covenant (token↔token + BID + taker fee, 7/7 mock + 8/8 live chipnet), discovery relay, and TWO Agora interfaces (Swap + Exchange) over one engine · _anthemus, dex, order-book, relay, agora, ui, chipnet_
|
||||
- [0221-wallet-uniform-fiat-usd-eur-ui-m6b](0221-wallet-uniform-fiat-usd-eur-ui-m6b.md) — M6b BUILT (user-directed handover from the UI session): the Uniform Fiat 'USD'/'EUR' surface in Agora — new self-contained Fiat panel (screens/UniformFiat: Hold/Deposit/Swap) implementing the posted WALLET-COORDINATION contract exactly: plain tickers one row per currency, MANDATORY oracle-price deposit disclosure ('USDT is trading at $0.9992 — you will receive 9.99 USD'), 'Swap' copy with the RoutePlan desk·market split rendered as a normal fill, honest label + live transparency (backing/circulating/collateralization + cells as backend detail) and pro-rata withdraw on integrated rails. Typed engine ports router.mjs + ledger semantics; AgoraScreen touch is the minimal 4-line Anthemus pattern. tsc clean · 149/149 · flows driven in-browser. · _wallet, uniform-fiat, m6b, ui, agora, usd, eur, deposit-disclosure, swap, route-plan, honest-label, adr-0051, adr-0053_
|
||||
- [0222-wallet-anthemus-amm-v2-lp-tokens-curve-fee](0222-wallet-anthemus-amm-v2-lp-tokens-curve-fee.md) — SharePairAmmV2 — fractional LP tokens + 0.30% curve fee validated (4/4 mock, 5/5 live chipnet incl. fee accrual on burn); pool-as-maker levels in the relay; ADR-0049 ratifies the Anthemus business model against real bytecode · _anthemus, dex, amm, lp-tokens, curve-fee, adr-0049, chipnet_
|
||||
|
||||
## ASM / Orbital Swap / Potidaea / Anthemus `(35)`
|
||||
|
||||
|
|
@ -427,7 +432,7 @@ _252 snapshots · 252 tagged · 8 domains · regenerate after adding one._
|
|||
- [0168-fiatm-exchange-demo-and-adaptive-layer-design](0168-fiatm-exchange-demo-and-adaptive-layer-design.md) — FIATμ Exchange demo + adaptive response layer design · _fiatm_
|
||||
- [0169-real-data-historical-sim-empirical-7b-va](0169-real-data-historical-sim-empirical-7b-va.md) — real-data historical sim — empirical §7b validation + data-source paywall finding
|
||||
|
||||
## Cross-chain / interchain hub / T6 `(41)`
|
||||
## Cross-chain / interchain hub / T6 `(42)`
|
||||
|
||||
- [0170-chipnet-amm-usdm-pusd-and-crosschain-wrap-tusdt](0170-chipnet-amm-usdm-pusd-and-crosschain-wrap-tusdt.md) — chipnet USDm/PUSD AMM + real cross-chain wrap (USDt → τUSDt) · _usdm, cross-chain, crosschain, wrap_
|
||||
- [0171-crosschain-wrap-safeerc20-fix-robustness-naming](0171-crosschain-wrap-safeerc20-fix-robustness-naming.md) — cross-chain wrap hardening: SafeERC20 fix, robustness suite, naming model, source-chain selector · _cross-chain, crosschain, wrap_
|
||||
|
|
@ -470,6 +475,7 @@ _252 snapshots · 252 tagged · 8 domains · regenerate after adding one._
|
|||
- [0217-cross-chain-solana-rail-pyusd-two-chains-real](0217-cross-chain-solana-rail-pyusd-two-chains-real.md) — Solana correspondent rail BUILT (services/correspondent/solana.mjs, Token-2022 aware, websocket-free) + PYUSD·Solana onboarded FOR REAL — a genuine PYUSD lock on Solana devnet (3 PYUSD into an operator vault, real Token-2022 tx finalized) backs 3 unified USD minted on chipnet. Verified both chains. PYUSD is now a TWO-CHAIN issuer with BOTH legs real & integrated (Ethereum 0216 + Solana 0217). USD basket: 4 integrated rails. · _uniform-fiat, solana-rail, correspondent, token-2022, pyusd, real-lock, two-chains, devnet, aggregate-covenant, integrated, adr-0047, cross-chain_
|
||||
- [0219-cross-chain-pyusd-redeem-roundtrip-closed](0219-cross-chain-pyusd-redeem-roundtrip-closed.md) — REDEEM ROUND-TRIP CLOSED — the full Uniform USD circle proven with real coins on BOTH PYUSD chains in one run: real lock 2 PYUSD on Sepolia (EVM vault) + 2 on Solana devnet (Token-2022) → chipnet mint 4 unified USD into both cells → covenant pro-rata withdraw burns all 4 (FT back to controller, aggregate → 0) → REAL releases (Sepolia vault.release + Solana rail release) matched to the ledger's largest-first cell debits. 10/10 checks; every balance round-trips; independently re-verified on all three chains. · _uniform-fiat, redeem, round-trip, withdraw, release, pyusd, sepolia, solana, correspondent, aggregate-covenant, chipnet, real-lock, adr-0053, cross-chain_
|
||||
- [0220-cross-chain-solana-first-class-correspondent-ledger](0220-cross-chain-solana-first-class-correspondent-ledger.md) — Solana wired into services/correspondent as a FIRST-CLASS ledger: config row PYUSD (Solana) + solana-adapter.mjs (vault = operator ATA, BCH recipient rides an SPL Memo in the lock tx, tx signature IS the lockId, Token-2022 aware) joining evm/tron behind the same server interface. bch.mjs mint operator hardened onto chipnet-safe + wallet D (off corrupted C). Validated by scripts/correspondent-e2e-solana.mjs 7/7 through the REAL service handlers: quote → memo-lock on devnet → confirm (finalization-gated, idempotent) → BCH mint → redeem quote → covenant burn → release on Solana; all txs re-verified independently. · _uniform-fiat, correspondent, solana, adapter, memo, token-2022, deposit-rail, redeem, service, chipnet-safe, pyusd, devnet, cross-chain_
|
||||
- [0222-cross-chain-eurc-ethereum-first-real-eur-cell](0222-cross-chain-eurc-ethereum-first-real-eur-cell.md) — EURC·Ethereum onboarded FOR REAL — the FIRST real EUR cell: a genuine Circle EURC lock on Sepolia (5 EURC, vault 0x5fE18BEa…, tx 0x17f8cc26…) backs 5.00 unified EUR minted on chipnet (cat e9aca38c…) via the aggregate covenant; 4/4 reconciled locked==minted, both legs independently re-verified. Uniform EUR is now real the same way Uniform USD is. Catalog: EURC.Ethereum candidate → integrated (EUR 1 integrated of 4). Basket-scaling audit: Base/Arbitrum/BSC/Fuji hold tokens (EURC·Base 20, USDC·Arbitrum 20) but ZERO gas ETH on the operator address — those graduations are gas-blocked, not code-blocked. · _uniform-fiat, eurc, euro, sepolia, real-lock, integrated, basket-scaling, aggregate-covenant, chipnet, cross-chain, adr-0053_
|
||||
|
||||
## Games / Tote / AnyHedge / Pari-mutuel `(13)`
|
||||
|
||||
|
|
|
|||
198
test/regtest/share-pair-amm-v2.test.ts
Normal file
198
test/regtest/share-pair-amm-v2.test.ts
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
/**
|
||||
* SharePairAmmV2 — local covenant evaluation (MockNetworkProvider, Anthemus Step 3).
|
||||
*
|
||||
* The production AMM: fractional LP tokens (reservoir model — outstanding LP =
|
||||
* lpSupply − treasury) + the 0.30% fee-on-input curve invariant + the treasury-drain
|
||||
* guard. Runs the real compiled bytecode on libauth's VM.
|
||||
*
|
||||
* Gate: swap collects the fee into k (exact boundary); addLiquidity releases LP
|
||||
* pro-rata (exact boundary); removeLiquidity pays a pro-rata slice INCLUDING accrued
|
||||
* fees; negatives — over-take swap, over-mint LP, over-withdraw, and unlocking the
|
||||
* treasury through swap().
|
||||
*/
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { Contract, type Artifact, type Utxo } from 'cashscript';
|
||||
import {
|
||||
createRegtestProvider, createTestWallet, fundAddress, tearDown, TransactionBuilder, binToHex,
|
||||
type TestWallet, type RegtestProvider,
|
||||
} from './setup.js';
|
||||
import { txidToConsensusBytes } from '../../src/verify/contract.js';
|
||||
|
||||
const ART = JSON.parse(
|
||||
readFileSync(join(import.meta.dirname, '..', '..', 'contracts', 'dist', 'SharePairAmmV2.json'), 'utf8'),
|
||||
) as Artifact;
|
||||
|
||||
const LP_SUPPLY = 1_000_000n;
|
||||
const X0 = 10_000n, Y0 = 10_000n, L0 = 10_000n; // bootstrap reserves + circulating LP
|
||||
const DUST = 1_000n;
|
||||
|
||||
interface Env {
|
||||
provider: RegtestProvider; contract: Contract;
|
||||
xField: string; yField: string; lpField: string;
|
||||
lp: TestWallet; trader: TestWallet; funder: TestWallet;
|
||||
}
|
||||
|
||||
/** Bootstrap: genesis placement (unconstrained funding) of X/Y reserves + LP treasury. */
|
||||
function makePool(): Env {
|
||||
const provider = createRegtestProvider();
|
||||
const [xField, yField, lpField] = [0, 0, 0].map(() => binToHex(randomBytes(32)));
|
||||
const contract = new Contract(
|
||||
ART,
|
||||
[txidToConsensusBytes(xField), txidToConsensusBytes(yField), txidToConsensusBytes(lpField), LP_SUPPLY],
|
||||
{ provider },
|
||||
);
|
||||
const add = (category: string, amount: bigint) => (provider as any).addUtxo(contract.tokenAddress, {
|
||||
txid: binToHex(randomBytes(32)), vout: 0, satoshis: DUST, token: { amount, category },
|
||||
});
|
||||
add(xField, X0);
|
||||
add(yField, Y0);
|
||||
add(lpField, LP_SUPPLY - L0); // founder circulates L0 LP off-covenant
|
||||
const lp = createTestWallet(); const trader = createTestWallet(); const funder = createTestWallet();
|
||||
return { provider, contract, xField, yField, lpField, lp, trader, funder };
|
||||
}
|
||||
|
||||
const legOf = async (env: Env, field: string): Promise<Utxo> =>
|
||||
(await env.provider.getUtxos(env.contract.tokenAddress)).find((u) => u.token?.category === field)!;
|
||||
const seedFt = (env: Env, w: TestWallet, category: string, amount: bigint): Utxo => {
|
||||
const u: Utxo = { txid: binToHex(randomBytes(32)), vout: 0, satoshis: DUST, token: { amount, category } };
|
||||
(env.provider as any).addUtxo(w.tokenAddress, u);
|
||||
return u;
|
||||
};
|
||||
const bal = async (env: Env, w: TestWallet, field: string): Promise<bigint> =>
|
||||
(await env.provider.getUtxos(w.tokenAddress)).filter((u) => u.token?.category === field)
|
||||
.reduce((a, u) => a + (u.token!.amount as bigint), 0n);
|
||||
|
||||
/** Swap dxIn X for dyOut Y (or reversed) against the two reserve legs. */
|
||||
async function swap(env: Env, opts: { dxIn?: bigint; dyOut?: bigint; dyIn?: bigint; dxOut?: bigint }): Promise<void> {
|
||||
const xLeg = await legOf(env, env.xField);
|
||||
const yLeg = await legOf(env, env.yField);
|
||||
const x = xLeg.token!.amount as bigint, y = yLeg.token!.amount as bigint;
|
||||
const x2 = x + (opts.dxIn ?? 0n) - (opts.dxOut ?? 0n);
|
||||
const y2 = y + (opts.dyIn ?? 0n) - (opts.dyOut ?? 0n);
|
||||
const tb = new TransactionBuilder({ provider: env.provider })
|
||||
.addInput(xLeg, env.contract.unlock.swap())
|
||||
.addInput(yLeg, env.contract.unlock.swap());
|
||||
const inFt = opts.dxIn ? seedFt(env, env.trader, env.xField, opts.dxIn) : seedFt(env, env.trader, env.yField, opts.dyIn!);
|
||||
tb.addInput(inFt, env.trader.signatureTemplate.unlockP2PKH());
|
||||
tb.addInput(fundAddress(env.provider, env.funder.address, 10_000n), env.funder.signatureTemplate.unlockP2PKH());
|
||||
tb.addOutput({ to: env.contract.tokenAddress, amount: DUST, token: { amount: x2, category: env.xField } });
|
||||
tb.addOutput({ to: env.contract.tokenAddress, amount: DUST, token: { amount: y2, category: env.yField } });
|
||||
const outAmt = opts.dyOut ?? opts.dxOut ?? 0n;
|
||||
if (outAmt > 0n) {
|
||||
tb.addOutput({ to: env.trader.tokenAddress, amount: DUST, token: { amount: outAmt, category: opts.dyOut ? env.yField : env.xField } });
|
||||
}
|
||||
tb.addOutput({ to: env.funder.address, amount: 6_000n });
|
||||
await tb.send();
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
test('SWAP: the 0.30% input fee is enforced at the exact boundary (906 ok, 907 rejected)', async () => {
|
||||
const env = makePool();
|
||||
try {
|
||||
// 1,000 X in → net 997 credited → max out = floor(10000·997/10997) = 906.
|
||||
await assert.rejects(swap(env, { dxIn: 1_000n, dyOut: 907n }), 'taking 907 must violate the fee-adjusted invariant');
|
||||
await swap(env, { dxIn: 1_000n, dyOut: 906n });
|
||||
const xLeg = await legOf(env, env.xField);
|
||||
const yLeg = await legOf(env, env.yField);
|
||||
assert.equal(xLeg.token?.amount, 11_000n, 'X reserve grew by the full input (fee stays in the pool)');
|
||||
assert.equal(yLeg.token?.amount, 9_094n, 'Y reserve released 906');
|
||||
assert.equal(await bal(env, env.trader, env.yField), 906n, 'trader received 906 Y');
|
||||
const k0 = X0 * Y0, k1 = 11_000n * 9_094n;
|
||||
assert.ok(k1 > k0, `k grew by the fee: ${k1} > ${k0}`);
|
||||
} finally { tearDown(env.provider); }
|
||||
});
|
||||
|
||||
test('ADD LIQUIDITY: treasury releases LP pro-rata (exact boundary), over-mint rejected', async () => {
|
||||
const env = makePool();
|
||||
try {
|
||||
await swap(env, { dxIn: 1_000n, dyOut: 906n }); // x=11000 y=9094 L=10000
|
||||
const doAdd = async (dx: bigint, dy: bigint, lpMint: bigint) => {
|
||||
const [xLeg, yLeg, tLeg] = [await legOf(env, env.xField), await legOf(env, env.yField), await legOf(env, env.lpField)];
|
||||
const xFt = seedFt(env, env.lp, env.xField, dx);
|
||||
const yFt = seedFt(env, env.lp, env.yField, dy);
|
||||
await new TransactionBuilder({ provider: env.provider })
|
||||
.addInput(xLeg, env.contract.unlock.addLiquidity())
|
||||
.addInput(yLeg, env.contract.unlock.addLiquidity())
|
||||
.addInput(tLeg, env.contract.unlock.addLiquidity())
|
||||
.addInput(xFt, env.lp.signatureTemplate.unlockP2PKH())
|
||||
.addInput(yFt, env.lp.signatureTemplate.unlockP2PKH())
|
||||
.addInput(fundAddress(env.provider, env.funder.address, 10_000n), env.funder.signatureTemplate.unlockP2PKH())
|
||||
.addOutput({ to: env.contract.tokenAddress, amount: DUST, token: { amount: (xLeg.token!.amount as bigint) + dx, category: env.xField } })
|
||||
.addOutput({ to: env.contract.tokenAddress, amount: DUST, token: { amount: (yLeg.token!.amount as bigint) + dy, category: env.yField } })
|
||||
.addOutput({ to: env.contract.tokenAddress, amount: DUST, token: { amount: (tLeg.token!.amount as bigint) - lpMint, category: env.lpField } })
|
||||
.addOutput({ to: env.lp.tokenAddress, amount: DUST, token: { amount: lpMint, category: env.lpField } })
|
||||
.addOutput({ to: env.funder.address, amount: 4_000n })
|
||||
.send();
|
||||
};
|
||||
// lpMint bound: min(1100·10000/11000, 910·10000/9094) = min(1000, 1000.66) = 1000.
|
||||
await assert.rejects(doAdd(1_100n, 910n, 1_001n), 'minting 1001 LP for an 1100/910 deposit must fail');
|
||||
await doAdd(1_100n, 910n, 1_000n);
|
||||
assert.equal(await bal(env, env.lp, env.lpField), 1_000n, 'provider holds 1000 LP');
|
||||
assert.equal((await legOf(env, env.lpField)).token?.amount, LP_SUPPLY - L0 - 1_000n, 'treasury shrank by the mint');
|
||||
assert.equal((await legOf(env, env.xField)).token?.amount, 12_100n);
|
||||
assert.equal((await legOf(env, env.yField)).token?.amount, 10_004n);
|
||||
} finally { tearDown(env.provider); }
|
||||
});
|
||||
|
||||
test('REMOVE LIQUIDITY: pro-rata slice INCLUDES accrued fees; over-withdraw rejected', async () => {
|
||||
const env = makePool();
|
||||
try {
|
||||
// Fee-rich pool: two swaps grow k while L stays 10,000.
|
||||
await swap(env, { dxIn: 1_000n, dyOut: 906n }); // x=11000 y=9094
|
||||
await swap(env, { dyIn: 900n, dxOut: 987n }); // y in: net 897.3 → dxOut ≤ floor(11000·897/9991)=987
|
||||
const xNow = (await legOf(env, env.xField)).token!.amount as bigint; // 10013
|
||||
const yNow = (await legOf(env, env.yField)).token!.amount as bigint; // 9994
|
||||
const doRemove = async (lpBurn: bigint, dx: bigint, dy: bigint) => {
|
||||
const [xLeg, yLeg, tLeg] = [await legOf(env, env.xField), await legOf(env, env.yField), await legOf(env, env.lpField)];
|
||||
const lpFt = seedFt(env, env.lp, env.lpField, lpBurn); // the founder's circulating LP
|
||||
await new TransactionBuilder({ provider: env.provider })
|
||||
.addInput(xLeg, env.contract.unlock.removeLiquidity())
|
||||
.addInput(yLeg, env.contract.unlock.removeLiquidity())
|
||||
.addInput(tLeg, env.contract.unlock.removeLiquidity())
|
||||
.addInput(lpFt, env.lp.signatureTemplate.unlockP2PKH())
|
||||
.addInput(fundAddress(env.provider, env.funder.address, 10_000n), env.funder.signatureTemplate.unlockP2PKH())
|
||||
.addOutput({ to: env.contract.tokenAddress, amount: DUST, token: { amount: (xLeg.token!.amount as bigint) - dx, category: env.xField } })
|
||||
.addOutput({ to: env.contract.tokenAddress, amount: DUST, token: { amount: (yLeg.token!.amount as bigint) - dy, category: env.yField } })
|
||||
.addOutput({ to: env.contract.tokenAddress, amount: DUST, token: { amount: (tLeg.token!.amount as bigint) + lpBurn, category: env.lpField } })
|
||||
.addOutput({ to: env.lp.tokenAddress, amount: DUST, token: { amount: dx, category: env.xField } })
|
||||
.addOutput({ to: env.lp.tokenAddress, amount: DUST, token: { amount: dy, category: env.yField } })
|
||||
.addOutput({ to: env.funder.address, amount: 4_000n })
|
||||
.send();
|
||||
};
|
||||
// Burn 10% of outstanding LP → entitled to 10% of BOTH reserves (fees included).
|
||||
const dxMax = 1_000n * xNow / L0; // 1001 (> the 1000 a feeless pool held)
|
||||
const dyMax = 1_000n * yNow / L0; // 999
|
||||
await assert.rejects(doRemove(1_000n, dxMax + 1n, dyMax), 'withdrawing over the pro-rata slice must fail');
|
||||
await doRemove(1_000n, dxMax, dyMax);
|
||||
assert.equal(await bal(env, env.lp, env.xField), dxMax, `LP received ${dxMax} X`);
|
||||
assert.equal(await bal(env, env.lp, env.yField), dyMax, `LP received ${dyMax} Y`);
|
||||
// Fee accrual, made concrete: 10% of the k-grown pool > 10% of the bootstrap pool.
|
||||
assert.ok(dxMax > 1_000n, 'the X slice exceeds the feeless 1000 — fees accrued to LP');
|
||||
assert.equal((await legOf(env, env.lpField)).token?.amount, LP_SUPPLY - L0 + 1_000n, 'burned LP absorbed into the treasury');
|
||||
} finally { tearDown(env.provider); }
|
||||
});
|
||||
|
||||
test('GUARD: the LP treasury cannot be unlocked through swap()', async () => {
|
||||
const env = makePool();
|
||||
try {
|
||||
const [xLeg, yLeg, tLeg] = [await legOf(env, env.xField), await legOf(env, env.yField), await legOf(env, env.lpField)];
|
||||
// Attacker tries to sweep the treasury while satisfying swap's reserve shape.
|
||||
await assert.rejects(
|
||||
new TransactionBuilder({ provider: env.provider })
|
||||
.addInput(xLeg, env.contract.unlock.swap())
|
||||
.addInput(yLeg, env.contract.unlock.swap())
|
||||
.addInput(tLeg, env.contract.unlock.swap()) // ← the drain attempt
|
||||
.addInput(fundAddress(env.provider, env.trader.address, 10_000n), env.trader.signatureTemplate.unlockP2PKH())
|
||||
.addOutput({ to: env.contract.tokenAddress, amount: DUST, token: { amount: X0, category: env.xField } })
|
||||
.addOutput({ to: env.contract.tokenAddress, amount: DUST, token: { amount: Y0, category: env.yField } })
|
||||
.addOutput({ to: env.trader.tokenAddress, amount: DUST, token: { amount: LP_SUPPLY - L0, category: env.lpField } })
|
||||
.addOutput({ to: env.trader.address, amount: 6_000n })
|
||||
.send(),
|
||||
'unlocking the treasury via swap() must fail activeInputIndex <= 1',
|
||||
);
|
||||
} finally { tearDown(env.provider); }
|
||||
});
|
||||
|
|
@ -2177,3 +2177,80 @@ internal oracle mint. This is the concrete rule behind the Phase-C "no USD-peg l
|
|||
**Cross-references:** ADR-0035 (bridge/oracle trust), ADR-0047 (interchain hub), ADR-0049 (Anthemus
|
||||
business model — where cross-currency swaps settle). Memories: [[correspondent-system-and-reserve-covenant]],
|
||||
[[bch-interchain-settlement-hub]], [[fiatm-oracle-as-signaling-machine]], [[anthemus-business-model]].
|
||||
|
||||
---
|
||||
|
||||
## ADR-0049 — Anthemus business model ratified against validated bytecode: book-first engine, maker-0 consensus taker fee, 0.30% curve fee 100%-to-LP
|
||||
|
||||
**Date:** 2026-07-21 · **Status:** Accepted
|
||||
**Source design:** `Parameters/Anthemus_DEX/anthemus_business-model.md` (rev. 1) ·
|
||||
**Build plan:** `Parameters/Anthemus_DEX/anthemus_mvp-critical-path.md`
|
||||
|
||||
### Context
|
||||
|
||||
The business-model doc (§9) recommended this ADR be written only after the two coupled
|
||||
contract changes were scoped and validated, "so the ADR can reference real bytecode" —
|
||||
the discipline ADR-0048 used. That bar is now met (snapshots 0220, 0222):
|
||||
|
||||
- **`AnthemusOrderBook.cash`** — the generalized resting limit order (token↔token, BID
|
||||
side, consensus-enforced taker-fee output). 7/7 MockNetworkProvider + 8/8 live chipnet
|
||||
(place / announce / discover / partial / full / BID / cancel, exact fee deltas).
|
||||
- **`SharePairAmmV2.cash`** — fractional LP tokens (reservoir treasury; outstanding =
|
||||
supply − treasury), 0.30% fee-on-input invariant so k grows to LPs, treasury-drain
|
||||
guard. 4/4 MockNetworkProvider + 5/5 live chipnet (fee accrual paid out on burn).
|
||||
- **The relay** (`services/anthemus-relay`) — discovery-only; covenant address = order
|
||||
identity; the pool rendered as a maker on the SAME book (`curveLevels`).
|
||||
|
||||
### Decision
|
||||
|
||||
1. **Brand.** "Anthemus" is locked as the user-facing name of the first-party market
|
||||
venue (Agora ▸ Anthemus). Plain "Anthemus" — never "Anthemus Sea".
|
||||
2. **Engine order.** Book-first, ratifying the critical path: the order book is the
|
||||
engine; the AMM is an automated maker on the same book — one matching engine, one
|
||||
settlement path, one fee surface. The swap UI is the exchange with order-making
|
||||
automated.
|
||||
3. **ADR-0016 refined (not reversed).** "No first-party AMM" was about the thin native
|
||||
CashTokens market — that reasoning stands: native Token-class pairs still route to
|
||||
Cauldron at 0% retention. The refinement: **Anthemus pools serve the cross-chain hub
|
||||
pairs Cauldron does not carry** (τUSDt, εUSDC, USDμ legs…), a market that did not
|
||||
exist when 0016 was written. No liquidity fork, no double venue.
|
||||
4. **Book fees.** **Maker 0% / taker `feeBps` (default 15 = 0.15%), consensus-enforced**
|
||||
in the order covenant — the validated bytecode made the fee *hard* at launch, going
|
||||
beyond the model's "start soft, harden later". A sats-quote fee below the 546-sat
|
||||
dust floor is waived by the covenant (small BCH fills are fee-free, not un-fillable).
|
||||
5. **Curve fee.** **0.30% on input, 100% to LPs, on every pair** (option i). The
|
||||
optional ≤0.05% protocol slice on hub-exclusive pairs is **deferred** until depth is
|
||||
proven (per §9.2's lean). LP tokens are the claim on the fee-grown k — validated
|
||||
end-to-end (an LP burn paid out more than the feeless share).
|
||||
6. **Bridge toll.** Direction accepted: 0.15% each way, flat-capped, as a published
|
||||
D-1-verified `tollBps` in the wrap covenant. **Implementation pending** (the wrap
|
||||
covenants are the cross-chain session's surface); until it lands, the toll is not
|
||||
charged — never a hidden haircut.
|
||||
7. **USDμ base.** USDμ is the canonical base/quote; routing through it is **preferred,
|
||||
not forced** (§9.4 lean) — the router favors USDμ legs, the user can trade direct
|
||||
pairs where they exist.
|
||||
8. **No mercenary token.** Confirmed. If LP incentives are ever added they route
|
||||
through the existing flagship DEA, not a new emission.
|
||||
9. **Atomic swap (List A).** ~0% protocol fee confirmed; monetized via the book taker
|
||||
fee + funnel only.
|
||||
10. **Founder-fee split to service provider.** Deviant keeps declining its own
|
||||
consensus slice (ADR-0021 Decision 5 stands); revisit only as an explicit, optional
|
||||
founder election alongside white-label work.
|
||||
11. **Jurisdiction.** R-016-class counsel required before any mainnet/fiat-adjacent
|
||||
pilot of the book + bridge; chipnet remains unrestricted.
|
||||
|
||||
### Consequences
|
||||
|
||||
- The honest-posture test for Anthemus is **disclosure + labeling**: "Market swap"
|
||||
rendering with price impact and each fee as separate disclosed lines (ADR-0016
|
||||
Decision 5) — enforced in the shipped UI, not just documented.
|
||||
- The liquidity cold-start (the model's #1 risk) stays off the MVP critical path: the
|
||||
book works with zero LPs; pools add continuous liquidity per pair as flow justifies.
|
||||
- Next contract work item carrying a fee decision: `tollBps` in the wrap covenant (§6
|
||||
above), owner cross-chain session.
|
||||
|
||||
**Cross-references:** ADR-0008/0016/0017/0020/0021/0030/0044/0045/0047/0048.
|
||||
Contracts: `contracts/AnthemusOrderBook.cash` · `contracts/SharePairAmmV2.cash` ·
|
||||
relay `services/anthemus-relay/`. Snapshots 0220 (MVP: engine+relay+two UIs), 0222
|
||||
(SharePairAmmV2). Memories: [[anthemus-dex-naming]], [[anthemus-business-model]],
|
||||
[[potidaea-oceanic-liquidity]] (why Potidaea copy never applies here).
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue