Clean names that match the Mathematically Backed Asset framing (which came
first; the covenant source files were a later exploratory implementation
whose historical names outlived their usefulness). Rename is bytecode-
preserving — the chipnet-validated 52,998-swap proof carries over unchanged
because cashscript's `contract Name { }` is a source-level identifier that
doesn't enter the compiled script.
Source renames (git mv):
contracts/DeviantOpenMintFT.cash → contracts/CoinMBA.cash (templateId 0x00)
contracts/DeviantShareMBA.cash → contracts/ShareMBA.cash (templateId 0x01)
contracts/DeviantOracleMBA.cash → contracts/OracleMBA.cash (templateId 0x02)
Deleted:
contracts/ArtemisionMintFT.cash (byte-identical duplicate of DeviantOpenMintFT;
"Artemision" lives on only as the wizard UI
concept, not a distinct template)
New:
contracts/DynamicMBA.cash (templateId 0x03 — reserved for the Dynamic
Rate Asset covenant; scaffold only, rejects
every call. DRA is distinct from FIATμ:
FIATμ = dynamic-rate + oracle, DRA = pure
state-driven, no oracle, reflective
against a shared pool)
Compiled artifacts rebuilt via `cashc` and shipped to:
- design/potidaea/src/artifacts/ (live client consumption)
- site/deviant/docs/mba/artifacts/ (public download URLs for third-party
wallet integration; also deployed to
silentmode primary + Sia fallback)
Bytecode verification (sha256-of-bytecode):
CoinMBA == DeviantOpenMintFT ✓ (6a9ae89b...)
ShareMBA == DeviantShareMBA ✓
OracleMBA == DeviantOracleMBA ✓
Client code updates (live surface only — historical scripts, snapshots,
knowledgebase etc. keep their original names since they reference specific
chipnet tx hashes that happened under those names):
- design/potidaea/src/orbital.ts — artifact imports
- design/potidaea/src/genesis.ts — artifact imports + Artemision deprecation note
- design/potidaea/src/registry.ts — artifact imports + dispatch doc + 0x03 note
- design/potidaea/src/publish.ts — TEMPLATE_* constants renamed + 0x03 added
- design/potidaea/src/main.ts — TEMPLATE_* usages renamed
Docs updates:
- site/deviant/docs/mba/index.html — templates table expanded to 4 rows,
DRA vs FIATμ note added, dev-section
code-reference lines updated
- site/deviant/docs/mba/artifacts/index.html — one card per new artifact, DRA
scaffold flagged "not deployable"
Follow-up (not in this commit): the chipnet reseed. Current ASM/ASM2
instances were minted under the DeviantOpenMintFT covenant address and
remain redeemable (bytecode is identical, addresses derive to the same
value). A fresh-start deploy that mints new CoinMBA instances under the
new identity is a separate step requiring real chipnet BCH funding +
Katalogos publish. ASM/ASM2 seed entries stay in orbital.ts SEED_MBAS
until that follow-up ships.
Also deferred: the ~15 experimental MBA-adjacent covenants
(DeviantOpenMintFT_Backdoor, _Escalating, _NoPeg, _Whitelist,
DeviantOracleMBAFloor, _DraFee, DeviantManagedMintFT,
DeviantTieredMintOracleFT, OraclePricedVault, ExpiringMBA,
DeviantPlatformMBA, etc.) stay in contracts/ at their current names —
none are user-facing templates, and their fate (archive, promote,
retire) deserves its own taxonomy session.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
119 lines
6.1 KiB
Text
119 lines
6.1 KiB
Text
pragma cashscript ^0.12.0;
|
|
|
|
// DeviantOpenMintFT — open-mint covenant for a single CashTokens FT category.
|
|
// Per ADR-0006 (native CashTokens + CashScript composite), corrected by
|
|
// the chipnet finding in snapshot 0008 (FT supply is fixed at genesis).
|
|
//
|
|
// Architecture: RESERVOIR MODEL.
|
|
// CashTokens consensus (CHIP-2022-02-CashTokens) fixes FT supply at
|
|
// genesis. A minting NFT lets you create additional NFTs of the
|
|
// category, but NOT additional FTs. So "open mint" here means a
|
|
// permissionless transfer-from-reservoir, not on-demand FT creation.
|
|
//
|
|
// At genesis a single UTXO is parked at the contract address carrying:
|
|
// - the category's entire FT supply (typically 10^18 units, just
|
|
// under the consensus cap 2^63-1)
|
|
// - a minting NFT (capability 0x02) — included so future
|
|
// upgrades/governance can issue new NFTs of the same category if
|
|
// desired; not used by mint() itself
|
|
//
|
|
// Each call to mint() transfers `tokenAmount` FT units from the
|
|
// reservoir to the caller in exchange for `tokenAmount * rate` sats of
|
|
// BCH. The reservoir UTXO is re-output to the contract with its FT
|
|
// amount reduced and the NFT preserved.
|
|
//
|
|
// Self-backed FT UTXOs:
|
|
// Each FT output to a user carries `tokenAmount * backingRateSats` BCH
|
|
// sats inside it. Redemption is therefore a normal P2PKH spend — the
|
|
// holder spends their FT UTXO and consensus releases the embedded BCH.
|
|
// No contract path needed for redeem. The protocol swap consumes a
|
|
// source-token FT UTXO this way and feeds the released BCH into the
|
|
// target-token contract's mint() in the same atomic transaction.
|
|
//
|
|
// Output layout (mint):
|
|
// [0] reservoir UTXO back to contract: minting NFT + (remaining_FT)
|
|
// at dust-or-greater value
|
|
// [1] FT to recipient, self-backed at tokenAmount * rate sats
|
|
// [2] founder fee, if feeSats > 0
|
|
// [3+] BCH change (unconstrained)
|
|
//
|
|
// Category byte order:
|
|
// The `tokenCategory` constructor parameter must be in CashTokens
|
|
// consensus order (little-endian = reverse of the display TXID).
|
|
// Libauth byte-reverses string `category` fields when encoding
|
|
// outputs, so the on-chain field is consensus order; the OP_*TOKEN-
|
|
// CATEGORY opcodes return consensus order; constructor bytes must
|
|
// match. The off-chain scripts handle the reversal.
|
|
//
|
|
// Capability propagation:
|
|
// tx.inputs[i].tokenCategory / tx.outputs[i].tokenCategory carry the
|
|
// 32-byte category id with the capability byte appended (0x02 for
|
|
// minting NFT) and no suffix for FT-only outputs. We propagate the
|
|
// reservoir's full tokenCategory to the reservoir-out output —
|
|
// consensus validates capability preservation. The bare 32-byte
|
|
// constructor `tokenCategory` parameter is compared only against the
|
|
// FT output, which carries no capability suffix.
|
|
//
|
|
// Spec divergence noted:
|
|
// Spec §4.3 illustrative sketch used a `tokenCapability` field (does
|
|
// not exist) and a "mint creates new FT supply" model (CashTokens
|
|
// forbids). This contract is the consensus-correct realisation. See
|
|
// snapshot 0008.
|
|
|
|
contract CoinMBA(
|
|
bytes tokenCategory, // bare 32-byte category id == genesis TXID
|
|
int backingRateSats, // sats per FT unit, >= 1000 per spec §1.3
|
|
int founderFeeBps, // 0..500 (0% .. 5%)
|
|
bytes founderLockingBytecode // 25-byte P2PKH bytecode of fee recipient
|
|
) {
|
|
// claim() — permissionless transfer-from-reservoir.
|
|
//
|
|
// Despite the historical "mint" naming in the CashTokens / spec
|
|
// literature, this function does NOT create new FT supply (that
|
|
// happened once at genesis and is consensus-fixed thereafter).
|
|
// It transfers `tokenAmount` FT units from the reservoir UTXO at
|
|
// this contract to `recipientLockingBytecode`, in exchange for
|
|
// `tokenAmount * backingRateSats` of BCH that the caller provides.
|
|
// Anyone holding BCH can call it.
|
|
function claim(int tokenAmount, bytes recipientLockingBytecode) {
|
|
require(tokenAmount > 0);
|
|
|
|
int grossSats = tokenAmount * backingRateSats;
|
|
int feeSats = grossSats * founderFeeBps / 10000;
|
|
|
|
// [0] reservoir back to this contract. Three invariants:
|
|
// - same lockingBytecode → stays at this contract address
|
|
// - same tokenCategory → preserves NFT category+capability
|
|
// - FT amount reduced by exactly tokenAmount (transferred to [1])
|
|
// Consensus enforces that the FT difference equals the amount
|
|
// appearing in [1]; we double-check by hand here so the contract
|
|
// can give a clear failure message rather than a generic token-
|
|
// validation error.
|
|
require(tx.outputs[0].lockingBytecode == tx.inputs[this.activeInputIndex].lockingBytecode);
|
|
require(tx.outputs[0].tokenCategory == tx.inputs[this.activeInputIndex].tokenCategory);
|
|
require(tx.outputs[0].tokenAmount == tx.inputs[this.activeInputIndex].tokenAmount - tokenAmount);
|
|
require(tx.outputs[0].value >= 546);
|
|
|
|
// [1] FT to recipient, self-backed.
|
|
// tokenCategory on FT outputs has no capability suffix — it is
|
|
// exactly the 32-byte category id we hold as the constructor
|
|
// parameter.
|
|
require(tx.outputs[1].lockingBytecode == recipientLockingBytecode);
|
|
require(tx.outputs[1].tokenCategory == tokenCategory);
|
|
require(tx.outputs[1].tokenAmount == tokenAmount);
|
|
require(tx.outputs[1].value == grossSats);
|
|
|
|
// [2] Founder fee — only constrained when non-zero. If zero, the
|
|
// tx may have additional outputs (change, etc.) starting at
|
|
// index 2 with no contract restriction.
|
|
if (feeSats > 0) {
|
|
// Anti-dust: sub-dust fees are forbidden. Rounding noise must
|
|
// not produce non-standard outputs. Off-chain math must mirror.
|
|
require(feeSats >= 546);
|
|
require(tx.outputs[2].lockingBytecode == founderLockingBytecode);
|
|
require(tx.outputs[2].value == feeSats);
|
|
// Fee output is pure BCH — no token category.
|
|
require(tx.outputs[2].tokenCategory == 0x);
|
|
}
|
|
}
|
|
}
|