From 2b1430ab1ea0ff6b55118362a2e4e160120b2aab Mon Sep 17 00:00:00 2001 From: David J Parrott Date: Wed, 7 Oct 2026 17:42:01 -0400 Subject: [PATCH] Retire reward and reserve creation carriers Require a configurable gross desyndication minimum and retire external staking reward and reserve creation carriers, DClaim reward cursors, treasury-funded LIQ bonuses, and obsolete reserve and underwriting bootstrap settings. Preserve funded launch imports and distribute only supplied WIRE yield. Document pair-scoped incident snapshots and reconciliation, approval and intervention deadlines during freezes, prefunded challengers, and request-keyed return completion or refund. Cover minimum boundaries, unknown retired and undeclared wire discriminants, saturated linked imports, and interventions after approval eligibility. Include rebuilt contract artifacts. Use fresh pre-launch state without compatibility decoders or reserved schema identifiers. Companion bootstrap uses one shared minimum of 1,000,000 base units, equivalent to 0.001 SOL at nine decimals. Change-Id: Ic9e7a85b546c4e72b54884851ffe45f8fe2a3d9e --- OVERVIEW.md | 5 +- .../include/sysio.dclaim/sysio.dclaim.hpp | 97 +------- contracts/sysio.dclaim/src/sysio.dclaim.cpp | 123 +--------- contracts/sysio.dclaim/sysio.dclaim.abi | 98 -------- contracts/sysio.dclaim/sysio.dclaim.wasm | Bin 27531 -> 19820 bytes contracts/sysio.epoch/sysio.epoch.wasm | Bin 79006 -> 78983 bytes contracts/sysio.liq/README.md | 20 +- .../sysio.liq/include/sysio.liq/sysio.liq.hpp | 37 +-- contracts/sysio.liq/src/sysio.liq.cpp | 45 ---- contracts/sysio.liq/sysio.liq.abi | 56 ----- contracts/sysio.liq/sysio.liq.wasm | Bin 42688 -> 40186 bytes contracts/sysio.msgch/README.md | 4 +- contracts/sysio.msgch/src/sysio.msgch.cpp | 51 +--- contracts/sysio.msgch/sysio.msgch.abi | 8 - contracts/sysio.msgch/sysio.msgch.wasm | Bin 148850 -> 145482 bytes .../include/sysio.opp.common/claimable.hpp | 2 +- .../sysio.opp.common/opp_table_types.hpp | 35 --- .../include/sysio.opp.common/safe_ops.hpp | 2 +- .../include/sysio.synd/sysio.synd.hpp | 8 +- contracts/sysio.synd/src/sysio.synd.cpp | 10 +- contracts/sysio.synd/sysio.synd.abi | 8 + contracts/sysio.synd/sysio.synd.wasm | Bin 103713 -> 103962 bytes contracts/sysio.system/EMISSIONS.md | 43 +--- .../include/sysio.system/emissions.hpp | 23 +- .../include/sysio.system/sysio.system.hpp | 27 +-- contracts/sysio.system/src/emissions.cpp | 130 ++--------- contracts/sysio.system/sysio.system.abi | 23 -- contracts/sysio.system/sysio.system.wasm | Bin 201491 -> 200474 bytes contracts/tests/DEPOT_FLOW_TESTING.md | 7 +- contracts/tests/depot_flow_coverage.json | 29 +-- contracts/tests/emissions_tests.cpp | 219 ++---------------- contracts/tests/sysio.authex_tests.cpp | 31 ++- contracts/tests/sysio.dclaim_tests.cpp | 162 ++++--------- contracts/tests/sysio.dispatch_tests.cpp | 83 +++---- contracts/tests/sysio.liq_tests.cpp | 120 +++------- contracts/tests/sysio.synd_tests.cpp | 96 +++++++- docs/contract-upgrade-order.md | 16 +- docs/emergency-stop-playbook.md | 158 ++++++++----- docs/platform-bootstrap-config.md | 14 +- docs/sysio-synd.md | 20 +- etc/config/dex/dex-config.dev.json | 39 +--- etc/config/dex/dex-config.launch.example.json | 33 +-- libraries/opp/include/sysio/opp/opp.hpp | 2 - .../sysio/opp/attestations/attestations.proto | 102 +------- .../proto/sysio/opp/bootstrap/bootstrap.proto | 122 +--------- .../opp/proto/sysio/opp/types/types.proto | 7 +- .../test/test_bootstrap_platform_config.cpp | 90 ++----- 47 files changed, 473 insertions(+), 1732 deletions(-) diff --git a/OVERVIEW.md b/OVERVIEW.md index 3f38fc3b21..a27b66d7df 100644 --- a/OVERVIEW.md +++ b/OVERVIEW.md @@ -30,7 +30,7 @@ Envelope (one per outpost per epoch, per direction) - **Epochs**: Time-bounded intervals managed by `sysio.epoch` — one global epoch clock for every chain. Each epoch has one group of batch operators on duty as relayers. - **Envelopes**: Epoch-level containers. Each envelope carries the epoch index and timestamp, its route endpoints, and `previous_envelope_hash` — the digest of the previous envelope on the same (depot, outpost) stream — so each direction forms a hash chain. An envelope is capped at 32,768 bytes on every chain. - **Messages**: Individual cross-chain payloads containing a header (message ID, sequence, checksum, encoding flags) and a payload (an array of attestation entries). -- **Attestations**: Typed data units that represent cross-chain actions — operator rosters, liquidity syndication and desyndication, yield reports, node-owner registrations, staking rewards. +- **Attestations**: Typed data units that represent cross-chain actions — operator rosters, liquidity syndication and desyndication, yield reports and node-owner registrations. ### Chain Addressing @@ -71,7 +71,6 @@ This allows a single unified message format across all supported chains. | **Liquidity Syndication** | `SYNDICATE_LIQ`, `LIQ_YIELD` | outpost → depot | A user syndicating liq tokens into outpost custody; realized custody yield | | **Liquidity Desyndication** | `DESYNDICATE_LIQ` | depot → outpost | Return syndicated liq tokens to a holder on the outpost | | **Node Owners** | `NODE_OWNER_REG` | outpost → depot | Node-owner NFT committed on Ethereum, registering the owner on the depot | -| **Staking** | `STAKING_REWARD` | outpost → depot | Per-staker reward credited to the `sysio.dclaim` claim ledger (validator-staking lifecycle types are reserved but deferred) | Cross-chain swaps routed through outposts, outpost reserves and outpost-side operator collateral have been removed. Swaps are a depot-local AMM (`sysio.swap`) with no outpost participation; operator collateral is held on the depot by `sysio.opreg`. @@ -112,7 +111,7 @@ Holders of a node-owner NFT who commit it on the Ethereum outpost (`NODE_OWNER_R | **sysio.bond** | Generic bonded underwriting of provable statements | | **sysio.liq** | Shadow liq tokens (`LIQETH`, `LIQSOL`) minted against outpost custody, with a WIRE yield index | | **sysio.swap** | Depot-local constant-product AMM; every pair's second leg is WIRE | -| **sysio.dclaim** | WIRE claim ledger (launch import, staking rewards) | +| **sysio.dclaim** | Funded WIRE launch import, account linking and claims | | **sysio.andon** | Emergency stop: one cord freezing depot fund egress | | **sysio.roa** | Resources and node-owner registry | | **sysio.authex** | Links a depot account to its external-chain keys | diff --git a/contracts/sysio.dclaim/include/sysio.dclaim/sysio.dclaim.hpp b/contracts/sysio.dclaim/include/sysio.dclaim/sysio.dclaim.hpp index af96e12269..2bad18cc48 100644 --- a/contracts/sysio.dclaim/include/sysio.dclaim/sysio.dclaim.hpp +++ b/contracts/sysio.dclaim/include/sysio.dclaim/sysio.dclaim.hpp @@ -19,15 +19,8 @@ namespace sysio { /** * @brief sysio.dclaim — depot-side WIRE distribution and claim ledger. * - * Holds the pending-WIRE balances owed to LIQ-token stakers and pre-launch - * pretoken purchasers, and the per-staker WIRE-side leg of the - * `STAKING_REWARD` flow. - * - * Inbound `STAKING_REWARD` attestations route through `sysio.msgch`, which - * dispatches the per-staker body here via `onreward`. The reward arrives - * already WIRE-denominated — native -> WIRE conversion and source-chain - * precision scaling happen outpost-side — so `onreward` simply credits the - * staker's claim ledger. + * Holds funded pre-launch WIRE distribution credits and preserves existing claims. + * External staking rewards no longer create credits or draw from T5. * * Ledgers: * - `pending_claims` — per-Wire-account WIRE owed. `claim` drains a row via @@ -35,10 +28,6 @@ namespace sysio { * - `unmapped_tokens` — per-(chain, native_pubkey) WIRE owed for stakers / * purchasers without a Wire account yet. Completing AuthX linking * inline-calls `linkswept`, which moves the credit into `pending_claims`. - * - `reward_cursors` — per-(chain_code, chain, native_pubkey) high-water - * mark of the last processed source-chain epoch reference. Replays / - * duplicates (`external_epoch_ref <= last`) are rejected at ingest, so no - * per-reward history is retained (roll-up + data-leak safe). * * Credited and imported balances never expire, including balances waiting * for AuthX linking. They remain owed until claimed; there is no expiry @@ -56,10 +45,7 @@ namespace sysio { // Well-known accounts. static constexpr name AUTHEX_ACCOUNT = "sysio.authex"_n; - static constexpr name MSGCH_ACCOUNT = "sysio.msgch"_n; static constexpr name TOKEN_ACCOUNT = "sysio.token"_n; - static constexpr name RESERV_ACCOUNT = "sysio.reserv"_n; - static constexpr name SYSTEM_ACCOUNT = "sysio"_n; /// WIRE token symbol with the system-wide nine-decimal precision. static constexpr symbol WIRE_SYM = opp::wire::asset_symbol; @@ -88,37 +74,6 @@ namespace sysio { opp::types::ChainKind chain, std::vector native_pubkey); - /// Per-staker WIRE-side credit of a `STAKING_REWARD`. Dispatched inline - /// by `sysio.msgch` (the proto body flattened to primitives). Dedupes - /// on the source-chain epoch reference and credits `pending_claims` (if - /// linked) or `unmapped_tokens` (if not). The reward arrives already - /// WIRE-denominated; native -> WIRE conversion is outpost-side. - /// Auth=sysio.msgch. - /// - /// @param chain_code Emitting outpost's chain code (dedupe scope). - /// @param staker_wire_account Staker's Wire account name, or "" when not - /// yet AuthX-linked (then parked by native - /// address until the link sweep). - /// @param reward_chain Source chain (parking + dedupe key). - /// @param staker_native_addr Staker's raw native address (dedupe + - /// parking key; always populated). - /// @param reward_amount Absolute WIRE reward amount in atomic - /// units (already the staker's prorated - /// portion). - /// @param reward_epoch_index WIRE epoch index (informational / audit). - /// @param external_epoch_ref Source-chain epoch reference; monotonic - /// per (chain, staker) — dedupe key. - /// @param share_bps Staker share in bps (informational only). - [[sysio::action]] - void onreward(uint64_t chain_code, - std::string staker_wire_account, - opp::types::ChainKind reward_chain, - std::vector staker_native_addr, - uint64_t reward_amount, - uint32_t reward_epoch_index, - uint64_t external_epoch_ref, - uint32_t share_bps); - /// One row of an import batch: a pre-launch holder's WIRE credit on /// `chain`. `native_address` is the raw on-chain key (20 B for ETH, /// 32 B for Solana). `wire_atomic` is denominated in WIRE's 9-decimal @@ -194,37 +149,7 @@ namespace sysio { sysio::const_mem_fun> >; - /// Per-(chain_code, chain, native_pubkey) dedupe cursor: the highest - /// source-chain epoch reference processed. Anything `<=` is a replay. - struct rwdcur_key { - uint64_t id; - uint64_t primary_key() const { return id; } - SYSLIB_SERIALIZE(rwdcur_key, (id)) - }; - - struct [[sysio::table("rwdcursors")]] reward_cursor { - uint64_t id = 0; - uint64_t chain_code = 0; - opp::types::ChainKind chain = opp::types::ChainKind::CHAIN_KIND_UNKNOWN; - std::vector native_pubkey; - uint64_t last_external_epoch_ref = 0; - - uint64_t primary_key() const { return id; } - - uint128_t by_chaincode_addr() const { - return chaincode_addr_key(chain_code, chain, native_pubkey); - } - - SYSLIB_SERIALIZE(reward_cursor, - (id)(chain_code)(chain)(native_pubkey)(last_external_epoch_ref)) - }; - - using rwdcursors_t = sysio::kv::table<"rwdcursors"_n, rwdcur_key, reward_cursor, - sysio::kv::index<"bychaincode"_n, - sysio::const_mem_fun> - >; - - /// Cap-staking config singleton. + /// Bootstrap import config singleton. struct [[sysio::table("capcfg")]] cap_config { /// One-way flag protecting the bootstrap `importseed` action. bool imported_complete = false; @@ -236,9 +161,8 @@ namespace sysio { /// Monotonic id counters. struct [[sysio::table("capcounters")]] cap_counters { uint64_t next_unmapped_id = 1; - uint64_t next_cursor_id = 1; - SYSLIB_SERIALIZE(cap_counters, (next_unmapped_id)(next_cursor_id)) + SYSLIB_SERIALIZE(cap_counters, (next_unmapped_id)) }; using capcounters_t = sysio::kv::global<"capcounters"_n, cap_counters>; @@ -263,19 +187,6 @@ namespace sysio { return (static_cast(chain) << 64) | prefix; } - /// (chain_code, chain, native address) -> uint128 narrowing key. High 64 - /// bits = chain_code; low 64 = chain (high 32) xored with the first 4 - /// address bytes. - static uint128_t chaincode_addr_key(uint64_t chain_code, - opp::types::ChainKind chain, - const std::vector& addr) { - uint32_t prefix = 0; - const size_t n = addr.size() < sizeof(uint32_t) ? addr.size() : sizeof(uint32_t); - if (n > 0) std::memcpy(&prefix, addr.data(), n); - uint64_t lo = (static_cast(chain) << 32) ^ static_cast(prefix); - return (static_cast(chain_code) << 64) | lo; - } - private: using ChainKind = opp::types::ChainKind; using TokenKind = opp::types::TokenKind; diff --git a/contracts/sysio.dclaim/src/sysio.dclaim.cpp b/contracts/sysio.dclaim/src/sysio.dclaim.cpp index c72417f42b..de305ce1ee 100644 --- a/contracts/sysio.dclaim/src/sysio.dclaim.cpp +++ b/contracts/sysio.dclaim/src/sysio.dclaim.cpp @@ -1,5 +1,4 @@ #include -#include #include #include @@ -18,8 +17,7 @@ constexpr name ram_payer = "sysio"_n; /// Exact-match scan over a uint128 secondary index: `lower_bound` then walk /// while the narrowing key still matches, returning the first row the /// predicate accepts (or `idx.end()`). The uint128 key only narrows; the -/// predicate resolves prefix collisions deterministically. One implementation -/// shared by the unmapped + cursor lookups (no duplicated scan loops). +/// predicate resolves prefix collisions deterministically for unmapped-credit lookups. template auto scan_find(Index& idx, uint128_t key, KeyFn key_of, MatchFn matches) { auto it = idx.lower_bound(key); @@ -42,14 +40,9 @@ uint64_t next_id(name self, Pick pick) { return id; } -/// Non-throwing validation of a string destined for `name(std::string_view)`. Shared with every -/// other OPP inbound handler via `sysio.opp.common/safe_ops.hpp` so the never-throw name domain is -/// defined and audited in exactly one place. - /// Saturating WIRE credit. `asset::operator+=` aborts on overflow past `asset::max_amount` -/// (2^62-1); credit_wire runs inside the never-throw OPP inbound path (via onreward), so cap at -/// the asset maximum rather than abort. A single staker balance approaching 2^62 atomic WIRE units -/// is not reachable in practice — the cap exists purely to preserve the never-throw contract. +/// (2^62-1). AuthX linkswept is reachable through OPP node-owner registration and must not +/// abort that delivery. Both imported and swept balances preserve saturation at the asset maximum. inline void add_wire_capped(asset& balance, const asset& amt) { const int64_t room = asset::max_amount - balance.amount; // balance.amount in [0, max_amount] balance.amount += (amt.amount <= room ? amt.amount : room); @@ -70,16 +63,9 @@ void credit_pending(name self, name wacct, const asset& amt) { } } -/// Credit `amt` WIRE to the staker. Linked (`wacct` set) -> `pending_claims`; -/// otherwise parked in `unmapped_tokens` keyed by (chain, addr). Both ledgers -/// retain unclaimed balances indefinitely. -void credit_wire(name self, name wacct, ChainKind chain, - const std::vector& addr, const asset& amt) { - if (wacct.value != 0) { - credit_pending(self, wacct, amt); - return; - } - +/// Park an imported WIRE credit by native identity until AuthX links it to a claim account. +void credit_unmapped(name self, ChainKind chain, + const std::vector& addr, const asset& amt) { dclaim::unmapped_t unmapped(self); auto idx = unmapped.template get_index<"bychainad"_n>(); auto it = scan_find(idx, dclaim::chain_addr_key(chain, addr), @@ -104,42 +90,6 @@ void credit_wire(name self, name wacct, ChainKind chain, } } -/// Dedupe at ingest. Returns true if `(chain_code, chain, addr)` has not seen -/// `ext_ref` (or any >= it) yet — and advances the cursor. Returns false for a -/// replay / out-of-order duplicate (`ext_ref <= last`). Advancing here (not at -/// conversion time) means a replay is rejected even while an earlier reward is -/// still staged awaiting a quote. -bool cursor_admit(name self, uint64_t chain_code, ChainKind chain, - const std::vector& addr, uint64_t ext_ref) { - dclaim::rwdcursors_t cur(self); - auto idx = cur.template get_index<"bychaincode"_n>(); - auto it = scan_find(idx, dclaim::chaincode_addr_key(chain_code, chain, addr), - [](const auto& r) { return r.by_chaincode_addr(); }, - [&](const auto& r) { - return r.chain_code == chain_code - && r.chain == chain - && r.native_pubkey == addr; - }); - if (it == idx.end()) { - uint64_t id = next_id(self, [](dclaim::cap_counters& c) -> uint64_t& { - return c.next_cursor_id; - }); - cur.emplace(ram_payer, dclaim::rwdcur_key{id}, - dclaim::reward_cursor{ .id = id, - .chain_code = chain_code, - .chain = chain, - .native_pubkey = addr, - .last_external_epoch_ref = ext_ref }); - return true; - } - if (ext_ref <= it->last_external_epoch_ref) return false; - uint64_t rid = it->id; - cur.modify(same_payer, dclaim::rwdcur_key{rid}, [&](auto& r) { - r.last_external_epoch_ref = ext_ref; - }); - return true; -} - } // anonymous namespace // --------------------------------------------------------------------------- @@ -196,61 +146,6 @@ void dclaim::linkswept(name wire_account, ChainKind chain, std::vector nat } } -// --------------------------------------------------------------------------- -// onreward — per-staker WIRE-side credit of a STAKING_REWARD -// --------------------------------------------------------------------------- -void dclaim::onreward(uint64_t chain_code, - std::string staker_wire_account, - opp::types::ChainKind reward_chain, - std::vector staker_native_addr, - uint64_t reward_amount, - uint32_t reward_epoch_index, - uint64_t external_epoch_ref, - uint32_t share_bps) { - require_auth(MSGCH_ACCOUNT); - - // Tolerate degenerate input rather than aborting the inbound OPP envelope - // (the verifier role lives upstream in msgch::evalcons; dclaim trusts but - // must not break the message chain on a malformed row). reward_amount is cross-chain-supplied; - // bound it to the asset range here so an oversized value soft-drops instead of aborting later - // when the WIRE asset is constructed/credited (asset()/operator+= range-check abort). - if (reward_amount == 0 || reward_amount > static_cast(asset::max_amount) || - staker_native_addr.empty()) return; - - // Dedupe at ingest so a replay / out-of-order duplicate is rejected. - if (!cursor_admit(get_self(), chain_code, reward_chain, - staker_native_addr, external_epoch_ref)) { - return; - } - - name wacct; // value 0 == not yet AuthX-linked - // Validate the cross-chain-supplied account string before constructing name(): an invalid - // string is treated as unlinked (credit parked by native address) rather than aborting the - // inbound dispatch via name()'s internal check(). `is_valid_literal` is name's OWN predicate -- - // the one its constructor uses -- so it cannot drift from what the constructor accepts. - if (!staker_wire_account.empty() && sysio::name::is_valid_literal(staker_wire_account)) { - wacct = name(staker_wire_account); - } - - // reward_amount arrives already WIRE-denominated -- native -> WIRE - // conversion and source-chain precision scaling are outpost-side -- so the - // claim ledger is credited directly. - credit_wire(get_self(), wacct, reward_chain, staker_native_addr, - asset{ static_cast(reward_amount), WIRE_SYM }); - - // Pull funding from sysio.system's drainable pool so the dclaim balance - // covers this credit immediately -- a staker can claim in the next block - // rather than waiting for a pay-epoch. sysio.system::fundclaim caps to - // the remaining pool and never throws, preserving the never-throw - // contract for OPP inbound dispatch. - action( - permission_level{ get_self(), "active"_n }, - SYSTEM_ACCOUNT, - "fundclaim"_n, - std::make_tuple(get_self(), static_cast(reward_amount)) - ).send(); -} - // --------------------------------------------------------------------------- // importseed — bootstrap pre-launch holders into unmapped_tokens // --------------------------------------------------------------------------- @@ -268,10 +163,8 @@ void dclaim::importseed(ChainKind chain, std::vector credits) { check(!credit.native_address.empty(), "empty native_address"); if (credit.wire_atomic == 0) continue; - // Pre-launch holders are unlinked by definition -> name{} routes the - // credit to unmapped_tokens, with the same non-expiring upsert path as - // staking rewards (one implementation in credit_wire). - credit_wire(get_self(), name{}, chain, credit.native_address, + // Pre-launch holders remain unmapped until an AuthX link sweeps their credit. + credit_unmapped(get_self(), chain, credit.native_address, asset{ credit.wire_atomic, WIRE_SYM }); } } diff --git a/contracts/sysio.dclaim/sysio.dclaim.abi b/contracts/sysio.dclaim/sysio.dclaim.abi index e3d84170fe..05e7714553 100644 --- a/contracts/sysio.dclaim/sysio.dclaim.abi +++ b/contracts/sysio.dclaim/sysio.dclaim.abi @@ -20,10 +20,6 @@ { "name": "next_unmapped_id", "type": "uint64" - }, - { - "name": "next_cursor_id", - "type": "uint64" } ] }, @@ -88,44 +84,6 @@ } ] }, - { - "name": "onreward", - "base": "", - "fields": [ - { - "name": "chain_code", - "type": "uint64" - }, - { - "name": "staker_wire_account", - "type": "string" - }, - { - "name": "reward_chain", - "type": "ChainKind" - }, - { - "name": "staker_native_addr", - "type": "bytes" - }, - { - "name": "reward_amount", - "type": "uint64" - }, - { - "name": "reward_epoch_index", - "type": "uint32" - }, - { - "name": "external_epoch_ref", - "type": "uint64" - }, - { - "name": "share_bps", - "type": "uint32" - } - ] - }, { "name": "pclaim_key", "base": "", @@ -150,42 +108,6 @@ } ] }, - { - "name": "reward_cursor", - "base": "", - "fields": [ - { - "name": "id", - "type": "uint64" - }, - { - "name": "chain_code", - "type": "uint64" - }, - { - "name": "chain", - "type": "ChainKind" - }, - { - "name": "native_pubkey", - "type": "bytes" - }, - { - "name": "last_external_epoch_ref", - "type": "uint64" - } - ] - }, - { - "name": "rwdcur_key", - "base": "", - "fields": [ - { - "name": "id", - "type": "uint64" - } - ] - }, { "name": "setconfig", "base": "", @@ -245,11 +167,6 @@ "type": "linkswept", "ricardian_contract": "" }, - { - "name": "onreward", - "type": "onreward", - "ricardian_contract": "" - }, { "name": "setconfig", "type": "setconfig", @@ -281,21 +198,6 @@ "key_types": ["uint64"], "table_id": 16291 }, - { - "name": "rwdcursors", - "type": "reward_cursor", - "index_type": "i64", - "key_names": ["id"], - "key_types": ["uint64"], - "table_id": 64468, - "secondary_indexes": [ - { - "name": "bychaincode", - "key_type": "uint128", - "table_id": 60686 - } - ] - }, { "name": "unmapped", "type": "unmapped_token", diff --git a/contracts/sysio.dclaim/sysio.dclaim.wasm b/contracts/sysio.dclaim/sysio.dclaim.wasm index fae4f84a107d1dd4a2ea2a6969754ab41518fff2..c1b9c6294f302065b5724fb4a1464422c29d7f32 100755 GIT binary patch delta 6166 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 E3uei8G5`Po diff --git a/contracts/sysio.epoch/sysio.epoch.wasm b/contracts/sysio.epoch/sysio.epoch.wasm index 18c986d39bbde1543afa95059bba2f869795ad4c..2cfc67510dfb755928bb48d70140563cd3252796 100755 GIT binary patch delta 675 zcmZvaKWGzC9LL}9ch^hmrTDZ)jTL#h7VRNopn4hnQ*&(*Crh!kgF=@sw>Y@y(i~E# z`$Nbi85}Yw9AwebA&VeVK@dttoootXa23SwyECl?hrHk2@Av(@?|pydMbF;o*~tf+ zd)w`8``5^^xA`Hi^BSeUaZyWj_xQ)L7V|CT)LIkSG;c{r6cD3UjzhwhVzaH3KB`cBB3%JHqoRzQ4f zmpoUw;q->0*Pa^+c>yDtj#MhD#VD3y&gEM!y>FdSQdZ~CRm`{Tuot(}>G!Y_=n+7#bTNIUZMmEV? zHrP@ht3$px4w|dBl((gu5wqGPoAy6*_FHygv`$-m&@S*z9~ZIX+1H z=AZFPnLWlW-X;BB7eST`E+5QxneQqq=yD`)t&NM(51>wxO;o9!98NsM>0Oy`wJ8*} zvZk~tlDEMJZy+L^0D|wIguJ6H&xom3R#u%~ky|QAuPIU`pM@&P8xH7oXguRLxsGze zl|4moT_+TB4yh@fs%uhUN1h?V_{YnOp=e1DU@vcn7VK73C6|tL%NpM(DE|nx&erKE zG=zE?Vn~0MD?2mY=(oFg|@U@d#zvj*eAx5H2k0RUM}&BAt@V<>?-RfA0;zaueEw-Mxye$kwu3O zXUXjEWZt|pO68h&bn3nmFtuTH8FTf>=ZusU?qk=526yxCqZe^J6b;D1z}2{qfF #include -#include #include #include #include @@ -61,13 +60,9 @@ namespace sysio { /// treasury the bootstrap drains and the holder of the protocol's pool shares. static constexpr name SYSTEM_ACCOUNT = "sysio"_n; - /// The yield asset. The kicker arrives in it from sysio.system, so every - /// pot is in WIRE and nothing else. + /// The yield asset: every pot holds supplied WIRE proceeds. static constexpr symbol WIRE_SYM = opp::wire::asset_symbol; - /// The kicker at launch: 2% of every yield intake, drawn from T5. - static constexpr uint32_t DEFAULT_KICKER_BPS = 200; - // ----------------------------------------------------------------------- // Deployment and governance // ----------------------------------------------------------------------- @@ -81,16 +76,8 @@ namespace sysio { /// Requires this contract's authority. [[sysio::action]] void create(symbol sym, sysio::slug_name chain_code, sysio::slug_name token_code); - /// Set the kicker, in basis points of each yield intake, for the intakes - /// from now on. Auth=sysio: council proposals execute as it. - [[sysio::action]] void setkicker(uint32_t bps); - - /// Governance: mint `quantity` back to `holder` after its outpost refused - /// a de-syndication (reconciled from the outpost log). Requires this - /// contract's authority. - /// Legacy exceptional supply repair, still requiring this contract's authority. Normal external - /// return recovery must use sysio.synd::refundreturn so request identity and exactly-once handling - /// are enforced. This action is not evidence that an external return was rejected. + /// Privileged supply repair requiring this contract's authority; never recover a return + /// tracked in sysio.synd::returns, which must use request-keyed refundreturn instead. [[sysio::action]] void recredit(name holder, asset quantity); // ----------------------------------------------------------------------- @@ -158,13 +145,8 @@ namespace sysio { [[sysio::action]] void claim(name holder, symbol_code sym); /// Distribute `quantity` WIRE to `target`'s holders: the index advances by /// quantity / supply with the remainder carried and the WIRE is pulled from - /// `from` by inline transfer. The kicker is requested from T5 only when - /// `from` is the swap; any other donation distributes itself alone. + /// `from` by inline transfer. No treasury bonus is added. [[sysio::action]] void addyield(name from, asset quantity, symbol_code target); - /// Fold the kicker `fundclaim` delivered into `sym`'s index: what this - /// contract's WIRE balance now exceeds `base_balance` by, at most - /// `requested`. Inline from `addyield`; this contract's authority. - [[sysio::action]] void addkicker(symbol_code sym, int64_t base_balance, uint64_t requested); // ----------------------------------------------------------------------- // Launch ingestion (privileged caller, epoch-0 bootstrap window) @@ -263,13 +245,6 @@ namespace sysio { return outstanding < range ? range - outstanding : 0; } - struct [[sysio::table("liqconfig")]] liq_config { - uint32_t kicker_bps = DEFAULT_KICKER_BPS; - SYSLIB_SERIALIZE(liq_config, (kicker_bps)) - }; - - using liqconfig_t = kv::global<"liqconfig"_n, liq_config>; - private: using ChainKind = opp::types::ChainKind; using u128 = opp::shadow::u128; @@ -282,8 +257,6 @@ namespace sysio { ChainKind kind_of_chain(sysio::slug_name chain_code) const; /// `sym`'s index now; zero before the first distribution. u128 current_index(symbol_code sym) const; - /// This contract's WIRE balance on sysio.token; zero without a row. - int64_t wire_balance() const; /// The one funnel every balance move goes through: settle `row` at `index`, /// stamp it, then apply `delta`. Nothing else writes `balance`. diff --git a/contracts/sysio.liq/src/sysio.liq.cpp b/contracts/sysio.liq/src/sysio.liq.cpp index 7785dca26c..924619fcc4 100644 --- a/contracts/sysio.liq/src/sysio.liq.cpp +++ b/contracts/sysio.liq/src/sysio.liq.cpp @@ -2,14 +2,11 @@ #include #include #include -#include #include -#include #include #include #include -#include namespace sysio { @@ -82,15 +79,6 @@ void liq::create(symbol sym, sysio::slug_name chain_code, sysio::slug_name token }, "symbol already exists"); } -void liq::setkicker(uint32_t bps) { - require_auth(SYSTEM_ACCOUNT); - check(bps <= opp::amm::BPS_TOTAL, "kicker_bps out of range"); - liqconfig_t config(get_self()); - liq_config cfg = config.get_or_default(liq_config{}); - cfg.kicker_bps = bps; - config.set(cfg, ram_payer); -} - void liq::recredit(name holder, asset quantity) { require_auth(get_self()); check(is_account(holder), "holder account does not exist"); @@ -324,33 +312,6 @@ void liq::addyield(name from, asset quantity, symbol_code target) { distribute(target, static_cast(quantity.amount)); action(active_of(from), TOKEN_ACCOUNT, "transfer"_n, std::make_tuple(from, get_self(), quantity, std::string{ YIELD_MEMO })).send(); - - // The kicker: `kicker_bps` of the intake, requested from T5 -- only on the - // swap's intake, the one that is yield (tickyield's proceeds). A donation - // draws nothing: a near-sole holder could otherwise donate, claim it back - // with the kicker on top, and repeat against the treasury. fundclaim caps - // the draw and never throws, so what actually lands is folded in afterwards - // by addkicker, measured against the balance the pull above will have left. - if (from != SWAP_ACCOUNT) return; - liqconfig_t config(get_self()); - const uint32_t kicker_bps = config.get_or_default(liq_config{}).kicker_bps; - const uint64_t kicker = static_cast( - static_cast(quantity.amount) * kicker_bps / opp::amm::BPS_TOTAL); - if (kicker == 0) return; - const int64_t base_balance = wire_balance() + quantity.amount; - action(active_of(get_self()), SYSTEM_ACCOUNT, "fundclaim"_n, - std::make_tuple(get_self(), static_cast(kicker))).send(); - action(active_of(get_self()), get_self(), "addkicker"_n, - std::make_tuple(target, base_balance, kicker)).send(); -} - -void liq::addkicker(symbol_code sym, int64_t base_balance, uint64_t requested) { - require_auth(get_self()); - const int64_t received = wire_balance() - base_balance; - if (received <= 0) return; // T5 had nothing to give: base yield only - const currency_stats st = stat_of(sym); - if (st.supply.amount <= 0) return; - distribute(sym, std::min(static_cast(received), requested)); } // --------------------------------------------------------------------------- @@ -435,12 +396,6 @@ u128 liq::current_index(symbol_code sym) const { return idx ? idx->index : 0; } -int64_t liq::wire_balance() const { - token::accounts holdings(TOKEN_ACCOUNT, get_self().value); - const auto row = holdings.try_get(token::acct_key{ WIRE_SYM.code().raw() }); - return row ? row->balance.amount : 0; -} - void liq::settle_and_adjust(opp::shadow::account& row, u128 index, const asset& delta) { row.owed_wire = opp::shadow::owed(row, index); // settle before mutate row.index_checkpoint = index; diff --git a/contracts/sysio.liq/sysio.liq.abi b/contracts/sysio.liq/sysio.liq.abi index 2edc2235ed..eae479ea67 100644 --- a/contracts/sysio.liq/sysio.liq.abi +++ b/contracts/sysio.liq/sysio.liq.abi @@ -21,24 +21,6 @@ } ] }, - { - "name": "addkicker", - "base": "", - "fields": [ - { - "name": "sym", - "type": "symbol_code" - }, - { - "name": "base_balance", - "type": "int64" - }, - { - "name": "requested", - "type": "uint64" - } - ] - }, { "name": "addyield", "base": "", @@ -157,16 +139,6 @@ } ] }, - { - "name": "liq_config", - "base": "", - "fields": [ - { - "name": "kicker_bps", - "type": "uint32" - } - ] - }, { "name": "mint", "base": "", @@ -301,16 +273,6 @@ } ] }, - { - "name": "setkicker", - "base": "", - "fields": [ - { - "name": "bps", - "type": "uint32" - } - ] - }, { "name": "settle", "base": "", @@ -381,11 +343,6 @@ } ], "actions": [ - { - "name": "addkicker", - "type": "addkicker", - "ricardian_contract": "" - }, { "name": "addyield", "type": "addyield", @@ -446,11 +403,6 @@ "type": "regliqpool", "ricardian_contract": "" }, - { - "name": "setkicker", - "type": "setkicker", - "ricardian_contract": "" - }, { "name": "settle", "type": "settle", @@ -471,14 +423,6 @@ "key_types": ["name","uint64"], "table_id": 25660 }, - { - "name": "liqconfig", - "type": "liq_config", - "index_type": "i64", - "key_names": ["name"], - "key_types": ["name"], - "table_id": 57729 - }, { "name": "liqpending", "type": "pending_yield", diff --git a/contracts/sysio.liq/sysio.liq.wasm b/contracts/sysio.liq/sysio.liq.wasm index 57c00f23ac6fe3fc13e351990f02b588c1135043..a3871235ca67b5ee33a8dc3bc38379809bc8e107 100755 GIT binary patch delta 5538 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 data, uint64_t chain_code) // and no reserve-ledger reconciliation. The retired type (enum slot 60957) no // longer exists; any stray inbound falls through to the default drop below. - case AttestationType::ATTESTATION_TYPE_STAKING_REWARD: - // Per-staker staking reward -> sysio.dclaim claim ledger. The - // staking-reward path does not deposit back to a reserve (the - // external-pool credit and native -> WIRE conversion are - // outpost-side), so the pre-refactor reserv::onreward leg is dropped and - // reward_amount.amount is forwarded as the WIRE-denominated credit. - { - opp::attestations::StakingReward sr; - auto in = zpp::bits::in{std::span{data.data(), data.size()}, zpp::bits::no_size{}}; - auto rc = in(sr); - if (rc != zpp::bits::errc{}) break; - // WSA-005: the reward-emitting chain (sr.chain_code) must be the proven delivering - // outpost before any claim credit is recorded. - if (!source_chain_binding_ok(chain_code, sr.chain_code, "staking_reward")) break; - // WSA-028: gate the signed reward through the shared fail-closed - // parser. A negative or out-of-range reward is dropped at the - // boundary so it never reaches the claim ledger (dclaim also - // soft-drops oversized values, but every inbound TokenAmount is - // validated uniformly at ingress). Never-throw: break, do not check(). - const std::optional reward_opt = - sysio::opp::safe::to_depot_amount(static_cast(sr.reward_amount.amount)); - if (!reward_opt) break; - const uint64_t reward_raw = *reward_opt; - // staker_wire_account.name is the raw account string (empty => - // staker not yet AuthX-linked, so sysio.dclaim parks by native - // address). staker_native_address carries the chain (kind) and - // the raw address bytes. Credit against the PROVEN chain_code - // (equal to sr.chain_code, enforced above), never the payload's copy. - action( - permission_level{self, "active"_n}, - "sysio.dclaim"_n, "onreward"_n, - std::make_tuple(chain_code, - sr.staker_wire_account.name, - sr.staker_native_address.kind, - sr.staker_native_address.address, - reward_raw, - sr.reward_epoch_index, - sr.external_epoch_ref, - sr.share_bps) - ).send(); - } - break; - case AttestationType::ATTESTATION_TYPE_SYNDICATE_LIQ: carried_syndication_value = dispatch_syndicate_liq(self, data, chain_code, epoch_index, envelope_digest); break; @@ -791,7 +747,6 @@ void dispatch_node_owner_reg(const std::vector& data, uint64_t chain_code) // outpost echoing one inbound is a benign no-op. break; - case AttestationType::ATTESTATION_TYPE_RESERVE_CREATE: case AttestationType::ATTESTATION_TYPE_RESERVE_CREATE_CANCEL: // Retired external reserve lifecycle. break; diff --git a/contracts/sysio.msgch/sysio.msgch.abi b/contracts/sysio.msgch/sysio.msgch.abi index d54a6586cf..b513a2158a 100644 --- a/contracts/sysio.msgch/sysio.msgch.abi +++ b/contracts/sysio.msgch/sysio.msgch.abi @@ -633,10 +633,6 @@ "name": "ATTESTATION_TYPE_NODE_OWNER_REG", "value": 60949 }, - { - "name": "ATTESTATION_TYPE_STAKING_REWARD", - "value": 60950 - }, { "name": "ATTESTATION_TYPE_STAKE_RESULT", "value": 60951 @@ -657,10 +653,6 @@ "name": "ATTESTATION_TYPE_DEPOSIT_REVERT", "value": 60956 }, - { - "name": "ATTESTATION_TYPE_RESERVE_CREATE", - "value": 60958 - }, { "name": "ATTESTATION_TYPE_RESERVE_CREATE_CANCEL", "value": 60959 diff --git a/contracts/sysio.msgch/sysio.msgch.wasm b/contracts/sysio.msgch/sysio.msgch.wasm index f7ab14b0232fa7893a27ed7dadec86fe7021a813..938bb5137aad7e20f3bfc4287c7be252fdb1dfda 100755 GIT binary patch delta 34843 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 delta 39084 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 diff --git a/contracts/sysio.opp.common/include/sysio.opp.common/claimable.hpp b/contracts/sysio.opp.common/include/sysio.opp.common/claimable.hpp index 60f97e6a2e..ef00b486b1 100644 --- a/contracts/sysio.opp.common/include/sysio.opp.common/claimable.hpp +++ b/contracts/sysio.opp.common/include/sysio.opp.common/claimable.hpp @@ -20,7 +20,7 @@ * the recipient later pulls it with an action carrying its own authority. A handler that aborts * then blocks only its own claim. * - * `sysio.dclaim` established this pattern (`onreward` credits `pending_claims`, `claim` pays out); + * `sysio.dclaim` uses this pattern (`linkswept` credits `pending_claims`, `claim` pays out); * these helpers generalize it so `sysio.system` and `sysio.opreg` share one * audited implementation rather than duplicate copies. * diff --git a/contracts/sysio.opp.common/include/sysio.opp.common/opp_table_types.hpp b/contracts/sysio.opp.common/include/sysio.opp.common/opp_table_types.hpp index 5ad2b85b08..fea838cbc1 100644 --- a/contracts/sysio.opp.common/include/sysio.opp.common/opp_table_types.hpp +++ b/contracts/sysio.opp.common/include/sysio.opp.common/opp_table_types.hpp @@ -551,45 +551,10 @@ DataStream& operator>>(DataStream& ds, NodeOwnerReg& t) { return ds >> t.owner_address >> t.token_id >> t.nft_address; } -// StakingReward: the single staker-reward feedback path. `sysio.msgch` -// dispatches the per-staker body to `sysio.dclaim::onreward` (the WIRE-side -// claim ledger); the amount is already WIRE-denominated, so there is no -// reserve leg. Field order mirrors the proto (1..7). -template -DataStream& operator<<(DataStream& ds, const StakingReward& t) { - return ds << t.chain_code << t.staker_wire_account << t.share_bps - << t.reward_epoch_index << t.external_epoch_ref - << t.reward_amount << t.staker_native_address; -} -template -DataStream& operator>>(DataStream& ds, StakingReward& t) { - return ds >> t.chain_code >> t.staker_wire_account >> t.share_bps - >> t.reward_epoch_index >> t.external_epoch_ref - >> t.reward_amount >> t.staker_native_address; -} - // --------------------------------------------------------------------------- // Reserve-flow attestations // --------------------------------------------------------------------------- -// ReserveCreate — reserve identity + custodied amount travel together in -// `external_amount` (a depot-frame `ReserveAmount`); field order mirrors -// the generated struct (proto fields 4,5,7,8,9,10,11,12). -template -DataStream& operator<<(DataStream& ds, const ReserveCreate& t) { - return ds << t.name << t.description - << t.requested_wire_amount << t.connector_weight_bps - << t.creator_addr << t.is_private << t.creator_pub_key - << t.external_amount; -} -template -DataStream& operator>>(DataStream& ds, ReserveCreate& t) { - return ds >> t.name >> t.description - >> t.requested_wire_amount >> t.connector_weight_bps - >> t.creator_addr >> t.is_private >> t.creator_pub_key - >> t.external_amount; -} - template DataStream& operator<<(DataStream& ds, const ReserveCreateCancel& t) { return ds << t.chain_code << t.token_code << t.reserve_code << t.creator_addr; diff --git a/contracts/sysio.opp.common/include/sysio.opp.common/safe_ops.hpp b/contracts/sysio.opp.common/include/sysio.opp.common/safe_ops.hpp index 68cf5b4f50..29437d1deb 100644 --- a/contracts/sysio.opp.common/include/sysio.opp.common/safe_ops.hpp +++ b/contracts/sysio.opp.common/include/sysio.opp.common/safe_ops.hpp @@ -4,7 +4,7 @@ * @brief Non-throwing primitives for the OPP inbound-consensus dispatch path. * * Every OPP contract handler reachable from `sysio.msgch::deliver -> evalcons -> - * dispatch` (createuwreq, rcrdcommit, onreward, the swap-settlement tail, ...) + * dispatch` (syndication intake, yield reporting, node-owner linking, ...) * MUST NOT abort on operator-supplied data: a `check()`/abort there rolls back * the consensus-tipping delivery and stalls epoch advancement chain-wide * (`feedback_opp_handlers_never_throw.md`). The two recurring foot-guns are diff --git a/contracts/sysio.synd/include/sysio.synd/sysio.synd.hpp b/contracts/sysio.synd/include/sysio.synd/sysio.synd.hpp index 75d780109c..ad2b2c4855 100644 --- a/contracts/sysio.synd/include/sysio.synd/sysio.synd.hpp +++ b/contracts/sysio.synd/include/sysio.synd/sysio.synd.hpp @@ -242,10 +242,13 @@ namespace sysio { /// of zero burst, releases no syndication of the pair; and a shadow whose precision is below /// `bond::BOND_INCREMENT_DECIMALS` can never be underwritten, so every envelope of it waits (only /// `dropenv` clears one). Auth=sysio. + /// `min_desyndicate` is a positive gross quantity in shadow base units, at most a nonzero + /// desyndication burst. Every configuration supplies this value explicitly. [[sysio::action]] void setconfig(sysio::slug_name chain_code, sysio::slug_name token_code, uint32_t synd_fee_bps, uint32_t desynd_fee_bps, uint64_t synd_burst, uint64_t synd_refill, uint64_t desynd_burst, uint64_t desynd_refill, - uint32_t window_sec, uint64_t bounty, uint64_t challenge_extra); + uint32_t window_sec, uint64_t bounty, uint64_t challenge_extra, + uint64_t min_desyndicate); /// Run bounded outcome synchronization, FIFO release, and underwriting, in that order. Sync /// visits at most `limit` envelopes; underwriting and release each have `limit` work units and @@ -603,8 +606,9 @@ namespace sysio { uint32_t window_sec = DEFAULT_WINDOW_SEC; ///< challenge window of each request uint64_t bounty = 0; ///< bounty posted on each request, paid from `feepot` uint64_t challenge_extra = 0; ///< charged to a challenger on top of the hold bond + uint64_t min_desyndicate; ///< Positive gross return minimum. SYSLIB_SERIALIZE(synd_config, (chain_code)(token_code)(synd_fee_bps)(desynd_fee_bps)(synd_burst) - (synd_refill)(desynd_burst)(desynd_refill)(window_sec)(bounty)(challenge_extra)) + (synd_refill)(desynd_burst)(desynd_refill)(window_sec)(bounty)(challenge_extra)(min_desyndicate)) }; /// Rules by outpost and token. diff --git a/contracts/sysio.synd/src/sysio.synd.cpp b/contracts/sysio.synd/src/sysio.synd.cpp index d62a4e7dc6..c45e1e5d73 100644 --- a/contracts/sysio.synd/src/sysio.synd.cpp +++ b/contracts/sysio.synd/src/sysio.synd.cpp @@ -110,6 +110,9 @@ constexpr std::string_view request_final_message = constexpr std::string_view charge_range_message = "hold bond plus challenge extra exceeds the asset range"; constexpr std::string_view desynd_unset_message = "desyndication budget is not configured"; constexpr std::string_view desynd_budget_message = "desyndication exceeds the current budget"; +constexpr std::string_view min_desynd_config_message = + "minimum desyndication must be positive and fit the burst and asset range"; +constexpr std::string_view min_desynd_quantity_message = "quantity is below the minimum desyndication"; constexpr std::string_view desynd_net_message = "desyndication net of the fee must be positive"; /// Diagnostic path names of the intake actions. @@ -1140,7 +1143,8 @@ void synd::closeenv(sysio::slug_name chain_code, uint32_t epoch_index, checksum2 void synd::setconfig(sysio::slug_name chain_code, sysio::slug_name token_code, uint32_t synd_fee_bps, uint32_t desynd_fee_bps, uint64_t synd_burst, uint64_t synd_refill, uint64_t desynd_burst, - uint64_t desynd_refill, uint32_t window_sec, uint64_t bounty, uint64_t challenge_extra) { + uint64_t desynd_refill, uint32_t window_sec, uint64_t bounty, uint64_t challenge_extra, + uint64_t min_desyndicate) { require_auth(SYSTEM_ACCOUNT); check(outpost_kind(chain_code).has_value(), not_outpost_message.data()); const auto st = liq::find_stat_by_token(LIQ_ACCOUNT, token_code); @@ -1153,6 +1157,8 @@ void synd::setconfig(sysio::slug_name chain_code, sysio::slug_name token_code, u } syndconfig_t configs(get_self()); + check(min_desyndicate > 0 && min_desyndicate <= max_asset_amount && + (desynd_burst == 0 || min_desyndicate <= desynd_burst), min_desynd_config_message.data()); configs.set(ram_payer, config_key{.chain_code = chain_code.value, .token_code = token_code.value}, synd_config{ .chain_code = chain_code, @@ -1166,6 +1172,7 @@ void synd::setconfig(sysio::slug_name chain_code, sysio::slug_name token_code, u .window_sec = window_sec, .bounty = bounty, .challenge_extra = challenge_extra, + .min_desyndicate = min_desyndicate, }); } @@ -1441,6 +1448,7 @@ void synd::desyndicate(name holder, asset quantity) { .token_code = st.token_code.value}); check(config.has_value(), desynd_unset_message.data()); const uint64_t quantity_units = static_cast(quantity.amount); + check(quantity_units >= config->min_desyndicate, min_desynd_quantity_message.data()); bucket_row bucket = tick_bucket(get_self(), st.chain_code, st.token_code, bucket_direction::DESYNDICATION, config->desynd_burst, config->desynd_refill, epoch::current_epoch_index()); check(quantity_units <= bucket.level, desynd_budget_message.data()); diff --git a/contracts/sysio.synd/sysio.synd.abi b/contracts/sysio.synd/sysio.synd.abi index 8139333a09..bfc9e324ef 100644 --- a/contracts/sysio.synd/sysio.synd.abi +++ b/contracts/sysio.synd/sysio.synd.abi @@ -777,6 +777,10 @@ { "name": "challenge_extra", "type": "uint64" + }, + { + "name": "min_desyndicate", + "type": "uint64" } ] }, @@ -879,6 +883,10 @@ { "name": "challenge_extra", "type": "uint64" + }, + { + "name": "min_desyndicate", + "type": "uint64" } ] }, diff --git a/contracts/sysio.synd/sysio.synd.wasm b/contracts/sysio.synd/sysio.synd.wasm index d7380406ee86515ec5d4c8f98a68e082e1349362..2c0201caa7e96d9d147f8c0ec277a56912255fc8 100755 GIT binary patch delta 8286 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 eVfez!h;z& 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 batch operators (claimpay) |-- capex_bps ------> sysio.ops (pushed) |-- governance_bps -> sysio.gov (pushed) - '-- remainder ------> capital reserve -> sysio.dclaim / sysio.liq (fundclaim) (claim) + '-- remainder ------> retained in sysio treasury ``` ## Emission actions @@ -216,7 +199,6 @@ period_emission | `accrueepoch` | `sysio.epoch` | Accrue this epoch's curve share | | `rcrdbatch` | `sysio.epoch` | Record the batch-operator roster for this accrued epoch | | `payepoch` | `sysio.epoch` | Distribute the period's compute / capex / governance (credits `payclaims`; pushes only the category buckets) | -| `fundclaim` | the recipient: `sysio.dclaim` or `sysio.liq` | Lazy capital drain into the recipient (never-throw) | | `viewnodedist` | read-only | Preview a node owner's claimable amount | | `viewepoch` | read-only | Current treasury / next-emission estimate | | `viewemitcfg` | read-only | Current emission config | @@ -240,4 +222,3 @@ period_emission - Driven inline by `sysio.epoch::advance` (`accrueepoch` + `rcrdbatch` + `payepoch`). - Reads producer eligibility and operator status from `sysio.opreg`. - Reads the canonical epoch duration from `sysio.epoch::epochcfg`. -- Funds `sysio.dclaim` (staking rewards) and `sysio.liq` (the yield kicker) on demand via `fundclaim`. diff --git a/contracts/sysio.system/include/sysio.system/emissions.hpp b/contracts/sysio.system/include/sysio.system/emissions.hpp index 574c2a147e..6210fb4220 100644 --- a/contracts/sysio.system/include/sysio.system/emissions.hpp +++ b/contracts/sysio.system/include/sysio.system/emissions.hpp @@ -104,10 +104,8 @@ struct [[sysio::table("emitcfg"), sysio::contract("sysio.system")]] emission_con int64_t annual_min_emission; // per-year floor // Category splits (basis points). compute + capex + governance must - // sum to <= 10000; the remainder is the implicit capital reserve drained - // lazily by sysio.dclaim::onreward via sysio.system::fundclaim, not at - // payepoch. Sum == 10000 means no implicit reserve (capital draws eat - // future periods' headroom). + // sum to <= 10000; any unallocated remainder stays in the treasury. + // LIQ yield draws no treasury bonus. uint16_t compute_bps; uint16_t capex_bps; uint16_t governance_bps; @@ -260,8 +258,8 @@ using payclaims_t = sysio::kv::table<"payclaims"_n, payclaim_key, pay_claim>; // // The WIRE backing unclaimed rows sits in sysio's token balance but is already owed, so every // gate that spends against that balance must reserve it -- otherwise the treasury double-commits -// and a later `claimpay` fails on overdraw, stranding earned pay. Two readers depend on it: -// `sysio.system::fundclaim` (its balance cap) and `sysio.epoch`'s emissions readiness gate. +// and a later `claimpay` fails on overdraw, stranding earned pay. Treasury withdrawals +// and `sysio.epoch`'s emissions readiness gate reserve this total. // // Held as a maintained counter rather than recomputed by scanning `payclaims`, because the epoch // gate reads it on EVERY advance and an O(rows) scan there would grow with the standby set. Kept @@ -307,12 +305,6 @@ struct [[sysio::table("t5state"), sysio::contract("sysio.system")]] t5_state { // chains see length 0. std::vector batch_group_epochs; - // Cumulative shortfall (WIRE subunits) between requested and actually- - // funded capital draws. fundclaim caps each request at the remaining - // pool; any unfunded delta is added here so under-sized pools are - // visible without breaking the OPP-handler never-throw contract. - int64_t capital_shortfall_total = 0; - /// Block slots the open pay period is entitled to, accumulated as each epoch accrues. /// /// The DIVISOR has to be built the same way the POOL is. `pending_emission_amount` above adds @@ -333,7 +325,7 @@ struct [[sysio::table("t5state"), sysio::contract("sysio.system")]] t5_state { (start_time)(epoch_count)(last_epoch_index) (last_epoch_time)(last_epoch_emission)(total_distributed) (pending_emission_amount)(period_start_epoch)(batch_group_epochs) - (capital_shortfall_total)(pending_nominal_slots)) + (pending_nominal_slots)) }; using t5state_t = sysio::kv::global<"t5state"_n, t5_state>; @@ -478,9 +470,8 @@ struct [[sysio::table("epochlog"), sysio::contract("sysio.system")]] epoch_log { uint64_t epoch_count = 0; // internal counter of payepoch invocations sysio::time_point_sec timestamp; // Total budget for the period (curve output). compute + capex + governance - // are paid out of this at payepoch; the implicit capital reserve - // (= total_emission - compute - capex - governance) is drained lazily - // by fundclaim across the same period and never appears here. + // are paid out of this at payepoch; the unallocated remainder stays in + // the treasury and is not a separate payout category. int64_t total_emission = 0; int64_t compute_amount = 0; int64_t capex_amount = 0; diff --git a/contracts/sysio.system/include/sysio.system/sysio.system.hpp b/contracts/sysio.system/include/sysio.system/sysio.system.hpp index 8420c1840b..bbd6430527 100644 --- a/contracts/sysio.system/include/sysio.system/sysio.system.hpp +++ b/contracts/sysio.system/include/sysio.system/sysio.system.hpp @@ -689,8 +689,8 @@ namespace sysiosystem { * The T5 category buckets (`sysio.ops` capex, `sysio.gov` governance) are NOT credited * here — payepoch transfers to them directly. A claim needs `require_auth(account_name)` * and neither can ever produce it: `sysio.roa` forces `net_weight`/`cpu_weight` to zero - * for every `sysio`-prefixed account, so they cannot pay for a transaction, and unlike - * `sysio.dclaim` they carry no contract that could emit the claim inline. They are + * for every `sysio`-prefixed account, so they cannot pay for a transaction. They also + * carry no contract that could emit the claim inline. They are * protocol-owned holding accounts with no code, so the notify-handler threat the pull * model defends against does not exist for them. See the note at the push site in * emissions.cpp for the standing constraint that keeps it that way. @@ -721,7 +721,7 @@ namespace sysiosystem { * `period_emission` is the gate-computed sum of pending accrued * emissions plus this epoch's per-epoch share. payepoch trusts that * value (single-trx semantics make recomputation unnecessary) and - * distributes it across producer / batch / capital / capex / gov + * distributes it across producer / batch / capex / gov * pools as today, scaled to the period. * * `batch_op_groups` is retained as an ABI-compatible reserved field; @@ -779,27 +779,6 @@ namespace sysiosystem { [[sysio::action]] void rcrdbatch(uint32_t epoch_index, std::vector members); - /** - * Fund a capital draw against the T5 drainable pool for `recipient`, - * one of the two drains: sysio.dclaim, inline from its `onreward` as - * each STAKING_REWARD lands, and sysio.liq, inline from its `addyield` - * for the kicker on each yield intake. The recipient is funded the - * moment its ledger row is written, so a claim can follow at once. - * Auth: the recipient. - * - * Never throws for those two callers (OPP-handler never-throw - * contract): if the pool cannot cover `amount`, the transfer caps at - * what's available and the unfunded delta is accrued to - * t5state.capital_shortfall_total for operator visibility. Any other - * recipient is refused. - * - * Amounts actually transferred count toward t5state.total_distributed - * so the emission curve auto-throttles via its remaining-headroom - * clamp. - */ - [[sysio::action]] - void fundclaim(sysio::name recipient, int64_t amount); - /** * Read-only: current T5 treasury emission state. */ diff --git a/contracts/sysio.system/src/emissions.cpp b/contracts/sysio.system/src/emissions.cpp index a6299c2d40..9c04942a36 100644 --- a/contracts/sysio.system/src/emissions.cpp +++ b/contracts/sysio.system/src/emissions.cpp @@ -50,9 +50,6 @@ constexpr int64_t MS_PER_SECOND = 1000; // Basis-point denominator for all category / sub-split ratios. constexpr int64_t BPS_DENOMINATOR = 10000; -constexpr sysio::name CAPITAL_ACCOUNT = "sysio.dclaim"_n; -// The shadow-liq token draws the yield kicker through fundclaim beside sysio.dclaim. -constexpr sysio::name LIQ_ACCOUNT = "sysio.liq"_n; constexpr sysio::name GOVERNANCE_ACCOUNT = "sysio.gov"_n; // Capex ("capital expenditure") bucket lives on sysio.ops -- operational spend. constexpr sysio::name CAPEX_OPERATIONS_ACCOUNT = "sysio.ops"_n; @@ -60,7 +57,6 @@ constexpr sysio::name TOKEN_CONTRACT = "sysio.token"_n; constexpr sysio::name ROA_CONTRACT = "sysio.roa"_n; namespace memo { - constexpr std::string_view capital = "T5 capital"; constexpr std::string_view capex = "T5 capex"; constexpr std::string_view governance = "T5 governance"; constexpr std::string_view batch_op_reward = "T5 batch operator reward"; @@ -152,19 +148,14 @@ int64_t get_wire_balance(name account) { // account the protocol does not control therefore hands it an abort switch over every parent // inline action. // -// THREE call sites remain, and every one targets a PROTOCOL-CONTROLLED account. What makes each -// safe differs, and the difference is the thing to preserve: +// Two automatic call sites target protocol-controlled holding accounts with no deployed code: // -// * `fundclaim` -> `sysio.dclaim` — DEPLOYED contracts, not bare accounts. Safe -// `sysio.liq` because both are protocol-controlled and neither -// has a failing `sysio.token::transfer` notify -// path; that invariant must hold as they evolve. // * `payepoch` (capex) -> `sysio.ops` — no code deployed // * `payepoch` (governance) -> `sysio.gov` — no code deployed // // The two `payepoch` pushes are on the never-throw `advance` path, so those two accounts are the // ones that could abort epoch advancement if code were ever deployed on them — see the standing -// constraint at that call site. Adding a fourth AUTOMATIC destination outside protocol control +// constraint at that call site. Adding another AUTOMATIC destination outside protocol control // reopens the vulnerability this file exists to close. // // The scope is automatic payouts on never-throw paths. A claimant-authorized action MAY transfer @@ -206,7 +197,7 @@ void credit_pay(name self, name to, int64_t amount, std::string_view /*memo_str* sysio::opp::claimable::credit(claims, self, payclaim_key{to.value}, pay_claim{.account_name = to}, amt); - // Reserve the credited WIRE so fundclaim and the epoch gate cannot re-commit it. Saturates on + // Reserve the credited WIRE so treasury withdrawals and the epoch gate cannot re-commit it. Saturates on // the same cap as the row itself, keeping the counter consistent with the sum of the rows. payclaimtot_t tot_tbl(self); auto tot = tot_tbl.get_or_default(pay_claim_total{}); @@ -271,10 +262,8 @@ void system_contract::setemitcfg(const emissions::emission_config& cfg) { "annual_min_emission must be <= annual_max_emission"); // BPS splits. compute + capex + governance bound what payepoch transfers - // each period; the remainder (10000 - that sum) is the implicit capital - // reserve, drained lazily by sysio.dclaim::onreward via fundclaim. Sum - // exactly 10000 means no implicit reserve (capital draws come out of - // future periods' headroom). Sum > 10000 would over-commit period_emission. + // each period; any remainder stays in the treasury. Sum > 10000 + // would over-commit period_emission. const uint32_t paid_at_payepoch_bps = static_cast(cfg.compute_bps) + static_cast(cfg.capex_bps) @@ -477,7 +466,7 @@ void system_contract::claimnodedis(const sysio::name& account_name) { sysio::check(info.can_claim, "claim amount below minimum threshold"); // Node-owner vesting draws on the SAME `sysio` WIRE balance that backs unclaimed epoch pay, so - // it must respect the same reserve `fundclaim` and the epoch readiness gate hold. `payclaims` + // it must respect the same reserve the epoch readiness gate holds. `payclaims` // rows are already owed: paying them out is a promise this contract has made and cannot revoke, // whereas this withdrawal is a claim on the free remainder. // @@ -522,7 +511,7 @@ void system_contract::claimnodedis(const sysio::name& account_name) { // // The row is erased before the transfer is queued (inside pay_out), so a notify handler that // re-enters claimpay finds no row and cannot double spend. The outstanding-total counter is -// decremented in lockstep, releasing the reserve that fundclaim and the epoch gate hold against it. +// decremented in lockstep, releasing the reserve held against treasury withdrawals and the epoch gate. void system_contract::claimpay(const sysio::name& account_name) { require_auth(account_name); @@ -535,7 +524,7 @@ void system_contract::claimpay(const sysio::name& account_name) { auto tot = tot_tbl.get_or_default(pay_claim_total{}); // Defensive floor: the counter and the row sum are maintained together, so `paid` can only // exceed `outstanding` if they have already diverged. Clamping keeps the reserve from - // underflowing into a huge value that would freeze fundclaim and the epoch gate permanently. + // underflowing into a huge value that would freeze treasury withdrawals and the epoch gate permanently. tot.outstanding = (paid >= tot.outstanding) ? 0 : tot.outstanding - paid; tot_tbl.set(tot, get_self()); } @@ -695,11 +684,8 @@ void system_contract::rcrdbatch(uint32_t epoch_index, std::vector m // - per-epoch emission > 0 (treasury not at floor) // - sysio's WIRE balance >= period_emission (pending + this epoch's share) // -// Capital is NOT paid here. The implicit capital reserve -// (period_emission - compute - capex - governance) stays in sysio's balance -// and is drained lazily by fundclaim as sysio.dclaim::onreward fires, so -// dclaim has funds the moment a claim is credited rather than at the next -// pay-epoch. +// The unallocated remainder (period_emission - compute - capex - governance) +// stays in sysio's balance. LIQ yield draws no treasury bonus. // // Swap-fee rewards: when immutable roster history is complete and contains at // least one non-empty roster, the batch-operator share of collected swap fees @@ -746,10 +732,8 @@ void system_contract::payepoch(uint32_t epoch_index, // ----- Category splits ----- // payepoch transfers compute + capex + governance only. The implicit - // capital reserve (period_emission - compute - capex - governance) stays - // in sysio's balance and is drained lazily by fundclaim as - // sysio.dclaim::onreward fires. Sum-to-10000 BPS means zero reserve; - // sum < 10000 leaves the remainder available for capital coverage. + // remainder (period_emission - compute - capex - governance) stays + // in sysio's balance. Sum-to-10000 BPS means no unallocated remainder. const int64_t compute_amount = split_bps(period_emission, cfg.compute_bps); const int64_t capex_amount = split_bps(period_emission, cfg.capex_bps); const int64_t governance_amount = split_bps(period_emission, cfg.governance_bps); @@ -1082,13 +1066,10 @@ void system_contract::payepoch(uint32_t epoch_index, } // ======================================================================= - // Category buckets: fixed accounts, no opreg filter. Capital is NOT paid - // here -- it drains lazily via fundclaim per incoming OPP claim, so - // dclaim has WIRE the moment the claim is credited rather than waiting - // for the next pay-epoch. + // Category buckets: fixed accounts, no opreg filter. The unallocated + // remainder stays in the treasury; LIQ yield has no capital draw. // ======================================================================= - // PUSHED, not credited -- the deliberate exception to this contract's pull rule, and the same - // exception `fundclaim` already makes for `sysio.dclaim`. + // PUSHED, not credited -- the deliberate exception to this contract's pull rule. // // The pull model exists because a payout to an account the protocol does not control lets that // account's transfer-notify handler abort `advance`. These two are protocol-owned holding @@ -1098,9 +1079,9 @@ void system_contract::payepoch(uint32_t epoch_index, // Crediting them instead would strand the money. A claim needs `require_auth(account_name)`, // and a `sysio.*` account can neither sign nor be acted for here: `sysio.roa` forces // `net_weight`/`cpu_weight` to zero for every account whose prefix is `sysio` - // (`is_sysio_account`), so they cannot pay for a transaction, and unlike `sysio.dclaim` they + // (`is_sysio_account`), so they cannot pay for a transaction. They also // have no contract of their own to emit the claim inline. Their pay would accrue in - // `payclaims` forever while `payclaimtot` reserved the backing WIRE against `fundclaim` and the + // `payclaims` forever while `payclaimtot` reserved the backing WIRE against treasury withdrawals and the // epoch readiness gate -- a permanent, growing reservation of WIRE nobody can move. // // This is a standing constraint on the two accounts, not a reason to revisit this call: any @@ -1170,83 +1151,6 @@ void system_contract::payepoch(uint32_t epoch_index, } } -// fundclaim - transfer up to `amount` WIRE from sysio's drainable pool to -// `recipient`, one of the two capital drains: sysio.dclaim, inline from -// sysio.dclaim::onreward as each STAKING_REWARD attestation lands, and -// sysio.liq, inline from sysio.liq::addyield for the kicker on each yield -// intake. Either way the recipient is funded against the credit it just took -// on before anyone can attempt to claim it. -// -// Never throws for those two callers. STAKING_REWARD dispatch from sysio.msgch -// must not be aborted on emissions-side conditions (the never-throw contract -// for OPP inbound handlers), so a pool-too-small case caps the transfer at -// what's available and accrues the unfunded delta to -// t5state.capital_shortfall_total for operator visibility. A recipient that is -// not a drain is refused, which reaches only the stranger who asked. -// -// The transfer cap is the minimum of three caps that all must hold: -// * `amount` -- requested -// * `lifetime headroom - pending_emission_amount` -- accounting -// * `sysio WIRE balance - pending_emission_amount -// - outstanding payclaims` -- balance -// Both accounting and balance caps reserve `pending_emission_amount` for -// the next payepoch. `pending_emission_amount` is curve emission already -// accrued via accrueepoch but not yet paid; payepoch will distribute -// it across compute/capital/capex/governance. Drawing against those funds -// here would either trip the emissions readiness gate at the next epoch -// boundary (BALANCE_INSUFFICIENT) or leave payepoch's credits unbacked. -// -// The balance cap ALSO reserves outstanding `payclaims`. Those balances have -// already been credited to producers / standbys / batch operators but not yet -// pulled, so the WIRE backing them is still sitting in this account's -// token balance while being fully owed. Spending it here would leave a later -// `claimpay` unable to transfer, stranding earned pay -- the claimable-payout -// equivalent of the "overdrawn balance" abort this cap has always guarded. -// -// Negative or zero requests are silent no-ops (defensive). The amount -// actually transferred counts toward total_distributed -- the curve sees -// less remaining headroom on its next per-epoch computation and emissions -// auto-throttle to match real claim load. -void system_contract::fundclaim(name recipient, int64_t amount) { - require_auth(recipient); - check(recipient == CAPITAL_ACCOUNT || recipient == LIQ_ACCOUNT, - "fundclaim: recipient is not a capital drain"); - - if (amount <= 0) return; - - t5state_t t5s(get_self()); - if (!t5s.exists()) return; // pre-init: silently absorb - auto state = t5s.get(); - - payclaimtot_t claim_tot_tbl(get_self()); - const int64_t claims_reserve = - static_cast(claim_tot_tbl.get_or_default(pay_claim_total{}).outstanding); - - const auto cfg = get_emit_cfg(get_self()); - const int64_t lifetime_headroom = cfg.t5_distributable - cfg.t5_floor - state.total_distributed; - const int64_t pending_reserve = state.pending_emission_amount; - const int64_t sysio_balance = get_wire_balance(get_self()); - const int64_t accounting_available = lifetime_headroom - pending_reserve; - const int64_t balance_available = sysio_balance - pending_reserve - claims_reserve; - - const int64_t cap = std::min({amount, accounting_available, balance_available}); - const int64_t to_transfer = (cap > 0) ? cap : 0; - const int64_t shortfall = amount - to_transfer; - - if (to_transfer > 0) { - send_wire_transfer(get_self(), recipient, to_transfer, memo::capital); - state.total_distributed += to_transfer; - } - - if (shortfall > 0) { - state.capital_shortfall_total += shortfall; - } - - if (to_transfer > 0 || shortfall > 0) { - t5s.set(state, get_self()); - } -} - emissions::epoch_info_result system_contract::viewepoch() { const auto cfg = get_emit_cfg(get_self()); diff --git a/contracts/sysio.system/sysio.system.abi b/contracts/sysio.system/sysio.system.abi index 76c60e37d9..129dcb6a49 100644 --- a/contracts/sysio.system/sysio.system.abi +++ b/contracts/sysio.system/sysio.system.abi @@ -674,20 +674,6 @@ } ] }, - { - "name": "fundclaim", - "base": "", - "fields": [ - { - "name": "recipient", - "type": "name" - }, - { - "name": "amount", - "type": "int64" - } - ] - }, { "name": "getsnaphash", "base": "", @@ -1622,10 +1608,6 @@ "name": "batch_group_epochs", "type": "uint32[]" }, - { - "name": "capital_shortfall_total", - "type": "int64" - }, { "name": "pending_nominal_slots", "type": "uint64" @@ -1972,11 +1954,6 @@ "type": "delsnapprov", "ricardian_contract": "" }, - { - "name": "fundclaim", - "type": "fundclaim", - "ricardian_contract": "" - }, { "name": "getsnaphash", "type": "getsnaphash", diff --git a/contracts/sysio.system/sysio.system.wasm b/contracts/sysio.system/sysio.system.wasm index aef3622d031b92a104d4a5843ec04ee762fdee77..07311b89e74a3263b643c3476c50c89eaa66c24c 100755 GIT binary patch delta 22101 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⋙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 sysio.system -> sysio.token`. +path, `sysio.synd` and `sysio.liq` for both transport families. The funded import +case verifies that pre-launch credits link and pay. +LIQ yield distributes only supplied WIRE. Its regression checks exact holder settlement and no treasury draw. Ethereum NFT registration remains covered by the Ethereum-specific dispatch and ROA tests: changing that source chain would hide an actual protocol constraint. diff --git a/contracts/tests/depot_flow_coverage.json b/contracts/tests/depot_flow_coverage.json index 823de4e9b7..a8d9a1af2a 100644 --- a/contracts/tests/depot_flow_coverage.json +++ b/contracts/tests/depot_flow_coverage.json @@ -40,7 +40,9 @@ "sysio_synd_tests/generated_generic_custody_bond_challenge_and_return_sequences", "sysio_synd_tests/generic_custody_shortfall_freezes_and_repair_resumes", "sysio_dispatch_tests/a_custody_shortfall_pulls_the_cord_and_the_epoch_advances", - "sysio_dispatch_tests/a_shortfall_freezes_the_same_envelopes_queue_step" + "sysio_dispatch_tests/a_shortfall_freezes_the_same_envelopes_queue_step", + "sysio_synd_tests/a_freeze_does_not_extend_the_challenge_deadline", + "sysio_synd_tests/a_frozen_request_can_be_held_or_ruled_invalid_after_its_deadline" ] }, { @@ -54,7 +56,10 @@ "t5_emissions_tests/governance_gets_remainder_no_dust_loss", "sysio_dclaim_tests/importseed_accepts_credit_batch", "sysio_authex_tests/createlink_eth_sweeps_prelink_dclaim_rewards", - "sysio_dispatch_tests/generic_staking_rewards_fund_claim_and_dedupe_through_dispatch" + "sysio_dispatch_tests/funded_import_claims_link_and_settle", + "sysio_dclaim_tests/importseed_linked_credits_pending_claims", + "sysio_dclaim_tests/importseed_unlinked_parks_unmapped_then_linkswept", + "sysio_dclaim_tests/linkswept_caps_combined_imports_without_aborting" ] }, { @@ -74,7 +79,8 @@ "tests": [ "sysio_synd_tests/generic_parked_credit_yield_conversion_and_fee_rounded_return", "sysio_liq_tests/queued_yield_sells_through_the_pool_and_pays_holders", - "sysio_synd_tests/createlink_sweeps_parked_from_synd" + "sysio_synd_tests/createlink_sweeps_parked_from_synd", + "sysio_liq_tests/addyield_distributes_only_supplied_wire" ] }, { @@ -134,7 +140,9 @@ "sysio_synd_tests/bucket_limits_a_tranche_and_refills_per_epoch", "sysio_synd_tests/oversize_item_releases_in_tranches", "sysio_synd_tests/desyndicate_refuses_over_budget", - "sysio_synd_tests/generated_generic_custody_bond_challenge_and_return_sequences" + "sysio_synd_tests/generated_generic_custody_bond_challenge_and_return_sequences", + "sysio_synd_tests/desyndicate_enforces_configured_minimum", + "sysio_synd_tests/setconfig_validates_minimum_return" ] }, { @@ -151,19 +159,6 @@ "sysio_bond_tests/approved_returns_bonds_and_splits_the_bounty_by_stake", "sysio_synd_tests/second_envelope_waits_until_the_first_is_bonded" ] - }, - { - "name": "flow-yield-distribution", - "wire_behavior": "Staking reward credits linked/unmapped WIRE claims, suppresses stale external references, funds through accounting and liquid balance caps, and records shortfall.", - "cluster_only": "Ethereum mock emitter and real STAKING_REWARD transport.", - "tests": [ - "sysio_dclaim_tests/onreward_linked_credits_pending_claims", - "sysio_dclaim_tests/onreward_unlinked_parks_unmapped_then_linkswept", - "sysio_dclaim_tests/onreward_dedupes_stale_external_ref", - "t5_emissions_tests/fundclaim_transfers_and_tracks_distributed", - "t5_emissions_tests/fundclaim_caps_to_remaining_pool_and_records_shortfall", - "sysio_dispatch_tests/generic_staking_rewards_fund_claim_and_dedupe_through_dispatch" - ] } ] } diff --git a/contracts/tests/emissions_tests.cpp b/contracts/tests/emissions_tests.cpp index 72f8ffdf59..bb21f29eb2 100644 --- a/contracts/tests/emissions_tests.cpp +++ b/contracts/tests/emissions_tests.cpp @@ -215,10 +215,9 @@ static constexpr uint16_t COMPUTE_BPS = 4000; static constexpr uint16_t CAPEX_BPS = 2000; static constexpr uint16_t GOV_BPS = 1000; // Implicit capital reserve = whatever the three explicit shares leave behind. -// At the fixture defaults (4000 + 2000 + 1000), this is 3000 bps. Drained -// lazily via sysio.dclaim::onreward -> sysio.system::fundclaim, not paid at -// payepoch -- so it doesn't appear in t5state.total_distributed until the -// underlying fundclaim transfer happens. +// At the fixture defaults (4000 + 2000 + 1000), this is 3000 bps, +// held in the treasury, not paid at payepoch and excluded from +// t5state.total_distributed. // The implicit capital reserve at these defaults is // 10000 - COMPUTE_BPS - CAPEX_BPS - GOV_BPS = 3000 bps. static constexpr uint16_t PRODUCER_BPS = 7000; @@ -236,7 +235,7 @@ static constexpr uint32_t T_EPOCH_SECS = 60; // Helper: amount NOT transferred at payepoch when no producers / batch // members are paid. Equals producer_pool + batch_pool (compute share, both // undistributed) + implicit capital reserve (never paid at payepoch -- -// drained lazily via fundclaim). The implicit reserve naturally absorbs +// retained in the treasury). The implicit reserve naturally absorbs // the rounding dust from per-share split_bps floors, so the cleanest // definition is `emission - (everything payepoch actually transfers)`, // which collapses to `emission - capex_split - gov_split` when no @@ -499,29 +498,7 @@ class sysio_emissions_tester : public tester { produce_blocks(1); } - /// Deploy the real sysio.dclaim contract on the placeholder account and - /// mark it privileged. Required only by tests that push actions signed by - /// sysio.dclaim (e.g. fundclaim, which `require_auth(CAPITAL_ACCOUNT)`): - /// the sysio.* accounts get a 0-NET ROA policy, so without a deployed - /// contract + privileged flag they cannot afford the inline transaction - /// NET. Idempotent. - void deploy_dclaim_for_signing() { - const account_name DCLAIM = "sysio.dclaim"_n; - if (get_roa_policy(DCLAIM, "nodedaddy"_n).is_null()) { - auto tr = addpolicy_ram_only("nodedaddy"_n, DCLAIM, - asset::from_string("500.0000 SYS")); - BOOST_REQUIRE( tr ); - BOOST_REQUIRE( !tr->except ); - produce_blocks(1); - } - set_code( DCLAIM, contracts::dclaim_wasm() ); - set_abi ( DCLAIM, contracts::dclaim_abi().data() ); - set_privileged( DCLAIM ); - produce_blocks(1); - } - - /// The same for sysio.liq, the other capital drain: deployed and privileged - /// so a test can sign fundclaim as it. + /// Deploy and privilege sysio.liq for native shadow collateral tests. void deploy_liq_for_signing() { const account_name LIQ = "sysio.liq"_n; if (!control->db().find(LIQ)) { @@ -648,14 +625,6 @@ class sysio_emissions_tester : public tester { ); } - action_result fundclaim( account_name signer, account_name recipient, int64_t amount ) { - return push_system_action( - signer, - "fundclaim"_n, - mvo()("recipient", recipient)("amount", amount) - ); - } - /// Push a sysio.system action as a READ-ONLY transaction. /// /// Read-only transactions carry no authorization and no signature, and the chain refuses any @@ -1078,7 +1047,7 @@ class sysio_emissions_tester : public tester { /// Total epoch pay credited but not yet pulled, across every recipient (the `payclaimtot` /// singleton the contract maintains). This WIRE still sits in the treasury's token balance - /// while being fully owed, which is why `fundclaim` and the epoch readiness gate both reserve + /// while being fully owed, which is why treasury withdrawals and the epoch readiness gate both reserve /// it -- and why treasury-balance assertions must account for it. int64_t pay_outstanding_total() { auto data = get_row_by_account(config::system_account_name, config::system_account_name, @@ -2123,7 +2092,7 @@ BOOST_FIXTURE_TEST_CASE( setemitcfg_requires_sysio_auth, sysio_emissions_tester BOOST_FIXTURE_TEST_CASE( setemitcfg_rejects_bad_category_bps, sysio_emissions_tester ) try { // compute + capex + governance must be <= 10000 (the remainder is the - // implicit capital reserve drained lazily by fundclaim). + // unallocated remainder retained in the treasury). auto cfg = mvo() ("t1_allocation", int64_t(1)) ("t2_allocation", int64_t(1)) ("t3_allocation", int64_t(1)) ("t1_duration", uint32_t(1)) ("t2_duration", uint32_t(1)) ("t3_duration", uint32_t(1)) @@ -3213,7 +3182,7 @@ BOOST_FIXTURE_TEST_CASE( category_split_matches_basis_points, sysio_emissions_te BOOST_REQUIRE_EQUAL( compute, test_split_bps(total, COMPUTE_BPS) ); // capex gets only its base split (no producer dust redirect) BOOST_REQUIRE_EQUAL( capex, test_split_bps(total, CAPEX_BPS) ); - // capital is NOT in the epoch_log -- drained lazily via fundclaim, not at payepoch. + // the unallocated remainder is not an epoch_log payout category. BOOST_REQUIRE( !log.get_object().contains("capital_amount") ); } FC_LOG_AND_RETHROW() @@ -3228,7 +3197,7 @@ BOOST_FIXTURE_TEST_CASE( governance_gets_remainder_no_dust_loss, sysio_emissions int64_t gov = log["governance_amount"].as(); // governance is its own BPS share of total (independent of compute/capex): - // the implicit capital reserve (drained lazily via fundclaim) is what + // the unallocated treasury remainder is what // absorbs any unallocated remainder, not governance. BOOST_REQUIRE_EQUAL( gov, test_split_bps(total, uint16_t(1000)) ); } FC_LOG_AND_RETHROW() @@ -3559,14 +3528,13 @@ BOOST_FIXTURE_TEST_CASE( holding_accounts_receive_correct_amounts, sysio_emissio // so what they "received" is a real balance delta. They are protocol-owned holding accounts // with no code (no notify handler to abort `advance`) and, being `sysio.*`, no net/cpu to sign // a claim with and no contract to emit one inline: crediting them would strand the pay in - // `payclaims` permanently. `sysio.dclaim` is pushed for the same reason, via fundclaim. + // `payclaims` permanently. int64_t dclaim_received = get_wire_balance("sysio.dclaim"_n).get_amount() - dclaim_before.get_amount(); int64_t gov_received = get_wire_balance("sysio.gov"_n).get_amount() - gov_before.get_amount(); int64_t batch_received = get_wire_balance("sysio.batch"_n).get_amount() - batch_before.get_amount(); int64_t ops_received = get_wire_balance("sysio.ops"_n).get_amount() - ops_before.get_amount(); - // dclaim is not funded at payepoch anymore -- capital draws are lazy - // via sysio.dclaim::onreward -> sysio.system::fundclaim. + // DClaim receives no emissions allocation. BOOST_REQUIRE_EQUAL( dclaim_received, 0 ); BOOST_REQUIRE_EQUAL( gov_received, gov ); // sysio.batch is not an emissions recipient anymore -- batch pay goes to the @@ -4087,7 +4055,7 @@ BOOST_FIXTURE_TEST_CASE( multi_epoch_cumulative_accounting, sysio_emissions_test BOOST_FIXTURE_TEST_CASE( category_splits_sum_to_emission, sysio_emissions_tester ) try { // Verify compute + capex + governance + implicit_capital_reserve == total_emission. // The implicit capital reserve isn't recorded in epoch_log (drained lazily - // by fundclaim); back it out from the curve total to confirm the math. + // in the treasury); back it out from the curve total to confirm the math. create_t5_holding_accounts(); const uint32_t start = head_secs() - ONE_EPOCH - 1; BOOST_REQUIRE_EQUAL( success(), initt5( config::system_account_name, tpsec(start) ) ); @@ -4142,7 +4110,7 @@ BOOST_FIXTURE_TEST_CASE( epoch_log_records_all_fields, sysio_emissions_tester ) test_scale_annual_to_epoch(ANNUAL_INITIAL_EMISSION, 60) ); // All recorded category amounts are positive (capital_amount is no - // longer in epoch_log -- drained lazily via fundclaim). + // longer in epoch_log -- retained in the treasury). BOOST_REQUIRE( log["compute_amount"].as() > 0 ); BOOST_REQUIRE( log["capex_amount"].as() > 0 ); BOOST_REQUIRE( log["governance_amount"].as() > 0 ); @@ -4684,7 +4652,7 @@ BOOST_FIXTURE_TEST_CASE( opreg_unregistered_account_cannot_become_producer, sysi } FC_LOG_AND_RETHROW() // Node-owner vesting draws on the same `sysio` WIRE balance that backs unclaimed epoch pay, so it -// has to respect the same reserve `fundclaim` and the epoch gate hold. Without that, a vested owner +// has to respect the same reserve the epoch gate holds. Without that, a vested owner // can withdraw WIRE already promised to a `payclaims` row, leaving the later `claimpay` unpayable // and `payepoch` balance-blocked from then on. That exposure is new: before payouts became // claimable, epoch pay had already left the treasury by the time a node claim ran. @@ -5211,165 +5179,6 @@ BOOST_FIXTURE_TEST_CASE( pay_cadence_change_via_setemitcfg_takes_effect, sysio_e } } FC_LOG_AND_RETHROW() -// --------------------------------------------------------------------------- -// fundclaim: per-onreward immediate funding for sysio.dclaim -// --------------------------------------------------------------------------- - -BOOST_FIXTURE_TEST_CASE( fundclaim_requires_the_recipients_auth, sysio_emissions_tester ) try { - create_t5_holding_accounts(); - deploy_dclaim_for_signing(); - const uint32_t start = head_secs() - ONE_EPOCH - 1; - BOOST_REQUIRE_EQUAL( success(), initt5( config::system_account_name, tpsec(start) ) ); - - // alice has no claim to sysio.dclaim's authority. - create_user_accounts({ "alice"_n }); - auto r = fundclaim( "alice"_n, "sysio.dclaim"_n, int64_t(1'000'000) ); - BOOST_REQUIRE( r != success() ); - require_substr( r, "missing authority of sysio.dclaim" ); -} FC_LOG_AND_RETHROW() - -BOOST_FIXTURE_TEST_CASE( fundclaim_refuses_a_recipient_that_is_not_a_drain, sysio_emissions_tester ) try { - create_t5_holding_accounts(); - const uint32_t start = head_secs() - ONE_EPOCH - 1; - BOOST_REQUIRE_EQUAL( success(), initt5( config::system_account_name, tpsec(start) ) ); - - // alice signs for herself; only the two drains may be funded. - create_user_accounts({ "alice"_n }); - auto r = fundclaim( "alice"_n, "alice"_n, int64_t(1'000'000) ); - BOOST_REQUIRE( r != success() ); - require_substr( r, "recipient is not a capital drain" ); -} FC_LOG_AND_RETHROW() - -BOOST_FIXTURE_TEST_CASE( fundclaim_transfers_and_tracks_distributed, sysio_emissions_tester ) try { - create_t5_holding_accounts(); - deploy_dclaim_for_signing(); - const uint32_t start = head_secs() - ONE_EPOCH - 1; - BOOST_REQUIRE_EQUAL( success(), initt5( config::system_account_name, tpsec(start) ) ); - - const asset sysio_before = get_wire_balance_paid( config::system_account_name ); - const asset dclaim_before = get_wire_balance_paid( "sysio.dclaim"_n ); - const int64_t distributed_before = get_t5_state()["total_distributed"].as(); - - const int64_t amt = int64_t(50'000'000'000); // 50 WIRE - BOOST_REQUIRE_EQUAL( success(), fundclaim( "sysio.dclaim"_n, "sysio.dclaim"_n, amt ) ); - - const asset sysio_after = get_wire_balance_paid( config::system_account_name ); - const asset dclaim_after = get_wire_balance_paid( "sysio.dclaim"_n ); - const auto state = get_t5_state(); - - BOOST_REQUIRE_EQUAL( sysio_before.get_amount() - sysio_after.get_amount(), amt ); - BOOST_REQUIRE_EQUAL( dclaim_after.get_amount() - dclaim_before.get_amount(), amt ); - BOOST_REQUIRE_EQUAL( state["total_distributed"].as(), distributed_before + amt ); - BOOST_REQUIRE_EQUAL( state["capital_shortfall_total"].as(), 0 ); -} FC_LOG_AND_RETHROW() - -BOOST_FIXTURE_TEST_CASE( fundclaim_no_op_for_zero_or_negative, sysio_emissions_tester ) try { - create_t5_holding_accounts(); - deploy_dclaim_for_signing(); - const uint32_t start = head_secs() - ONE_EPOCH - 1; - BOOST_REQUIRE_EQUAL( success(), initt5( config::system_account_name, tpsec(start) ) ); - - const asset dclaim_before = get_wire_balance_paid( "sysio.dclaim"_n ); - const int64_t distributed_before = get_t5_state()["total_distributed"].as(); - - BOOST_REQUIRE_EQUAL( success(), fundclaim( "sysio.dclaim"_n, "sysio.dclaim"_n, int64_t(0) ) ); - BOOST_REQUIRE_EQUAL( success(), fundclaim( "sysio.dclaim"_n, "sysio.dclaim"_n, int64_t(-100) ) ); - - BOOST_REQUIRE_EQUAL( get_wire_balance_paid( "sysio.dclaim"_n ).get_amount(), - dclaim_before.get_amount() ); - BOOST_REQUIRE_EQUAL( get_t5_state()["total_distributed"].as(), - distributed_before ); - BOOST_REQUIRE_EQUAL( get_t5_state()["capital_shortfall_total"].as(), 0 ); -} FC_LOG_AND_RETHROW() - -BOOST_FIXTURE_TEST_CASE( fundclaim_caps_to_remaining_pool_and_records_shortfall, sysio_emissions_tester ) try { - // Squeeze t5_distributable down to a small headroom over t5_floor so the - // next fundclaim has a hard cap. Verify the partial transfer and the - // shortfall accumulator. - create_t5_holding_accounts(); - deploy_dclaim_for_signing(); - const uint32_t start = head_secs() - ONE_EPOCH - 1; - BOOST_REQUIRE_EQUAL( success(), initt5( config::system_account_name, tpsec(start) ) ); - - // Shrink drainable headroom to exactly 1000 subunits via a fresh emitcfg. - // The post-init guard requires t5_distributable >= t5_floor + total_distributed, - // so set t5_floor to 0 and t5_distributable just above current total_distributed. - const int64_t already = get_t5_state()["total_distributed"].as(); - const int64_t headroom = 1000; - auto cfg = mvo() - ("t1_allocation", T1_ALLOCATION.get_amount()) - ("t2_allocation", T2_ALLOCATION.get_amount()) - ("t3_allocation", T3_ALLOCATION.get_amount()) - ("t1_duration", T1_DURATION) ("t2_duration", T2_DURATION) ("t3_duration", T3_DURATION) - ("min_claimable", MIN_CLAIMABLE_AMOUNT) - ("t5_distributable", already + headroom) - ("t5_floor", int64_t(0)) - ("target_annual_decay_bps", TARGET_ANNUAL_DECAY_BPS) - ("annual_initial_emission", ANNUAL_INITIAL_EMISSION) - ("annual_max_emission", ANNUAL_MAX_EMISSION) - // annual_min_emission=0 so the post-init guard (per_epoch_min <= remaining) - // doesn't reject the deliberately-tiny pool used by this test. - ("annual_min_emission", int64_t(0)) - ("compute_bps", COMPUTE_BPS) - ("capex_bps", CAPEX_BPS) ("governance_bps", GOV_BPS) - ("producer_bps", PRODUCER_BPS) ("batch_op_bps", uint16_t(3000)) - ("standby_end_rank", T_STANDBY_END_RANK)("standby_bps", T_STANDBY_BPS) - ("epoch_log_retention_count", uint32_t(8640))("pay_cadence_epochs", uint16_t(1)); - BOOST_REQUIRE_EQUAL( success(), setemitcfg( config::system_account_name, cfg ) ); - - const asset dclaim_before = get_wire_balance_paid( "sysio.dclaim"_n ); - - // Request 3x the headroom; expect partial transfer of `headroom`, shortfall = 2x. - const int64_t request = headroom * 3; - const int64_t shortfall = request - headroom; - BOOST_REQUIRE_EQUAL( success(), fundclaim( "sysio.dclaim"_n, "sysio.dclaim"_n, request ) ); - - const asset dclaim_after = get_wire_balance_paid( "sysio.dclaim"_n ); - const auto state = get_t5_state(); - BOOST_REQUIRE_EQUAL( dclaim_after.get_amount() - dclaim_before.get_amount(), headroom ); - BOOST_REQUIRE_EQUAL( state["total_distributed"].as(), already + headroom ); - BOOST_REQUIRE_EQUAL( state["capital_shortfall_total"].as(), shortfall ); - - // A further request after pool is exhausted is a full shortfall. - BOOST_REQUIRE_EQUAL( success(), fundclaim( "sysio.dclaim"_n, "sysio.dclaim"_n, int64_t(500) ) ); - const auto state2 = get_t5_state(); - BOOST_REQUIRE_EQUAL( get_wire_balance_paid( "sysio.dclaim"_n ).get_amount(), - dclaim_after.get_amount() ); - BOOST_REQUIRE_EQUAL( state2["total_distributed"].as(), already + headroom ); - BOOST_REQUIRE_EQUAL( state2["capital_shortfall_total"].as(), shortfall + 500 ); -} FC_LOG_AND_RETHROW() - -BOOST_FIXTURE_TEST_CASE( fundclaim_silent_when_t5state_missing, sysio_emissions_tester ) try { - // initt5 NOT called, so t5state.exists() is false. fundclaim must absorb - // the call without throwing or mutating any state. - create_t5_holding_accounts(); - deploy_dclaim_for_signing(); - const asset dclaim_before = get_wire_balance_paid( "sysio.dclaim"_n ); - - BOOST_REQUIRE_EQUAL( success(), fundclaim( "sysio.dclaim"_n, "sysio.dclaim"_n, int64_t(1'000'000) ) ); - - BOOST_REQUIRE_EQUAL( get_wire_balance_paid( "sysio.dclaim"_n ).get_amount(), - dclaim_before.get_amount() ); -} FC_LOG_AND_RETHROW() - -BOOST_FIXTURE_TEST_CASE( fundclaim_funds_the_liq_kicker_drain, sysio_emissions_tester ) try { - // sysio.liq draws the yield kicker through the same drain as sysio.dclaim. - create_t5_holding_accounts(); - deploy_liq_for_signing(); - const uint32_t start = head_secs() - ONE_EPOCH - 1; - BOOST_REQUIRE_EQUAL( success(), initt5( config::system_account_name, tpsec(start) ) ); - - const asset liq_before = get_wire_balance( "sysio.liq"_n ); - const int64_t distributed_before = get_t5_state()["total_distributed"].as(); - - const int64_t amt = int64_t(2'000'000'000); // 2 WIRE - BOOST_REQUIRE_EQUAL( success(), fundclaim( "sysio.liq"_n, "sysio.liq"_n, amt ) ); - - BOOST_REQUIRE_EQUAL( get_wire_balance( "sysio.liq"_n ).get_amount() - liq_before.get_amount(), amt ); - BOOST_REQUIRE_EQUAL( get_t5_state()["total_distributed"].as(), distributed_before + amt ); - BOOST_REQUIRE_EQUAL( get_t5_state()["capital_shortfall_total"].as(), 0 ); -} FC_LOG_AND_RETHROW() - BOOST_AUTO_TEST_SUITE_END() // t5_emissions_tests // =========================================================================== diff --git a/contracts/tests/sysio.authex_tests.cpp b/contracts/tests/sysio.authex_tests.cpp index 35c7c23666..212673e097 100644 --- a/contracts/tests/sysio.authex_tests.cpp +++ b/contracts/tests/sysio.authex_tests.cpp @@ -128,18 +128,13 @@ class sysio_authex_tester : public tester { ("native_address", native_address)); } - action_result onreward(const std::vector& native_address, uint64_t amount, - ChainKind chain_kind = ChainKind::CHAIN_KIND_EVM) { + /// Import a pre-launch WIRE credit before testing authenticated link settlement. + action_result import_credit(const std::vector& native_address, uint64_t amount, + ChainKind chain_kind = ChainKind::CHAIN_KIND_EVM) { return sysio_system::test_support::push_contract_action_and_produce_block( - *this, DCLAIM, dclaim_abi_ser, MSGCH, "onreward"_n, mvo() - ("chain_code", uint64_t{1}) - ("staker_wire_account", std::string{}) - ("reward_chain", chain_kind) - ("staker_native_addr", native_address) - ("reward_amount", amount) - ("reward_epoch_index", uint32_t{7}) - ("external_epoch_ref", uint64_t{100}) - ("share_bps", uint32_t{10000})); + *this, DCLAIM, dclaim_abi_ser, DCLAIM, "importseed"_n, mvo() + ("chain", chain_kind)("credits", fc::variants{ + mvo()("native_address", native_address)("wire_atomic", static_cast(amount))})); } fc::variant get_dclaim_row(name table, const char* type, uint64_t id) { @@ -324,7 +319,7 @@ BOOST_FIXTURE_TEST_CASE( createlink_eth_sweeps_prelink_dclaim_rewards, sysio_aut const auto address_bytes = fc::crypto::ethereum::address_to_bytes(link.pub); const std::vector native_address(address_bytes.begin(), address_bytes.end()); - BOOST_REQUIRE_EQUAL(success(), onreward(native_address, 5000)); + BOOST_REQUIRE_EQUAL(success(), import_credit(native_address, 5000)); BOOST_REQUIRE(!get_dclaim_row("unmapped"_n, "unmapped_token", 1).is_null()); BOOST_REQUIRE_EQUAL(success(), createlink( @@ -343,7 +338,7 @@ BOOST_FIXTURE_TEST_CASE( createlink_preserves_old_prelink_dclaim_rewards, auto link = make_eth_link("alice", now_ms()); const auto address_bytes = fc::crypto::ethereum::address_to_bytes(link.pub); const std::vector native_address(address_bytes.begin(), address_bytes.end()); - BOOST_REQUIRE_EQUAL(success(), onreward(native_address, 5000)); + BOOST_REQUIRE_EQUAL(success(), import_credit(native_address, 5000)); BOOST_REQUIRE(!get_dclaim_row("unmapped"_n, "unmapped_token", 1).is_null()); constexpr uint32_t years_without_link_sec = 3u * 365u * 24u * 60u * 60u; produce_block(fc::seconds(years_without_link_sec)); @@ -402,7 +397,7 @@ BOOST_FIXTURE_TEST_CASE( recordlink_records_link_when_dclaim_is_not_privileged, const auto address_bytes = fc::crypto::ethereum::address_to_bytes(public_key); const std::vector native_address(address_bytes.begin(), address_bytes.end()); - BOOST_REQUIRE_EQUAL(success(), onreward(native_address, 6500)); + BOOST_REQUIRE_EQUAL(success(), import_credit(native_address, 6500)); BOOST_REQUIRE(!get_dclaim_row("unmapped"_n, "unmapped_token", 1).is_null()); set_dclaim_privileged(false); @@ -424,7 +419,7 @@ BOOST_FIXTURE_TEST_CASE( recordlink_eth_sweeps_prelink_dclaim_rewards, sysio_aut const auto address_bytes = fc::crypto::ethereum::address_to_bytes(public_key); const std::vector native_address(address_bytes.begin(), address_bytes.end()); - BOOST_REQUIRE_EQUAL(success(), onreward(native_address, 7000)); + BOOST_REQUIRE_EQUAL(success(), import_credit(native_address, 7000)); BOOST_REQUIRE_EQUAL(success(), recordlink(AUTHEX, "bob", public_key, native_address)); produce_blocks(); @@ -441,7 +436,7 @@ BOOST_FIXTURE_TEST_CASE( recordlink_svm_sweeps_prelink_dclaim_rewards, sysio_aut const auto raw_key = public_key.get().serialize(); const std::vector native_address(raw_key.begin(), raw_key.end()); - BOOST_REQUIRE_EQUAL(success(), onreward( + BOOST_REQUIRE_EQUAL(success(), import_credit( native_address, 8000, ChainKind::CHAIN_KIND_SVM)); BOOST_REQUIRE_EQUAL(success(), recordlink( AUTHEX, "carol", public_key, native_address, ChainKind::CHAIN_KIND_SVM)); @@ -461,7 +456,7 @@ BOOST_FIXTURE_TEST_CASE( recordlink_svm_mismatched_address_skips_sweep, sysio_au std::vector mismatched_address(raw_key.begin(), raw_key.end()); mismatched_address.front() ^= char{0x01}; - BOOST_REQUIRE_EQUAL(success(), onreward( + BOOST_REQUIRE_EQUAL(success(), import_credit( mismatched_address, 8500, ChainKind::CHAIN_KIND_SVM)); BOOST_REQUIRE_EQUAL(success(), recordlink( AUTHEX, "carol", public_key, mismatched_address, ChainKind::CHAIN_KIND_SVM)); @@ -483,7 +478,7 @@ BOOST_FIXTURE_TEST_CASE( recordlink_identical_retry_resweeps_dclaim_rewards, sys const std::vector native_address(address_bytes.begin(), address_bytes.end()); BOOST_REQUIRE_EQUAL(success(), recordlink(AUTHEX, "bob", public_key, native_address)); - BOOST_REQUIRE_EQUAL(success(), onreward(native_address, 9000)); + BOOST_REQUIRE_EQUAL(success(), import_credit(native_address, 9000)); BOOST_REQUIRE(!get_dclaim_row("unmapped"_n, "unmapped_token", 1).is_null()); BOOST_REQUIRE_EQUAL(success(), recordlink(AUTHEX, "bob", public_key, native_address)); diff --git a/contracts/tests/sysio.dclaim_tests.cpp b/contracts/tests/sysio.dclaim_tests.cpp index 3dab0dd6d3..1ebdb049bd 100644 --- a/contracts/tests/sysio.dclaim_tests.cpp +++ b/contracts/tests/sysio.dclaim_tests.cpp @@ -16,14 +16,10 @@ using namespace sysio::opp::types; using mvo = fc::mutable_variant_object; -/// Test fixture for sysio.dclaim. Deploys sysio.dclaim and creates sysio.msgch / -/// sysio.authex as the authorized inbound callers. The staking-reward path -/// credits a WIRE-denominated amount directly (native -> WIRE conversion is -/// outpost-side), so no sysio.reserv deployment is needed. +/// Test fixture for funded, non-expiring DClaim imports and AuthX linking. class sysio_dclaim_tester : public tester { public: static constexpr auto DCLAIM_ACCOUNT = "sysio.dclaim"_n; - static constexpr auto MSGCH_ACCOUNT = "sysio.msgch"_n; static constexpr auto AUTHEX_ACCOUNT = "sysio.authex"_n; static constexpr auto TOKEN_ACCOUNT = "sysio.token"_n; static constexpr uint32_t YEARS_WITHOUT_CLAIM_SEC = 3u * 365u * 24u * 60u * 60u; @@ -35,7 +31,7 @@ class sysio_dclaim_tester : public tester { // system account); it is signed for directly to drive linkswept, never // re-created here. create_accounts({ - DCLAIM_ACCOUNT, MSGCH_ACCOUNT, TOKEN_ACCOUNT, "alice"_n, "bob"_n, + DCLAIM_ACCOUNT, TOKEN_ACCOUNT, "alice"_n, "bob"_n, }); produce_blocks(2); @@ -69,22 +65,14 @@ class sysio_dclaim_tester : public tester { return push(DCLAIM_ACCOUNT, dclaim_abi_ser, signer, action_name, data); } - /// Dispatch a STAKING_REWARD per-staker body to dclaim::onreward. `amount` is - /// the WIRE-denominated reward (native -> WIRE conversion is outpost-side). - action_result onreward(name signer, uint64_t chain_code, - const std::string& wire_account, ChainKind chain, - const std::vector& native_addr, - uint64_t amount, uint32_t epoch_index, - uint64_t external_epoch_ref, uint32_t share_bps = 10000) { - return push_dclaim(signer, "onreward"_n, mvo() - ("chain_code", chain_code) - ("staker_wire_account", wire_account) - ("reward_chain", chain) - ("staker_native_addr", native_addr) - ("reward_amount", amount) - ("reward_epoch_index", epoch_index) - ("external_epoch_ref", external_epoch_ref) - ("share_bps", share_bps)); + /// Seed a pre-launch credit, optionally sweeping it into the linked account. + action_result seed_credit(const std::string& wire_account, const std::vector& address, int64_t amount) { + auto result = push_dclaim(DCLAIM_ACCOUNT, "importseed"_n, mvo() + ("chain", ChainKind::CHAIN_KIND_EVM) + ("credits", fc::variants{mvo()("native_address", address)("wire_atomic", amount)})); + if (result != success() || wire_account.empty()) return result; + return push_dclaim(AUTHEX_ACCOUNT, "linkswept"_n, mvo() + ("wire_account", wire_account)("chain", ChainKind::CHAIN_KIND_EVM)("native_pubkey", address)); } fc::variant get_kv(name table, const char* type, uint64_t id) { @@ -95,7 +83,6 @@ class sysio_dclaim_tester : public tester { } fc::variant pending_of(name acct) { return get_kv("pclaims"_n, "pending_claim", acct.to_uint64_t()); } fc::variant unmapped_row(uint64_t id) { return get_kv("unmapped"_n, "unmapped_token", id); } - fc::variant cursor_row(uint64_t id) { return get_kv("rwdcursors"_n, "reward_cursor", id); } /// Fund DClaim with real WIRE so delayed claims exercise transfers and row erasure. void fund_claims() { @@ -169,28 +156,23 @@ BOOST_FIXTURE_TEST_CASE(importdone_locks_subsequent_importseed, sysio_dclaim_tes success()); } FC_LOG_AND_RETHROW() } -// -- onreward auth + routing -- +// -- pre-launch import routing -- -BOOST_FIXTURE_TEST_CASE(onreward_requires_msgch_auth, sysio_dclaim_tester) { try { - BOOST_REQUIRE_NE( - onreward("alice"_n, 1, "alice", ChainKind::CHAIN_KIND_EVM, addr20, 1000, 7, 100), - success()); -} FC_LOG_AND_RETHROW() } -BOOST_FIXTURE_TEST_CASE(onreward_linked_credits_pending_claims, sysio_dclaim_tester) { try { +BOOST_FIXTURE_TEST_CASE(importseed_linked_credits_pending_claims, sysio_dclaim_tester) { try { BOOST_REQUIRE_EQUAL( - onreward(MSGCH_ACCOUNT, 1, "alice", ChainKind::CHAIN_KIND_EVM, addr20, 1000, 7, 100), + seed_credit("alice", addr20, 1000), success()); - // reward_amount is already WIRE-denominated -> credited verbatim. + // Imported WIRE is credited verbatim. auto p = pending_of("alice"_n); BOOST_REQUIRE(!p.is_null()); BOOST_REQUIRE_EQUAL(p["balance"].as().get_amount(), 1000); } FC_LOG_AND_RETHROW() } -BOOST_FIXTURE_TEST_CASE(onreward_unlinked_parks_unmapped_then_linkswept, sysio_dclaim_tester) { try { +BOOST_FIXTURE_TEST_CASE(importseed_unlinked_parks_unmapped_then_linkswept, sysio_dclaim_tester) { try { // Empty wire account -> parked in unmapped by native address. BOOST_REQUIRE_EQUAL( - onreward(MSGCH_ACCOUNT, 1, "", ChainKind::CHAIN_KIND_EVM, addr20, 5000, 7, 100), + seed_credit("", addr20, 5000), success()); BOOST_REQUIRE(pending_of("bob"_n).is_null()); auto u = unmapped_row(1); @@ -208,10 +190,9 @@ BOOST_FIXTURE_TEST_CASE(onreward_unlinked_parks_unmapped_then_linkswept, sysio_d BOOST_REQUIRE_EQUAL(pending_of("bob"_n)["balance"].as().get_amount(), 5000); } FC_LOG_AND_RETHROW() } -BOOST_FIXTURE_TEST_CASE(linkswept_preserves_old_unmapped_rewards, sysio_dclaim_tester) { try { +BOOST_FIXTURE_TEST_CASE(linkswept_preserves_old_unmapped_imports, sysio_dclaim_tester) { try { fund_claims(); - BOOST_REQUIRE_EQUAL(onreward(MSGCH_ACCOUNT, 1, "", ChainKind::CHAIN_KIND_EVM, - addr20, 5000, 7, 100), success()); + BOOST_REQUIRE_EQUAL(seed_credit("", addr20, 5000), success()); produce_block(fc::seconds(YEARS_WITHOUT_CLAIM_SEC)); BOOST_REQUIRE_EQUAL(push_dclaim(AUTHEX_ACCOUNT, "linkswept"_n, mvo() ("wire_account", "bob") @@ -226,15 +207,13 @@ BOOST_FIXTURE_TEST_CASE(linkswept_preserves_old_unmapped_rewards, sysio_dclaim_t check_claim("bob"_n, 5000); } FC_LOG_AND_RETHROW() } -BOOST_FIXTURE_TEST_CASE(linkswept_adds_old_rewards_to_newer_pending_balance, sysio_dclaim_tester) { try { +BOOST_FIXTURE_TEST_CASE(linkswept_adds_old_imports_to_newer_pending_balance, sysio_dclaim_tester) { try { fund_claims(); const std::vector newer_addr(20, char(0xB2)); - BOOST_REQUIRE_EQUAL(onreward(MSGCH_ACCOUNT, 1, "", ChainKind::CHAIN_KIND_EVM, - addr20, 5000, 7, 100), success()); + BOOST_REQUIRE_EQUAL(seed_credit("", addr20, 5000), success()); produce_block(fc::seconds(YEARS_WITHOUT_CLAIM_SEC)); - BOOST_REQUIRE_EQUAL(onreward(MSGCH_ACCOUNT, 1, "bob", ChainKind::CHAIN_KIND_EVM, - newer_addr, 2000, 8, 101), success()); + BOOST_REQUIRE_EQUAL(seed_credit("bob", newer_addr, 2000), success()); BOOST_REQUIRE_EQUAL(push_dclaim(AUTHEX_ACCOUNT, "linkswept"_n, mvo() ("wire_account", "bob") @@ -247,15 +226,13 @@ BOOST_FIXTURE_TEST_CASE(linkswept_adds_old_rewards_to_newer_pending_balance, sys check_claim("bob"_n, 7000); } FC_LOG_AND_RETHROW() } -BOOST_FIXTURE_TEST_CASE(linkswept_adds_newer_rewards_to_old_pending_balance, sysio_dclaim_tester) { try { +BOOST_FIXTURE_TEST_CASE(linkswept_adds_newer_imports_to_old_pending_balance, sysio_dclaim_tester) { try { fund_claims(); const std::vector newer_addr(20, char(0xB2)); - BOOST_REQUIRE_EQUAL(onreward(MSGCH_ACCOUNT, 1, "bob", ChainKind::CHAIN_KIND_EVM, - addr20, 2000, 7, 100), success()); + BOOST_REQUIRE_EQUAL(seed_credit("bob", addr20, 2000), success()); produce_block(fc::seconds(YEARS_WITHOUT_CLAIM_SEC)); - BOOST_REQUIRE_EQUAL(onreward(MSGCH_ACCOUNT, 1, "", ChainKind::CHAIN_KIND_EVM, - newer_addr, 5000, 8, 101), success()); + BOOST_REQUIRE_EQUAL(seed_credit("", newer_addr, 5000), success()); BOOST_REQUIRE_EQUAL(push_dclaim(AUTHEX_ACCOUNT, "linkswept"_n, mvo() ("wire_account", "bob") @@ -264,10 +241,22 @@ BOOST_FIXTURE_TEST_CASE(linkswept_adds_newer_rewards_to_old_pending_balance, sys const auto pending = pending_of("bob"_n); BOOST_REQUIRE_EQUAL(pending["balance"].as().get_amount(), 7000); - BOOST_REQUIRE(unmapped_row(1).is_null()); + BOOST_REQUIRE(unmapped_row(2).is_null()); check_claim("bob"_n, 7000); } FC_LOG_AND_RETHROW() } +/// Sweeping multiple imported identities into one account saturates without aborting its OPP parent. +BOOST_FIXTURE_TEST_CASE(linkswept_caps_combined_imports_without_aborting, sysio_dclaim_tester) { try { + constexpr int64_t headroom = 5; + const std::vector second_address(20, char(0xB2)); + BOOST_REQUIRE_EQUAL(success(), seed_credit("bob", addr20, asset::max_amount - headroom)); + BOOST_REQUIRE_EQUAL(success(), seed_credit("", second_address, headroom + 1)); + BOOST_REQUIRE_EQUAL(success(), push_dclaim(AUTHEX_ACCOUNT, "linkswept"_n, mvo() + ("wire_account", "bob")("chain", ChainKind::CHAIN_KIND_EVM)("native_pubkey", second_address))); + BOOST_REQUIRE_EQUAL(asset::max_amount, pending_of("bob"_n)["balance"].as().get_amount()); + BOOST_REQUIRE(unmapped_row(2).is_null()); +} FC_LOG_AND_RETHROW() } + BOOST_FIXTURE_TEST_CASE(imported_balances_remain_claimable_after_years, sysio_dclaim_tester) { try { fund_claims(); BOOST_REQUIRE_EQUAL(push_dclaim(DCLAIM_ACCOUNT, "importseed"_n, mvo() @@ -284,33 +273,14 @@ BOOST_FIXTURE_TEST_CASE(imported_balances_remain_claimable_after_years, sysio_dc check_claim("bob"_n, 5000); } FC_LOG_AND_RETHROW() } -// -- dedupe cursor -- - -BOOST_FIXTURE_TEST_CASE(onreward_dedupes_stale_external_ref, sysio_dclaim_tester) { try { - BOOST_REQUIRE_EQUAL( - onreward(MSGCH_ACCOUNT, 1, "alice", ChainKind::CHAIN_KIND_EVM, addr20, 1000, 7, 100), - success()); - BOOST_REQUIRE_EQUAL(pending_of("alice"_n)["balance"].as().get_amount(), 1000); - - // Replay same external_epoch_ref -> admitted=false -> no extra credit. - BOOST_REQUIRE_EQUAL( - onreward(MSGCH_ACCOUNT, 1, "alice", ChainKind::CHAIN_KIND_EVM, addr20, 1000, 7, 100), - success()); - BOOST_REQUIRE_EQUAL(pending_of("alice"_n)["balance"].as().get_amount(), 1000); - // A newer external_epoch_ref is accepted and adds more. - BOOST_REQUIRE_EQUAL( - onreward(MSGCH_ACCOUNT, 1, "alice", ChainKind::CHAIN_KIND_EVM, addr20, 1000, 8, 101), - success()); - BOOST_REQUIRE_EQUAL(pending_of("alice"_n)["balance"].as().get_amount(), 2000); -} FC_LOG_AND_RETHROW() } // -- indefinitely claimable balances -- -BOOST_FIXTURE_TEST_CASE(pending_rewards_remain_claimable_after_years, sysio_dclaim_tester) { try { +BOOST_FIXTURE_TEST_CASE(pending_imports_remain_claimable_after_years, sysio_dclaim_tester) { try { fund_claims(); BOOST_REQUIRE_EQUAL( - onreward(MSGCH_ACCOUNT, 1, "alice", ChainKind::CHAIN_KIND_EVM, addr20, 1000, 7, 100), + seed_credit("alice", addr20, 1000), success()); BOOST_REQUIRE(!pending_of("alice"_n).is_null()); @@ -320,59 +290,5 @@ BOOST_FIXTURE_TEST_CASE(pending_rewards_remain_claimable_after_years, sysio_dcla check_claim("alice"_n, 1000); } FC_LOG_AND_RETHROW() } -// onreward runs inside the OPP inbound dispatch chain (msgch::evalcons), where an abort rolls back -// the consensus-tipping deliver and stalls epoch advancement. A cross-chain-supplied -// staker_wire_account that name() would reject must NOT abort: onreward treats it as unlinked and -// parks the credit in unmapped_tokens by native address. name() has three distinct reject classes, -// and the guard absorbs all three: (1) overlong at > 13 chars, (2) a character outside the -// ".12345a-z" name alphabet such as uppercase or '-', and (3) a length-valid 13-character name whose -// final symbol exceeds 'j' (value 15): the 13th position encodes only 4 bits. Each malformed -// spelling is exercised on a distinct native address so it lands as its own row (unmapped + dedupe -// cursor are keyed by native address), covering the full reject domain the dispatch name guard absorbs. -BOOST_FIXTURE_TEST_CASE(onreward_invalid_wire_account_parks_unmapped, sysio_dclaim_tester) { try { - const struct { const char* wire_account; char addr_byte; } cases[] = { - { "thisnameistoolong", char(0xB1) }, // 17 chars: length > 13 - { "BADNAME", char(0xB2) }, // uppercase: outside the name alphabet - { "bad-name", char(0xB3) }, // '-': outside the name alphabet - { "aaaaaaaaaaaak", char(0xB4) }, // 13 chars, 13th symbol 'k' (16) > 'j' (15): 4-bit slot overflow - }; - - uint64_t expected_id = 1; // next_unmapped_id defaults to 1; one new row per distinct address - for (const auto& c : cases) { - const std::vector addr(20, c.addr_byte); - BOOST_REQUIRE_EQUAL(success(), - onreward(MSGCH_ACCOUNT, 1, c.wire_account, ChainKind::CHAIN_KIND_EVM, addr, - 1000, 7, 100)); - // Soft-handled: credit parked as unmapped (unlinked), never aborted. - auto u = unmapped_row(expected_id); - BOOST_REQUIRE(!u.is_null()); - BOOST_REQUIRE_EQUAL(u["balance"].as().get_amount(), 1000); - ++expected_id; - } -} FC_LOG_AND_RETHROW() } - -// A reward_amount above asset::max_amount (2^62-1) would abort the WIRE asset constructor. onreward -// must soft-skip it (return early) rather than abort the inbound dispatch — no credit is created. -BOOST_FIXTURE_TEST_CASE(onreward_oversized_amount_soft_skips, sysio_dclaim_tester) { try { - const uint64_t oversized = (uint64_t(1) << 62); // asset::max_amount + 1 - BOOST_REQUIRE_EQUAL(success(), - onreward(MSGCH_ACCOUNT, 1, "alice", ChainKind::CHAIN_KIND_EVM, addr20, - oversized, 7, 100)); - // Soft-skipped: neither a pending claim (valid name "alice") nor an unmapped row was created. - BOOST_REQUIRE(pending_of("alice"_n).is_null()); - BOOST_REQUIRE(unmapped_row(1).is_null()); -} FC_LOG_AND_RETHROW() } - -// The positive side of the dispatch name guard: a valid account name that is not plain lowercase -// (here one carrying both a dot and a digit) must still resolve to a linked staker and credit -// pending_claims, never fall through to the unmapped native-address parking path. -BOOST_FIXTURE_TEST_CASE(onreward_dotted_digit_name_credits_pending, sysio_dclaim_tester) { try { - BOOST_REQUIRE_EQUAL(success(), - onreward(MSGCH_ACCOUNT, 1, "stak.er1", ChainKind::CHAIN_KIND_EVM, addr20, 1000, 7, 100)); - auto p = pending_of("stak.er1"_n); - BOOST_REQUIRE(!p.is_null()); - BOOST_REQUIRE_EQUAL(p["balance"].as().get_amount(), 1000); - BOOST_REQUIRE(unmapped_row(1).is_null()); // linked directly, not parked as unmapped -} FC_LOG_AND_RETHROW() } BOOST_AUTO_TEST_SUITE_END() diff --git a/contracts/tests/sysio.dispatch_tests.cpp b/contracts/tests/sysio.dispatch_tests.cpp index 7abdfb2c19..3a52be73fa 100644 --- a/contracts/tests/sysio.dispatch_tests.cpp +++ b/contracts/tests/sysio.dispatch_tests.cpp @@ -61,10 +61,6 @@ using sysio_system::test_support::sign_createlink; namespace { -constexpr uint64_t PROTOBUF_VARINT_PAYLOAD_MASK = 0x7fu; -constexpr uint32_t PROTOBUF_VARINT_PAYLOAD_BITS = 7u; -constexpr uint8_t PROTOBUF_VARINT_CONTINUATION_BIT = 0x80u; -constexpr uint32_t PROTOBUF_FIELD_TAG_SHIFT = 3u; /// The outpost custody a SyndicateLIQ or LIQYield fixture carries by default: the whole asset range, at /// or above any outstanding shadow a dispatch test can reach. @@ -95,7 +91,8 @@ std::vector encode_envelope_with_mixed_attestations( uint32_t epoch_index, const std::vector& attestations, const std::string& previous_envelope_hash = {}, - const std::string& previous_message_id = {}) + const std::string& previous_message_id = {}, + const std::vector& unknown_types = {}) { sysio::opp::Envelope env; env.set_epoch_index(epoch_index); @@ -105,6 +102,14 @@ std::vector encode_envelope_with_mixed_attestations( auto* msg = env.add_messages(); auto* payload = msg->mutable_payload(); + // Open protobuf enums retain raw wire discriminants, including undeclared values. + constexpr auto type_field = "type"; + for (const auto raw_type : unknown_types) { + auto* att = payload->add_attestations(); + att->GetReflection()->SetEnumValue(att, att->GetDescriptor()->FindFieldByName(type_field), raw_type); + att->set_data(std::string(1, '\x0a')); + att->set_data_size(1); + } for (const auto& [att_type, att_data] : attestations) { auto* att = payload->add_attestations(); att->set_type(att_type); @@ -1000,14 +1005,12 @@ BOOST_FIXTURE_TEST_CASE(dispatch_routes_node_owner_reg_to_roa, sysio_dispatch_te auto eth_bytes = em_uncompressed_pubkey_bytes(eth_pub); auto eth_address = fc::crypto::ethereum::address_to_bytes(eth_pub); - // Seed a pre-link reward at the key-derived address, then deliberately put a conflicting address + // Seed a pre-link import at the key-derived address, then deliberately put a conflicting address // in the redundant actor field. Dispatch must derive from actor_pub_key and sweep the real row. const std::vector native_address(eth_address.begin(), eth_address.end()); - BOOST_REQUIRE_EQUAL(success(), push(DCLAIM_ACCOUNT, dclaim_abi, MSGCH_ACCOUNT, "onreward"_n, mvo() - ("chain_code", eth_code)("staker_wire_account", std::string{}) - ("reward_chain", ChainKind::CHAIN_KIND_EVM)("staker_native_addr", native_address) - ("reward_amount", uint64_t{4321})("reward_epoch_index", uint32_t{7}) - ("external_epoch_ref", uint64_t{100})("share_bps", uint32_t{10000}))); + BOOST_REQUIRE_EQUAL(success(), push(DCLAIM_ACCOUNT, dclaim_abi, DCLAIM_ACCOUNT, "importseed"_n, mvo() + ("chain", ChainKind::CHAIN_KIND_EVM)("credits", fc::variants{ + mvo()("native_address", native_address)("wire_atomic", int64_t{4321})}))); BOOST_REQUIRE(!get_dclaim_row("unmapped"_n, "unmapped_token", 1).is_null()); std::fill(eth_address.begin(), eth_address.end(), uint8_t{0xA5}); @@ -1656,18 +1659,24 @@ BOOST_FIXTURE_TEST_CASE(retired_attestations_do_not_mutate_collateral_or_block_l std::vector entries; for (auto type : {ATTESTATION_TYPE_OPERATOR_ACTION, ATTESTATION_TYPE_SWAP_REQUEST, ATTESTATION_TYPE_UNDERWRITE_INTENT_COMMIT, - ATTESTATION_TYPE_RESERVE_CREATE, ATTESTATION_TYPE_RESERVE_CREATE_CANCEL}) { + ATTESTATION_TYPE_RESERVE_CREATE_CANCEL}) { entries.emplace_back(type, std::string(1, '\x0a')); } entries.emplace_back(ATTESTATION_TYPE_SYNDICATE_LIQ, encode_syndicate_liq(eth, ChainKind::CHAIN_KIND_EVM, uwrit_op_eth_pubkey, liqeth, LIQ_UNIT, 1)); + constexpr int32_t removed_reward_type = 60950; + constexpr int32_t removed_reserve_create_type = 60958; + constexpr int32_t undeclared_type = 65000; const auto trace = deliver_trace(eth, - encode_envelope_with_mixed_attestations(current_epoch(), entries)); + encode_envelope_with_mixed_attestations(current_epoch(), entries, {}, {}, + {removed_reward_type, removed_reserve_create_type, undeclared_type})); BOOST_REQUIRE(trace != nullptr); BOOST_REQUIRE(!trace->except); BOOST_CHECK(get_operator(UWRIT_OP)["balances"].get_array().empty()); BOOST_CHECK(get_wtdw(0).is_null()); + BOOST_CHECK(get_dclaim_row("unmapped"_n, "unmapped_token", 1).is_null()); + BOOST_CHECK(get_dclaim_row("pclaims"_n, "pending_claim", UWRIT_OP.to_uint64_t()).is_null()); BOOST_REQUIRE_EQUAL(1u, synd_items().size()); BOOST_CHECK_EQUAL(LIQ_UNIT, liq_supply()); BOOST_CHECK_EQUAL(LIQ_UNIT, liq_balance(SYND_ACCOUNT)); @@ -2085,7 +2094,8 @@ BOOST_FIXTURE_TEST_CASE(a_shortfall_freezes_the_same_envelopes_queue_step, sysio BOOST_REQUIRE_EQUAL(success(), push(SYND_ACCOUNT, synd_abi, config::system_account_name, "setconfig"_n, mvo() ("chain_code", codename_mvo("ETH"))("token_code", codename_mvo("LIQETH"))("synd_fee_bps", 0) ("desynd_fee_bps", 0)("synd_burst", 1'000'000 * LIQ_UNIT)("synd_refill", 1'000'000 * LIQ_UNIT) - ("desynd_burst", 0)("desynd_refill", 0)("window_sec", 10800)("bounty", 0)("challenge_extra", 0))); + ("desynd_burst", 0)("desynd_refill", 0)("window_sec", 10800)("bounty", 0)("challenge_extra", 0) + ("min_desyndicate", 1))); setup_wire_token(); enable_epoch_advancement(); @@ -2227,46 +2237,18 @@ BOOST_AUTO_TEST_CASE(generic_external_simulator_drives_production_dispatch) { } } -// A real staking-reward envelope crosses msgch -> dclaim -> system -> token. -// Small treasury liquidity deliberately reaches the second fundclaim cap. -BOOST_FIXTURE_TEST_CASE(generic_staking_rewards_fund_claim_and_dedupe_through_dispatch, sysio_dispatch_tester) { +// Funded pre-launch imports link and settle the exact token balance. +BOOST_FIXTURE_TEST_CASE(funded_import_claims_link_and_settle, sysio_dispatch_tester) { using namespace sysio::testing::external; bootstrap_for_dispatch(First.chain, First.kind); deploy(TOKEN_ACCOUNT, contracts::token_wasm(), contracts::token_abi(), token_abi); BOOST_REQUIRE_EQUAL(success(), push(TOKEN_ACCOUNT, token_abi, TOKEN_ACCOUNT, "create"_n, mvo()("issuer", "sysio")("maximum_supply", "100.000000000 WIRE"))); BOOST_REQUIRE_EQUAL(success(), push(TOKEN_ACCOUNT, token_abi, config::system_account_name, "issue"_n, - mvo()("to", "sysio")("quantity", "100.000000000 WIRE")("memo", "reward funding"))); + mvo()("to", "sysio")("quantity", "100.000000000 WIRE")("memo", "import funding"))); enable_epoch_advancement(); - external::chain outpost(First); - constexpr uint64_t Reward = 10 * Unit; - sysio::opp::attestations::StakingReward reward; - reward.set_chain_code(fc::slug_name{First.chain}.value); - reward.mutable_staker_wire_account()->set_name(UWRIT_OP.to_string()); - reward.set_share_bps(10'000); - reward.set_reward_epoch_index(current_epoch()); - reward.set_external_epoch_ref(1); - reward.mutable_reward_amount()->set_token_code(fc::slug_name{"WIRE"}.value); - reward.mutable_reward_amount()->set_amount(Reward); - reward.mutable_staker_native_address()->set_kind(First.kind); - const std::vector address(20, '\x31'); - reward.mutable_staker_native_address()->set_address(address.data(), address.size()); - const auto body = reward.SerializeAsString(); - const auto bytes = outpost.envelope(current_epoch(), { - {ATTESTATION_TYPE_STAKING_REWARD, body}, {ATTESTATION_TYPE_STAKING_REWARD, body}}); - const auto trace = deliver_trace(fc::slug_name{First.chain}.value, bytes); - BOOST_REQUIRE(trace && !trace->except); const auto wire = symbol::from_string("9,WIRE"); const auto balance = [&](name account) { return get_currency_balance(TOKEN_ACCOUNT, wire, account).get_amount(); }; - BOOST_REQUIRE_EQUAL(Reward, get_dclaim_row("pclaims"_n, "pending_claim", UWRIT_OP.to_uint64_t()) - ["balance"].as().get_amount()); - BOOST_REQUIRE_EQUAL(Reward, balance(DCLAIM_ACCOUNT)); - BOOST_REQUIRE_EQUAL(90 * Unit, balance(config::system_account_name)); - BOOST_REQUIRE_EQUAL(success(), push(DCLAIM_ACCOUNT, dclaim_abi, UWRIT_OP, "claim"_n, - mvo()("wire_account", UWRIT_OP))); - BOOST_REQUIRE_EQUAL(Reward, balance(UWRIT_OP)); - BOOST_REQUIRE_EQUAL(0, balance(DCLAIM_ACCOUNT)); - BOOST_REQUIRE(get_dclaim_row("pclaims"_n, "pending_claim", UWRIT_OP.to_uint64_t()).is_null()); // The soak's launch import has its own funding path: pre-fund, import an // unlinked identity, authenticate the link, then claim the exact WIRE credit. constexpr uint64_t Imported = 5 * Unit; @@ -2287,16 +2269,5 @@ BOOST_FIXTURE_TEST_CASE(generic_staking_rewards_fund_claim_and_dedupe_through_di mvo()("wire_account", CLAIM_ACCOUNT))); BOOST_REQUIRE_EQUAL(Imported, balance(CLAIM_ACCOUNT)); BOOST_REQUIRE_EQUAL(0, balance(DCLAIM_ACCOUNT)); - const auto funding_left = balance(config::system_account_name); - // A new reward at the authenticated downstream boundary exceeds liquid - // treasury funds. Credit is retained, but funding cannot overspend custody. - BOOST_REQUIRE_EQUAL(success(), push(DCLAIM_ACCOUNT, dclaim_abi, MSGCH_ACCOUNT, "onreward"_n, - mvo()("chain_code", fc::slug_name{First.chain}.value)("staker_wire_account", UWRIT_OP.to_string()) - ("reward_chain", First.kind)("staker_native_addr", address)("reward_amount", 100 * Unit) - ("reward_epoch_index", current_epoch())("external_epoch_ref", 2)("share_bps", 10'000))); - BOOST_REQUIRE_EQUAL(funding_left, balance(DCLAIM_ACCOUNT)); - BOOST_REQUIRE_EQUAL(0, balance(config::system_account_name)); - BOOST_REQUIRE_EQUAL(100 * Unit, get_dclaim_row("pclaims"_n, "pending_claim", UWRIT_OP.to_uint64_t()) - ["balance"].as().get_amount()); } BOOST_AUTO_TEST_SUITE_END() diff --git a/contracts/tests/sysio.liq_tests.cpp b/contracts/tests/sysio.liq_tests.cpp index 8719902f44..4ead4d78a9 100644 --- a/contracts/tests/sysio.liq_tests.cpp +++ b/contracts/tests/sysio.liq_tests.cpp @@ -23,18 +23,6 @@ using namespace fc; using mvo = fc::mutable_variant_object; using sysio_system::test_support::codename_mvo; -namespace { - -/// The action data of `sysio.system::fundclaim(name recipient, int64_t amount)`, for -/// reading the kicker request out of an addyield trace. -struct fundclaim_args { - name recipient; - int64_t amount; -}; - -} // anonymous namespace -FC_REFLECT( fundclaim_args, (recipient)(amount) ) - /// sysio.liq end to end on the depot: the registries it validates against, the /// swap it feeds, and sysio.synd as the signer of every mint and burn (an account /// only; sysio.msgch is an account too, a signer that is not sysio.synd). The @@ -182,13 +170,6 @@ class sysio_liq_tester : public tester { return push_liq(from, "addyield"_n, mvo()("from", from)("quantity", asset(wire_amount, WIRE_SYM)) ("target", target.to_symbol_code())); } - action_result addkicker(name signer, symbol sym, int64_t base_balance, uint64_t requested) { - return push_liq(signer, "addkicker"_n, mvo()("sym", sym.to_symbol_code())("base_balance", base_balance) - ("requested", requested)); - } - action_result setkicker(name signer, uint32_t bps) { - return push_liq(signer, "setkicker"_n, mvo()("bps", bps)); - } action_result recredit(name holder, int64_t amount, name signer = LIQ_ACCOUNT) { return push_liq(signer, "recredit"_n, mvo()("holder", holder)("quantity", asset(amount, LIQSOL_SYM))); } @@ -252,9 +233,6 @@ class sysio_liq_tester : public tester { fc::variant pending_row(symbol sym = LIQSOL_SYM) { return liq_row("liqpending"_n, "pending_yield", LIQ_ACCOUNT, sym.to_symbol_code().value); } - fc::variant config_row() { - return decode(liq_abi_ser, "liq_config", get_row_by_account(LIQ_ACCOUNT, LIQ_ACCOUNT, "liqconfig"_n, "liqconfig"_n)); - } int64_t wire_balance(name holder) { const auto row = decode(token_abi_ser, "account", get_row_by_id(TOKEN_ACCOUNT, holder, "accounts"_n, WIRE_SYM.to_symbol_code().value)); @@ -482,76 +460,33 @@ BOOST_FIXTURE_TEST_CASE(the_index_grows_past_64_bits, sysio_liq_tester) try { BOOST_REQUIRE_EQUAL(donation, owed("alice"_n)); } FC_LOG_AND_RETHROW() -// --------------------------------------------------------------------------- -// The kicker -// --------------------------------------------------------------------------- - -BOOST_FIXTURE_TEST_CASE(addyield_requests_the_kicker_from_t5, sysio_liq_tester) try { +/// Swap proceeds and donations distribute exactly their funding without drawing from T5. +BOOST_FIXTURE_TEST_CASE(addyield_distributes_only_supplied_wire, sysio_liq_tester) try { + constexpr int64_t intake = 100 * UNIT; BOOST_REQUIRE_EQUAL(success(), mint("alice"_n, 100 * UNIT)); - BOOST_REQUIRE_EQUAL(uint32_t(200), config_row().is_null() ? 200u : config_row()["kicker_bps"].as()); - - // The swap's intake is the one that is yield, so it is the one that earns the - // kicker. Stand in for the pool's proceeds: WIRE deposited on the swap. - BOOST_REQUIRE_EQUAL(success(), openext("alice"_n, "alice"_n, extended_symbol{ WIRE_SYM, TOKEN_ACCOUNT })); - BOOST_REQUIRE_EQUAL(success(), transfer_wire("alice"_n, SWAP_ACCOUNT, 300 * UNIT)); - // 100 WIRE in from `from`; whether the trace carries the 2% fundclaim for - // this contract and the fold that follows it. - const auto intake = [&](name from) { - auto trace = base_tester::push_action(LIQ_ACCOUNT, "addyield"_n, from, mvo() - ("from", from)("quantity", asset(100 * UNIT, WIRE_SYM))("target", LIQSOL_SYM.to_symbol_code())); + BOOST_REQUIRE_EQUAL(success(), openext("alice"_n, "alice"_n, extended_symbol{WIRE_SYM, TOKEN_ACCOUNT})); + BOOST_REQUIRE_EQUAL(success(), transfer_wire("alice"_n, SWAP_ACCOUNT, intake)); + const auto treasury_before = wire_balance(SYSIO_ACCOUNT); + const auto holder_before = wire_balance("alice"_n); + for (const auto from : {SWAP_ACCOUNT, "carol"_n}) { + const auto source_before = wire_balance(from); + const auto trace = base_tester::push_action(LIQ_ACCOUNT, "addyield"_n, from, mvo() + ("from", from)("quantity", asset(intake, WIRE_SYM))("target", LIQSOL_SYM.to_symbol_code())); + BOOST_REQUIRE(trace && !trace->except); produce_block(); - bool requested = false, folded = false; - for (const auto& at : trace->action_traces) { - if (at.act.account == SYSIO_ACCOUNT && at.act.name == "fundclaim"_n) { - const auto args = fc::raw::unpack(at.act.data); - BOOST_REQUIRE_EQUAL(LIQ_ACCOUNT, args.recipient); - BOOST_REQUIRE_EQUAL(2 * UNIT, args.amount); - requested = true; - } - if (at.act.account == LIQ_ACCOUNT && at.act.name == "addkicker"_n) folded = true; + for (const auto& action : trace->action_traces) { + BOOST_CHECK(action.act.account != SYSIO_ACCOUNT); + BOOST_CHECK(action.act.name != "addkicker"_n); } - return std::make_pair(requested, folded); - }; - - // sysio runs no emissions here, so the request lands nothing and the fold is - // a no-op: holders get the base yield and nothing else. - BOOST_REQUIRE(intake(SWAP_ACCOUNT) == std::make_pair(true, true)); - BOOST_REQUIRE_EQUAL(100 * UNIT, owed("alice"_n)); - BOOST_REQUIRE_EQUAL(uint64_t(100 * UNIT), pot()); - - // A donation from anyone else distributes only itself: no request, no fold. - // Otherwise a near-sole holder could donate, claim it back with the kicker on - // top, and repeat against the treasury. - BOOST_REQUIRE(intake("carol"_n) == std::make_pair(false, false)); - BOOST_REQUIRE_EQUAL(200 * UNIT, owed("alice"_n)); - BOOST_REQUIRE_EQUAL(uint64_t(200 * UNIT), pot()); - - // With the kicker off, the swap's intake makes no request either. - BOOST_REQUIRE(mentions(setkicker("alice"_n, 0), "missing authority of sysio")); - BOOST_REQUIRE_EQUAL(wasm_assert_msg("kicker_bps out of range"), setkicker(SYSIO_ACCOUNT, 10'001)); - BOOST_REQUIRE_EQUAL(success(), setkicker(SYSIO_ACCOUNT, 0)); - BOOST_REQUIRE_EQUAL(0u, config_row()["kicker_bps"].as()); - BOOST_REQUIRE(intake(SWAP_ACCOUNT) == std::make_pair(false, false)); -} FC_LOG_AND_RETHROW() - -BOOST_FIXTURE_TEST_CASE(addkicker_folds_only_what_landed, sysio_liq_tester) try { - BOOST_REQUIRE_EQUAL(success(), mint("alice"_n, 100 * UNIT)); - // Stand in for T5: 5 WIRE arrive at the contract. - BOOST_REQUIRE_EQUAL(success(), transfer_wire("carol"_n, LIQ_ACCOUNT, 5 * UNIT)); - const int64_t balance = wire_balance(LIQ_ACCOUNT); - - BOOST_REQUIRE(mentions(addkicker("alice"_n, LIQSOL_SYM, balance - 2 * UNIT, 3 * UNIT), "missing authority of sysio.liq")); - // 2 WIRE over the base, 3 requested: 2 fold in. - BOOST_REQUIRE_EQUAL(success(), addkicker(LIQ_ACCOUNT, LIQSOL_SYM, balance - 2 * UNIT, 3 * UNIT)); - BOOST_REQUIRE_EQUAL(uint64_t(2 * UNIT), pot()); - BOOST_REQUIRE_EQUAL(2 * UNIT, owed("alice"_n)); - // 2 over, 1 requested: only the request folds in. - BOOST_REQUIRE_EQUAL(success(), addkicker(LIQ_ACCOUNT, LIQSOL_SYM, balance - 2 * UNIT, UNIT)); - BOOST_REQUIRE_EQUAL(uint64_t(3 * UNIT), pot()); - // Nothing over the base: nothing folds. - BOOST_REQUIRE_EQUAL(success(), addkicker(LIQ_ACCOUNT, LIQSOL_SYM, balance, UNIT)); - BOOST_REQUIRE_EQUAL(uint64_t(3 * UNIT), pot()); - BOOST_REQUIRE_EQUAL(3 * UNIT, owed("alice"_n)); + BOOST_CHECK_EQUAL(source_before - intake, wire_balance(from)); + BOOST_CHECK_EQUAL(treasury_before, wire_balance(SYSIO_ACCOUNT)); + } + BOOST_REQUIRE_EQUAL(2 * intake, owed("alice"_n)); + BOOST_REQUIRE_EQUAL(static_cast(2 * intake), pot()); + BOOST_REQUIRE_EQUAL(success(), claim("alice"_n)); + BOOST_CHECK_EQUAL(holder_before + 2 * intake, wire_balance("alice"_n)); + BOOST_CHECK_EQUAL(0, pot()); + BOOST_CHECK_EQUAL(0, owed("alice"_n)); } FC_LOG_AND_RETHROW() // --------------------------------------------------------------------------- @@ -559,7 +494,6 @@ BOOST_FIXTURE_TEST_CASE(addkicker_folds_only_what_landed, sysio_liq_tester) try // --------------------------------------------------------------------------- BOOST_FIXTURE_TEST_CASE(queued_yield_sells_through_the_pool_and_pays_holders, sysio_liq_tester) try { - BOOST_REQUIRE_EQUAL(success(), setkicker(SYSIO_ACCOUNT, 0)); BOOST_REQUIRE_EQUAL(success(), mint("alice"_n, 100 * UNIT)); // Nothing pending: queueing is a no-op, and the pool does not exist yet. @@ -623,7 +557,6 @@ BOOST_FIXTURE_TEST_CASE(queued_yield_sells_through_the_pool_and_pays_holders, sy // measures its headroom net of what is parked in liqpending, so queueyield can always // mint it. A report past that headroom is dropped whole, so no value is clipped. BOOST_FIXTURE_TEST_CASE(pending_yield_is_reserved_against_the_asset_range, sysio_liq_tester) try { - BOOST_REQUIRE_EQUAL(success(), setkicker(SYSIO_ACCOUNT, 0)); // The pool first, while the range is open: regliqpool mints the LCO seed. Then one // syndication takes supply to 100 base units under the range. BOOST_REQUIRE_EQUAL(success(), regliqpool(SOLANA, LIQSOL, POOL_SYM, 1000 * UNIT, 1000 * UNIT)); @@ -710,9 +643,8 @@ BOOST_FIXTURE_TEST_CASE(regliqpool_refusals, sysio_liq_tester) try { // Governance reconciliation // --------------------------------------------------------------------------- -// An outpost that refused a de-syndication is reconciled by governance: `recredit` mints the burned -// amount back to the holder, growing the supply by exactly that, and only this contract may sign it. -BOOST_FIXTURE_TEST_CASE(recredit_mints_a_refused_desyndication_back, sysio_liq_tester) try { +/// Exceptional supply repair preserves existing yield and requires the LIQ contract authority. +BOOST_FIXTURE_TEST_CASE(privileged_supply_repair_preserves_existing_yield, sysio_liq_tester) try { BOOST_REQUIRE_EQUAL(success(), mint("alice"_n, 60 * UNIT)); BOOST_REQUIRE_EQUAL(success(), addyield("carol"_n, 6 * UNIT)); diff --git a/contracts/tests/sysio.synd_tests.cpp b/contracts/tests/sysio.synd_tests.cpp index 3705f145ea..0432011017 100644 --- a/contracts/tests/sysio.synd_tests.cpp +++ b/contracts/tests/sysio.synd_tests.cpp @@ -56,6 +56,7 @@ constexpr std::string_view REQUEST_OPEN = "OPEN"; constexpr std::string_view REQUEST_BONDED = "BONDED"; constexpr std::string_view REQUEST_APPROVED = "APPROVED"; constexpr std::string_view REQUEST_HELD = "HELD"; +constexpr std::string_view REQUEST_INVALID = "INVALID"; /// The `total_syndicated` an intake helper carries by default: the whole asset range, at or above any /// outstanding shadow (supply plus parked yield) a test can reach, so an outpost custody of this size /// always covers the depot. @@ -455,14 +456,16 @@ class sysio_synd_tester : public tester { uint32_t window_sec = DEFAULT_WINDOW_SEC; uint64_t bounty = 0; uint64_t challenge_extra = 0; + uint64_t min_desyndicate = 1; ///< Permissive fixture floor for small-value edge cases. }; action_result setconfig(std::string_view chain_code, std::string_view token_code, const pair_config& cfg, name signer = SYSIO_ACCOUNT) { - return push(SYND_ACCOUNT, synd_abi_ser, signer, "setconfig"_n, mvo() + auto data = mvo() ("chain_code", codename_mvo(chain_code))("token_code", codename_mvo(token_code)) ("synd_fee_bps", cfg.synd_fee_bps)("desynd_fee_bps", cfg.desynd_fee_bps)("synd_burst", cfg.synd_burst) ("synd_refill", cfg.synd_refill)("desynd_burst", cfg.desynd_burst)("desynd_refill", cfg.desynd_refill) - ("window_sec", cfg.window_sec)("bounty", cfg.bounty)("challenge_extra", cfg.challenge_extra)); + ("window_sec", cfg.window_sec)("bounty", cfg.bounty)("challenge_extra", cfg.challenge_extra)("min_desyndicate", cfg.min_desyndicate); + return push(SYND_ACCOUNT, synd_abi_ser, signer, "setconfig"_n, data); } action_result dropenv(std::string_view chain_code, std::string_view token_code, uint32_t epoch, name signer = SYSIO_ACCOUNT) { @@ -1255,12 +1258,13 @@ BOOST_FIXTURE_TEST_CASE(liq_has_none_of_the_moved_actions, sysio_synd_tester) tr BOOST_REQUIRE(has_table(synd_abi, "feepot"_n)); for (auto action : { "setconfig"_n, "crank"_n, "dropenv"_n, "sweepyield"_n }) BOOST_TEST_CONTEXT(action.to_string()) { BOOST_REQUIRE(has_action(synd_abi, action)); } - // The import flag left sysio.liq's config with the import. + // Import state moved to syndication, and the bonus config is retired. const auto liq_config = std::find_if(liq_abi.structs.begin(), liq_abi.structs.end(), [](const auto& st) { return st.name == "liq_config"; }); - BOOST_REQUIRE(liq_config != liq_abi.structs.end()); - BOOST_REQUIRE(std::none_of(liq_config->fields.begin(), liq_config->fields.end(), - [](const auto& f) { return f.name == "import_complete"; })); + BOOST_REQUIRE(liq_config == liq_abi.structs.end()); + BOOST_REQUIRE(!has_table(liq_abi, "liqconfig"_n)); + BOOST_REQUIRE(!has_action(liq_abi, "setkicker"_n)); + BOOST_REQUIRE(!has_action(liq_abi, "addkicker"_n)); // What stays in the ledger stays. for (auto action : { "mint"_n, "burn"_n, "mintyield"_n, "creditowed"_n, "transfer"_n, "claim"_n, "recredit"_n, "regliqpool"_n }) @@ -1592,6 +1596,43 @@ BOOST_FIXTURE_TEST_CASE(desyndicate_carries_the_outstanding_shadow_after_its_bur BOOST_REQUIRE_EQUAL(wasm_assert_msg("supply exceeds the asset range"), liq_mint("alice"_n, LIQSOL, 1)); } FC_LOG_AND_RETHROW() +/// The gross return minimum rejects dust before custody, budgets or request ids change. +BOOST_FIXTURE_TEST_CASE(desyndicate_enforces_configured_minimum, + sysio_synd_tester) try { + constexpr uint64_t minimum = 10 * UNIT; + BOOST_REQUIRE_EQUAL(success(), liq_mint("alice"_n, LIQSOL, 100 * UNIT)); + BOOST_REQUIRE_EQUAL(success(), link_svm("alice"_n, ed_key())); + BOOST_REQUIRE_EQUAL(success(), setconfig(SOLANA, LIQSOL, + {.desynd_fee_bps = 1000, .desynd_burst = 100 * UNIT, .min_desyndicate = minimum})); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("quantity is below the minimum desyndication"), + desyndicate("alice"_n, minimum - 1)); + BOOST_CHECK_EQUAL(100 * UNIT, liq_balance("alice"_n)); + BOOST_CHECK_EQUAL(100 * UNIT, supply()); + BOOST_CHECK(desynd_bucket_row(SOLANA, LIQSOL).is_null()); + BOOST_REQUIRE_EQUAL(success(), setconfig(SOLANA, LIQSOL, {.desynd_fee_bps = 1000, .min_desyndicate = minimum})); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("quantity is below the minimum desyndication"), + desyndicate("alice"_n, minimum - 1)); + BOOST_REQUIRE_EQUAL(success(), desyndicate("alice"_n, minimum)); + BOOST_CHECK_EQUAL(9 * UNIT, queued_desyndication(*this, 1).amount().amount()); + BOOST_CHECK_EQUAL(90 * UNIT, liq_balance("alice"_n)); + BOOST_REQUIRE_EQUAL(success(), setconfig(SOLANA, LIQSOL, {.min_desyndicate = 1})); + BOOST_REQUIRE_EQUAL(success(), desyndicate("alice"_n, 1)); +} FC_LOG_AND_RETHROW() + +/// Governance cannot configure zero, an unrepresentable minimum, or one above an enabled burst. +BOOST_FIXTURE_TEST_CASE(setconfig_validates_minimum_return, sysio_synd_tester) try { + constexpr auto message = "minimum desyndication must be positive and fit the burst and asset range"; + BOOST_REQUIRE_EQUAL(wasm_assert_msg(message), setconfig(SOLANA, LIQSOL, {.min_desyndicate = 0})); + BOOST_REQUIRE_EQUAL(wasm_assert_msg(message), setconfig(SOLANA, LIQSOL, + {.desynd_burst = 0, .min_desyndicate = static_cast(asset::max_amount) + 1})); + BOOST_REQUIRE_EQUAL(wasm_assert_msg(message), setconfig(SOLANA, LIQSOL, + {.desynd_burst = UNIT, .min_desyndicate = UNIT + 1})); + BOOST_REQUIRE_EQUAL(success(), setconfig(SOLANA, LIQSOL, {.desynd_burst = 0, .min_desyndicate = UNIT})); + BOOST_REQUIRE_EQUAL(success(), setconfig(SOLANA, LIQSOL, {.desynd_burst = UNIT, .min_desyndicate = UNIT})); + BOOST_REQUIRE_EQUAL(success(), setconfig(SOLANA, LIQSOL, + {.desynd_burst = 0, .min_desyndicate = static_cast(asset::max_amount)})); +} FC_LOG_AND_RETHROW() + // The fee, `quantity * desynd_fee_bps / 10000` floored, stays in `feepot` in sysio.synd's own row; only // the net rest is burned, queued, logged and summed, and the bucket drops by the whole quantity. A fee of // the whole quantity leaves nothing to release on the outpost and is refused. @@ -2925,6 +2966,44 @@ BOOST_FIXTURE_TEST_CASE(challenge_refused_on_an_approved_request, sysio_synd_tes BOOST_REQUIRE_EQUAL(REQUEST_APPROVED, bond_request(id)["state"].as_string()); } FC_LOG_AND_RETHROW() +/// Approval eligibility does not itself close interventions while the request remains BONDED. +BOOST_FIXTURE_TEST_CASE(a_frozen_request_can_be_held_or_ruled_invalid_after_its_deadline, sysio_synd_tester) try { + constexpr uint32_t window = 60; + BOOST_REQUIRE_EQUAL(success(), setconfig(SOLANA, LIQSOL, {.window_sec = window})); + BOOST_REQUIRE_EQUAL(success(), liq_mint("carol"_n, LIQSOL, 5 * UNIT)); + syndicate_and_close(1, 1, ed_bytes(ed_key()), LIQSOL, 10 * UNIT); + underwrite_envelope(SOLANA, LIQSOL, 1); + BOOST_REQUIRE_EQUAL(success(), andon_pull()); + produce_block(fc::seconds(window + 1)); + BOOST_REQUIRE_EQUAL(success(), challenge("carol"_n, SOLANA, LIQSOL, 1)); + BOOST_REQUIRE_EQUAL(REQUEST_HELD, bond_request(1)["state"].as_string()); + BOOST_REQUIRE_EQUAL(success(), push(BOND_ACCOUNT, bond_abi_ser, SYSIO_ACCOUNT, "rslvinvalid"_n, + mvo()("request_id", 1))); + BOOST_REQUIRE_EQUAL(REQUEST_INVALID, bond_request(1)["state"].as_string()); +} FC_LOG_AND_RETHROW() + +/// The cord pauses releases, but approval after the deadline permanently closes challenges. +BOOST_FIXTURE_TEST_CASE(a_freeze_does_not_extend_the_challenge_deadline, sysio_synd_tester) try { + constexpr uint32_t window = 60; + const auto key = ed_key(); + BOOST_REQUIRE_EQUAL(success(), link_svm("alice"_n, key)); + BOOST_REQUIRE_EQUAL(success(), setconfig(SOLANA, LIQSOL, {.window_sec = window})); + syndicate_and_close(1, 1, ed_bytes(key), LIQSOL, 10 * UNIT); + const auto id = envelope_row(SOLANA, LIQSOL, 1)["request_id"].as_uint64(); + underwrite_envelope(SOLANA, LIQSOL, 1); + BOOST_REQUIRE_EQUAL(success(), liq_mint("carol"_n, LIQSOL, 5 * UNIT)); + BOOST_REQUIRE_EQUAL(success(), andon_pull()); + produce_block(fc::seconds(window + 1)); + BOOST_REQUIRE_EQUAL(success(), push(BOND_ACCOUNT, bond_abi_ser, "carol"_n, "approve"_n, mvo()("request_id", id))); + BOOST_REQUIRE_EQUAL(REQUEST_APPROVED, bond_request(id)["state"].as_string()); + BOOST_REQUIRE(mentions(challenge("carol"_n, SOLANA, LIQSOL, 1), CHALLENGE_FINAL)); + BOOST_REQUIRE_EQUAL(success(), crank()); + BOOST_CHECK_EQUAL(0, liq_balance("alice"_n)); + BOOST_REQUIRE_EQUAL(success(), andon_clear()); + BOOST_REQUIRE_EQUAL(success(), crank()); + BOOST_CHECK_EQUAL(10 * UNIT, liq_balance("alice"_n)); +} FC_LOG_AND_RETHROW() + // Review Focus 4: an envelope with no request issued yet (WAITING behind an unbonded one), or with no // row at all, cannot be challenged; nor can sysio.synd, sysio.bond or an account with contract code // challenge. @@ -4016,7 +4095,7 @@ BOOST_FIXTURE_TEST_CASE(a_released_yield_counts_before_it_is_queued, sysio_synd_ // `sysio.liq::recredit`, which grows the supply directly, and the outpost still holds what it never paid. // The next comparison sees the recredit with nothing recorded in `sysio.synd`: custody 20 matches // 16 + 4 exactly, where a check that missed the recredit would read an excess of 4. -BOOST_FIXTURE_TEST_CASE(a_recredit_is_seen_by_the_next_comparison, sysio_synd_tester) try { +BOOST_FIXTURE_TEST_CASE(a_return_refund_is_seen_by_the_next_comparison, sysio_synd_tester) try { const auto pubkey = ed_bytes(ed_key()); constexpr auto SVM = ChainKind::CHAIN_KIND_SVM; BOOST_REQUIRE_EQUAL(success(), liq_mint("alice"_n, LIQSOL, 20 * UNIT)); // released: custody 20 @@ -4035,7 +4114,7 @@ BOOST_FIXTURE_TEST_CASE(a_recredit_is_seen_by_the_next_comparison, sysio_synd_te MSGCH_ACCOUNT, 21 * UNIT)); BOOST_REQUIRE(!console_has(EXCESS)); require_no_shortfall(); - // The recredit wrote nothing to the ledger's running sums: the check does not read them. + // The return refund wrote nothing to the ledger's running sums: the check does not read them. } FC_LOG_AND_RETHROW() // Review focus 2: only an admitted message is compared. A replayed sequence, or a message dropped for any @@ -4303,7 +4382,6 @@ BOOST_FIXTURE_TEST_CASE(generic_parked_credit_yield_conversion_and_fee_rounded_r constexpr uint64_t PoolSeed = 1'000 * UNIT; constexpr uint32_t Horizon = 86'400, DepthBps = 300; constexpr int64_t ClipFloor = 1000; - BOOST_REQUIRE_EQUAL(success(), push(LIQ_ACCOUNT, liq_abi_ser, SYSIO_ACCOUNT, "setkicker"_n, mvo()("bps", 0))); for (size_t i = 0; i < outposts.size(); ++i) { BOOST_TEST_CONTEXT("chain=" << outposts[i].asset().chain) { auto& external = outposts[i]; diff --git a/docs/contract-upgrade-order.md b/docs/contract-upgrade-order.md index 8a0b74c41c..3e5555a818 100644 --- a/docs/contract-upgrade-order.md +++ b/docs/contract-upgrade-order.md @@ -148,7 +148,7 @@ WIRE-352 adds separate inline edges whose failure behavior depends on the caller | `sysio.synd::challenge` | `sysio.liq::transfer`, `sysio.bond::hold` | Deploy `sysio.bond` before or with `sysio.synd`. | A signed action: a missing or refusing callee fails only the challenge, which the challenger can retry. | | Andon custody readers: swap, liq, bond, synd | Three-field `cord` row | Deploy all five from the same revision on a fresh deployment; configure native pull/clear permissions. | Existing cord rows use an incompatible layout; no in-place migration is supplied. | | Syndication custody shortfall | Andon `pull(reason)` with `sysio.andon@active` | Deploy Andon before syndication carries traffic. Privileged syndication needs no custom pull permission. | Missing account/permission or incompatible code fails the transaction atomically. | -| `sysio.synd` reading its own `mismatch` rows | its own table: `mismatch` is new | Deploy this `sysio.synd` fresh, as the row below requires. | Rows accumulate with no prune; nothing else reads them. | +| `sysio.synd` reading its own `mismatch` rows | its own table: `mismatch` is new | Deploy this `sysio.synd` fresh, as the row below requires. | One latest incident per chain/token; a new shortfall overwrites it. Snapshot evidence externally before governance reconciliation erases it. | | Syndication envelope and bucket rows | Current ABI layouts | Deploy fresh without rows from earlier layouts. | Buckets now store level and last epoch only; no frozen-epoch marker or migration is included. | Production deployment through `sysio.roa::setsyscode` privileges `sysio.dclaim` @@ -321,3 +321,17 @@ The safe procedure is therefore: code order alone. If a rollback has to happen anyway, the outstanding balances are a liability to settle deliberately, not a detail the deploy sequence absorbs. + +## PR 662 follow-on pre-launch deployment + +Deploy this revision on fresh state and regenerate the OPP models and SDK contract types together. +There is no migration or compatibility path for earlier development deployments. Recreate development +clusters rather than decoding their former configuration and emissions rows. + +The external staking-reward message, dispatch, DClaim reward cursor/counter, T5 bonus funding action +and shortfall counter, and LIQ bonus actions/configuration are removed. Funded pre-launch imports, +LIQ earned-yield distribution and their normal claim actions remain. + +`sysio.synd::setconfig` requires `min_desyndicate` as an ordinary uint64, and `syndconfig` stores it +as an ordinary field. Bootstrap schema version 1 contains chains, tokens, LIQ pools and syndications; +it has no external-reserve or underwriting configuration. diff --git a/docs/emergency-stop-playbook.md b/docs/emergency-stop-playbook.md index a510901aad..b1fa566467 100644 --- a/docs/emergency-stop-playbook.md +++ b/docs/emergency-stop-playbook.md @@ -42,16 +42,17 @@ and `OLD_ETH_PANIC` (when the panic account is replaced), `PAUSE_BLOCK` (the Eth first `pause()`), `REQUEST_ID` (a deferred desyndication being paid), `RELAY_SIGNER` (a Solana relay signer's public key), and, for an envelope in doubt, `CHALLENGER`, `CHAIN_CODE`, `TOKEN_CODE`, `EPOCH_INDEX` and `BOND_REQUEST_ID` (see [Holding and ruling](#holding-and-ruling-steps-5-and-6)), and, -for a skipped desyndication being recredited, `HOLDER` and `QUANTITY` (see -[Reconciling](#reconciling-a-desyndication-the-outpost-did-not-pay)). +for return resolution, `REQUEST_ID` alone (see +[Reconciling](#reconciling-a-desyndication-the-outpost-did-not-pay)). Pair-level custody reconciliation +uses `CHAIN_CODE`, `TOKEN_CODE` and `REPORTED` (verified live custody in base units). Ethereum transactions below are sent with Foundry's `cast` and a keystore named with `--account` (`--ledger` works the same way). `SyndicationPool` is not deployed in any environment yet (wire-ethereum `docs/deploy.md`, Rollout); until it is, skip its lines. Every `clio get table` is written in the v6 form `clio get table -S `. The tables -read here (`sysio.andon` `cord` and `andonconfig`, `sysio.synd` `envelopes`, `ledger`, `syndcursors` and -`desyndlog`, `sysio.epoch` `epochstate`, `sysio.msgch` `envlog`) are unscoped KV tables stored under +read here (`sysio.andon` `cord`, `sysio.synd` `envelopes`, `ledger`, `syndcursors` and +`returns`, `sysio.bond` `requests`, `sysio.epoch` `epochstate`, `sysio.msgch` `envlog`) are unscoped KV tables stored under their contract's own account, so the scope is the contract account; rows come back as `{key, value}`. Depot actions signed by `sysio` are shown as `-p sysio@active`. Where `sysio` is held by a multisig, @@ -59,6 +60,11 @@ propose the same action and data with `clio multisig propose` and execute it onc ## Before an incident +Fund each designated challenger with the relevant shadow LIQ token before a freeze, enough for +the request hold bond plus the pair's `challenge_extra`. While frozen, `sysio.liq::transfer` +refuses funding an ordinary challenger account; transfers from an already funded challenger +into protocol custody remain allowed. + Each item is a deploy or upgrade step. An item not done is a freeze that cannot be pulled. 1. **Depot.** `sysio.andon` is deployed privileged before `sysio.swap`, `sysio.liq`, `sysio.bond` and @@ -227,15 +233,19 @@ Pausing a paused contract reverts (`EnforcedPause`), which is harmless: it was a | 2 | Replace the panic account in case its key is the fault ([Replacing the panic account](#replacing-the-panic-account)). | Skip. | | 3 | Record the block, the epoch and the last accepted envelope digest on each chain ([Recording](#recording-the-state-of-each-chain)). | Same. | | 4 | Read the depot's `mismatch` table and each outpost's shortfall records, and compare each outpost's custody with the depot's outstanding shadow ([Comparing totals](#comparing-totals), [A shortfall-triggered pull](#a-shortfall-triggered-pull)). | Same. No new `mismatch` row; custody covers the outstanding. | -| 5 | For each envelope in doubt, place a hold (`sysio.synd::challenge`), or rule it invalid (`sysio.bond::rslvinvalid`) if it is unbonded. | None. | +| 5 | Before each request's challenge window ends, hold every doubtful OPEN/BONDED request (`sysio.synd::challenge`) or obtain a governance invalid ruling (`sysio.bond::rslvinvalid`). | None. | | 6 | `sysio` rules each held request (`rslvvalid` / `rslvinvalid`). Invalid envelopes burn as in §4.7 once the cord clears. | None. | | 7 | Fix and redeploy the faulty contract while still frozen, in the order of [`contract-upgrade-order.md`](contract-upgrade-order.md). | None. | -| 8 | Repair custody and clear the outposts first. Have each repaired outpost emit a post-repair message (or wait for its next one); wait for depot admission, verify its sequence in `syndcursors` and no `mismatch` row for it, then clear the depot cord and do the work that waited ([Clearing](#clearing-outposts-first-then-the-depot)). | Same admission check before clearing the depot. | +| 8 | Repair custody and clear the outposts first. Have each repaired outpost emit a post-repair message (or wait for its next one); wait for depot admission, verify its admission and healthy custody report, run governance `reconcile` for every affected pair, verify its incident is erased, then clear the depot cord and do the work that waited ([Clearing](#clearing-outposts-first-then-the-depot)). | Same admission check before clearing the depot. | | 9 | Continue monitoring admitted messages from each outpost for `mismatch` rows and `SHORTFALL` ([Comparing totals](#comparing-totals)). Record earlier pre-repair sequences reporting the known shortfall as part of this incident. | Same. | | 10 | Write up the cause and what was burned or returned ([What to record](#what-to-record)). | Write up what triggered the pull. | -Challenge windows keep counting during a freeze and challenges stay open, so step 5 can be done while -frozen. A frozen queue step still issues requests, so `crank` during the freeze gives every WAITING +Challenge windows keep counting during a freeze. Read `sysio.bond::requests`: a BONDED request +becomes eligible for permissionless approval at `bonded_at + window_sec`; an OPEN request has +no running challenge deadline yet. `approve` ignores the cord. A challenge or `rslvinvalid` +still works after the deadline while the request remains OPEN/BONDED, but approval can win +that race at any time. Once APPROVED, neither intervention is allowed and held items release +after clear. Act before approval eligibility whenever possible. A frozen queue step still issues requests, so `crank` during the freeze gives every WAITING envelope a request to hold or rule. ### Holding and ruling (steps 5 and 6) @@ -243,6 +253,7 @@ envelope a request to hold or rule. ```bash clio -u "$DEPOT_URL" get table sysio.synd envelopes -S sysio.synd -r -l 50 # chain_code, token_code, epoch_index, state, request_id clio -u "$DEPOT_URL" push action sysio.synd crank '[256]' -p "$PANIC@active" # issues the requests of WAITING envelopes +clio -u "$DEPOT_URL" get table sysio.bond requests -S sysio.bond -l 100 # match request_id; state, bonded_at, window_sec; read every result page clio -u "$DEPOT_URL" push action sysio.synd challenge \ "{\"challenger\": \"$CHALLENGER\", \"chain_code\": $CHAIN_CODE, \"token_code\": $TOKEN_CODE, \"epoch_index\": $EPOCH_INDEX}" \ -p "$CHALLENGER@active" @@ -253,8 +264,7 @@ clio -u "$DEPOT_URL" push action sysio.bond rslvvalid "[$BOND_REQUEST_ID]" -p `CHAIN_CODE` and `TOKEN_CODE` are the `chain_code` and `token_code` objects exactly as the `envelopes` row prints them, `EPOCH_INDEX` its `epoch_index`, and `BOND_REQUEST_ID` its `request_id`. `challenge` accepts only an envelope in state REQUESTED, RELEASABLE or DONE whose request is OPEN or BONDED: a -WAITING envelope has no request yet, so run `crank` first. `CHALLENGER` must be an account with no -contract code, and not `sysio.synd` or `sysio.bond`. The challenger pays the request's hold bond plus the pair's `challenge_extra`; both transfers go into +WAITING envelope has no request yet, so run `crank` first. `CHALLENGER` must not be `sysio.synd` or `sysio.bond`; contract accounts are supported through ledger settlement. The challenger pays the request's hold bond plus the pair's `challenge_extra`; both transfers go into custody, so the challenge runs while the depot is frozen. `rslvinvalid` rules a request bonded or not, so an unbonded envelope in doubt is ruled invalid directly. The queue records each ruling on its next step; while the cord is pulled the burn of an INVALID envelope and the forward of a VALID ruling's hold @@ -317,11 +327,11 @@ of the token (the `sysio.liq` supply plus the yield in `liqpending`) as it admit it is written to `mismatch` and pulls the cord. Custody above it is normal and only printed as `EXCESS`. ```bash -clio -u "$DEPOT_URL" get table sysio.synd mismatch -S sysio.synd -l 100 # every shortfall: chain_code, token_code, epoch_index, sequence, kind, reported, expected, at +clio -u "$DEPOT_URL" get table sysio.synd mismatch -S sysio.synd -l 100 # latest unresolved incident per pair: chain_code, token_code, epoch_index, sequence, kind, reported, expected, at clio -u "$DEPOT_URL" get table sysio.liq stat -S sysio.liq # supply per shadow symbol clio -u "$DEPOT_URL" get table sysio.liq liqpending -S sysio.liq # released yield not yet queued clio -u "$DEPOT_URL" get table sysio.synd syndcursors -S sysio.synd # last_sequence admitted per outpost -clio -u "$DEPOT_URL" get table sysio.synd desyndlog -S sysio.synd -r -l 20 # total_syndicated each desyndication carried +clio -u "$DEPOT_URL" get table sysio.synd returns -S sysio.synd -r -l 20 # outstanding unpaid return obligations cast call --rpc-url "$ETH_RPC_URL" "$SYNDICATION_POOL" 'poolBalanceDepot()(uint64)' cast call --rpc-url "$ETH_RPC_URL" "$SYNDICATION_POOL" 'liqSequence()(uint64)' ``` @@ -331,23 +341,25 @@ and `status` on Solana (`liq_sequence`, the pool ATA balance). What to check: - Each `mismatch` row names the outpost, token and sequence of the message that reported the shortfall, the custody it reported and the outstanding it was compared with (`expected`). Match the sequence to the outpost's `synd` call (Solana) or `Syndicated` / yield event (Ethereum) to find the envelope. - Rows are never pruned; note the last one read, so the next reading shows only new rows. + There is one row per `(chain_code, token_code)`: each new shortfall overwrites it. Snapshot + every observed row and its admission trace externally. Only governance `reconcile` erases it; + a later healthy report does not erase the incident. - Now: each outpost's custody (the Solana pool ATA, `poolBalanceDepot`) is at least the depot's outstanding for its token (`supply` plus `liqpending`). Messages in flight only widen the margin: an - unpaid or pending desyndication, an INVALID burn and a `recredit` all leave custody above the + unpaid or pending desyndication, an INVALID burn and a request-keyed `refundreturn` all leave custody above the outstanding. - `syndcursors.last_sequence` for an outpost is at most the outpost's sequence (`liqSequence` / `liq_sequence`); the gap is messages the depot has not yet admitted, and so not yet compared. -- Every `desyndlog` request id the outpost has not paid is still in flight on the depot, stored as a +- Every `returns` request id the outpost has not paid is still in flight on the depot, stored as a pending payout on the outpost ([Deferred desyndications](#deferred-desyndications)), or skipped by the - outpost; a skipped one is reconciled by the recredit rule + outpost; a permanently cancelled one is reconciled by the return recovery rule ([Reconciling](#reconciling-a-desyndication-the-outpost-did-not-pay)). - Between two readings during a freeze, no outpost custody balance falls. ### Reconciling a desyndication the outpost did not pay -Match every `desyndlog` request id with the outpost's record of it. Each one is paid, stored or skipped +Match every `returns` request id with the outpost's record of it. Each one is paid, stored or skipped ([`sysio-synd.md`](sysio-synd.md), Desyndication): - **Paid**: Solana logs `opp_outpost: desyndicate_liq: paid ... request_id=` (wire-solana @@ -357,37 +369,51 @@ Match every `desyndlog` request id with the outpost's record of it. Each one is `desyndicationsPaid(requestId)` is true. - **Stored**: a `PendingPayout` exists at `["pending_desyndication", request_id_le8]` (`npx ts-node scripts/wire-config/emergencyStop.ts pending`), or `pendingDesyndications(requestId)` has a non-zero `depotAmount`. Pay it - ([Deferred desyndications](#deferred-desyndications)). Never recredit it. + ([Deferred desyndications](#deferred-desyndications)). Keep the depot return row; never refund a pending or still-payable payout. - **Skipped**: Solana logged `opp_outpost: DesyndicateLIQError ... request_id= err_data=` (`inbound.rs:4986-4994`) for a content or configuration refusal; Ethereum emitted `DesyndicationDropped(requestId, depotAmount, reason)` with `wrong chain`, `wrong token`, `amount out of range` or `unresolvable recipient` (wire-ethereum `contracts/outpost/SyndicationPool.sol:594-618` at d4fbb8f8). -**The recredit rule.** Governance returns the shadow of a skipped desyndication with -`sysio.liq::recredit`, and only after confirming the outpost holds no record of the request and never -paid it: +**The return recovery rule.** Read the request in `sysio.synd::returns`, which retains its original +holder, token and net burned amount. Establish the external outcome with transaction finality and +the outpost's paid/pending records. A timeout, absence of a pending record, a duplicate event or a +rejected submission alone does not prove cancellation: a queued or replayable payout may still pay. + +- **Paid and final:** governance calls `finishreturn(request_id)` to erase the depot obligation; + it mints nothing. +- **Pending, stored or still payable:** keep the depot row and pay/retry on the outpost. Never refund. +- **Irreversibly rejected or cancelled:** only after proving nothing was paid and no payable external + obligation remains, governance calls `refundreturn(request_id)`. It restores exactly the net burned + amount to the original holder and erases the row atomically. A duplicate refund fails. The fee + stays collected. Governance can resolve returns while the depot is frozen: restoring shadow + mints into custody accounting without paying out external assets. ```bash -# Solana: the PendingPayout PDA of REQUEST_ID must not exist -ANCHOR_PROVIDER_URL="$SOL_RPC" ANCHOR_WALLET="$SOL_CRANK_KEYPAIR" npx ts-node scripts/wire-config/emergencyStop.ts pending # REQUEST_ID not listed -# Ethereum: nothing stored (depotAmount 0) and never paid (false) -cast call --rpc-url "$ETH_RPC_URL" "$SYNDICATION_POOL" 'pendingDesyndications(uint64)(address,uint64,uint8)' "$REQUEST_ID" +clio -u "$DEPOT_URL" get table sysio.synd returns -S sysio.synd -r -l 50 cast call --rpc-url "$ETH_RPC_URL" "$SYNDICATION_POOL" 'desyndicationsPaid(uint64)(bool)' "$REQUEST_ID" -# The depot: the net amount of the request, then the recredit to the holder who signed the desyndicate -clio -u "$DEPOT_URL" get table sysio.synd desyndlog -S sysio.synd -r -l 50 # the row whose request_id is REQUEST_ID -clio -u "$DEPOT_URL" push action sysio.liq recredit "[\"$HOLDER\", \"$QUANTITY\"]" -p sysio.liq@active +cast call --rpc-url "$ETH_RPC_URL" "$SYNDICATION_POOL" 'pendingDesyndications(uint64)(address,uint64,uint8)' "$REQUEST_ID" +ANCHOR_PROVIDER_URL="$SOL_RPC" ANCHOR_WALLET="$SOL_ADMIN_KEYPAIR" npx ts-node scripts/wire-config/emergencyStop.ts pending +``` + +After external payment is final, acknowledge only that outcome: + +```bash +clio -u "$DEPOT_URL" push action sysio.synd finishreturn "[$REQUEST_ID]" -p sysio@active +``` + +After irreversible cancellation is independently proven, with nothing paid or still payable, refund instead: + +```bash +clio -u "$DEPOT_URL" push action sysio.synd refundreturn "[$REQUEST_ID]" -p sysio@active ``` -`recredit` is signed by `sysio.liq` itself (`contracts/sysio.liq/src/sysio.liq.cpp:86`); `QUANTITY` is the -`desyndlog` row's `amount` in the shadow symbol. A stored release is paid by the crank: recrediting it as -well pays the holder twice, and the crank's payment then takes custody down by the amount the recredit -added to the outstanding. The Ethereum drops `already paid` and `already pending` -(`SyndicationPool.sol:609-612`, `:638-641`, `:687-690`) and Solana's `DesyndicateLIQAlreadyPending` -(`inbound.rs:5361-5378`) are -re-emits of an id already paid or stored: never recredit them either. A recredit writes nothing in -`sysio.synd`; the next comparison reads the grown supply, and the custody the outpost never paid out -covers it. +`sysio.liq::recredit` is a separate privileged repair and must not recover a tracked return: it leaves +the `returns` row open, allowing a later refund to mint the same amount again. The Ethereum drops +`already paid` / `already pending` and Solana's `DesyndicateLIQAlreadyPending` are duplicate notices, +never evidence of irreversible cancellation. Retain the incident's paid/cancelled transaction evidence +externally; completed depot return rows are erased rather than archived. ### A shortfall-triggered pull @@ -405,17 +431,18 @@ may have crossed. **Depot.** A cord pulled by `sysio.synd` is identifiable in the action trace and has a reason of the form `custody shortfall seq `. Start at step 3 of [the playbook](#the-playbook): the -`mismatch` row for `(, )` has the figures (`reported`, `expected`). The message that reported -it was still held and minted, and every later message is compared too: each further shortfall adds its -own row while the cord stays pulled (`CORD ALREADY PULLED -- the shortfall is recorded` on the console). - -Repairing custody does not change a `SyndicateLIQ` or `LIQYield` already built: it still carries the -pre-repair custody. If admitted after the depot cord is cleared, that message can write a `mismatch` -row and pull the cord again. Repair custody, have the repaired outpost emit a message after the repair -(or wait for its next one), then wait for the depot to admit that message and confirm no `mismatch` -row for its sequence before clearing the depot cord. Use the `syndcursors` check under -[Clearing](#clearing-outposts-first-then-the-depot). Mismatch rows for earlier pre-repair sequences -reporting the known shortfall are expected: record them under this incident, not as a new incident. +`mismatch` row for `(chain_code, token_code)` has the figures (`reported`, `expected`). +The message that reported it was still held and minted. Each later shortfall overwrites that pair's +row, even while the cord remains pulled. Snapshot every observed overwrite and its admission trace +externally; the table is not an incident history. + +Repairing custody does not change reports already built with pre-repair custody. Such a report can +write another incident and pull the cord again. Verify admission of a post-repair report, snapshot the +latest incident, then call governance `reconcile(chain_code, token_code, reported)` with verified live +custody covering current outstanding. A healthy report does not erase the incident. Verify every +affected pair is reconciled before clearing the depot cord; a later stale shortfall requires the same +verification and reconciliation again. Use the admission checks under +[Clearing](#clearing-outposts-first-then-the-depot). Andon must be deployed before processing envelopes. A failed inline pull aborts the transaction atomically when the account or declared permission is missing. Bootstrap must @@ -472,7 +499,7 @@ raised on both sides. Recovery at v1 is a top-up. Send liqETH to the pool until covers the depot's outstanding plus every stored release (a plain transfer: a donation only raises custody), then unpause the pool and pay the stored releases. Have the repaired outpost emit a post-repair message (or wait for its next one), wait for depot admission and confirm no `mismatch` row -for it as below; only then clear the depot cord. A governance +for the pair after governance reconciliation below; only then clear the depot cord. A governance write-down of the depot's outstanding instead is pending the owner's ruling. ## Clearing: outposts first, then the depot @@ -517,15 +544,30 @@ clio -u "$DEPOT_URL" get table sysio.synd mismatch -S sysio.synd ``` For the repaired outpost's `chain_code`, wait until `syndcursors.last_sequence` reaches the post-repair -message's `sequence`, and confirm there is no `mismatch` row with that `(chain_code, sequence)`. +message's `sequence`. Snapshot the current incident for each `(chain_code, token_code)`; +a healthy report does not erase its earlier mismatch. Confirm the post-repair admission trace has no +new shortfall. + Read all result pages when checking these tables. Confirm the message was actually admitted: if the cursor has already advanced past it, a lower-sequence message can be dropped as a replay, so the cursor alone plus an absent mismatch is not proof that this message passed the custody check. Use its admission trace, or observe a later post-repair message's admission and check that sequence. A mismatch on the post-repair message means recovery is not complete; investigate before clearing. -Earlier pre-repair sequences can add mismatch rows while waiting: retain and record them as evidence -of this incident, not as a new incident. Once the post-repair admission check passes for every repaired -outpost, clear the depot cord: +Earlier pre-repair sequences can overwrite the pair's mismatch while waiting: snapshot each one +externally. After a confirmed healthy post-repair admission, set `CHAIN_CODE` and `TOKEN_CODE` to the +objects printed by the row and `REPORTED` to independently verified live custody in base units: + +```bash +clio -u "$DEPOT_URL" push action sysio.synd reconcile \ + "{\"chain_code\": $CHAIN_CODE, \"token_code\": $TOKEN_CODE, \"reported\": $REPORTED}" -p sysio@active +clio -u "$DEPOT_URL" get table sysio.synd mismatch -S sysio.synd +``` + +`reconcile` requires reported custody to cover current outstanding shadow and erases only this +pair's incident. It is a trusted governance attestation, not an on-chain external measurement, and +does not clear the cord. Repeat for every affected pair. Any newly admitted shortfall requires another +repair; read all table pages and ensure every affected pair's incident is absent before clearing. +Once these checks pass for every repaired outpost, clear the depot cord: ```bash clio -u "$DEPOT_URL" push action sysio.andon clear "[\"$NOTE\"]" -p sysio.andon@clear @@ -557,7 +599,7 @@ A desyndication an outpost stored instead of paying (while it was frozen, on a c on a refused settlement) was burned on the depot and is held on the outpost. Neither relay pays it: paying it is this step, once its cause clears (for a freeze or a shortfall, after the outpost clears). Each record is paid exactly once; a second payment fails. Match each one against the depot's -`desyndlog` by request id. +`returns` by request id. ### Paying on Solana @@ -700,14 +742,16 @@ Keep one record per incident, real or false alarm: - the `mismatch` rows and the custody and outstanding readings of [Comparing totals](#comparing-totals), at the pull and after the first envelope from each outpost following the clear; for a custody repair, also the post-repair message's chain and sequence, emission and admission evidence, the observed `syndcursors.last_sequence`, - the absence of its `mismatch` row before clearing, and earlier pre-repair mismatch rows; + the governance reconciliation transaction, absence of each affected pair's incident before clearing, + and external snapshots of earlier pre-repair mismatch rows; - for a real incident: each envelope held or ruled, its request id and outcome, what was burned (`envelopes.burned`) and what was returned; each redeploy; - every deferred desyndication: request id, amount, reason, the transaction that paid it, and any still unpaid with its refusal; for a `CustodyShortfall` / `CUSTODY_SHORTFALL` record, the two balances it carries; -- every skipped desyndication recredited: request id, holder, quantity, and the reads that showed no - record and no payment; +- every return finished or refunded: request id, original holder, net amount, and final external + evidence proving final payment or irreversible cancellation with nothing paid and no payable + external obligation remaining; - whether the panic account was replaced, and the new one; - for a false alarm: what triggered the pull and what, if anything, should change so it does not recur. diff --git a/docs/platform-bootstrap-config.md b/docs/platform-bootstrap-config.md index 95444d6ed4..6472bda4a6 100644 --- a/docs/platform-bootstrap-config.md +++ b/docs/platform-bootstrap-config.md @@ -80,14 +80,12 @@ the 32-byte Ed25519 key on SVM, 0x-hex of the 33-byte **compressed** secp256k1 point on EVM (never the 20-byte address). Each credit mints to the account that pubkey has linked through `sysio.authex`, or parks it until the link is made. -### Defined but not consumed +### Launch schema -`bootstrap.proto` still defines `ReserveSpec`, `UwritConfig`, -`BootstrapPlatformConfig.reserves`, `BootstrapPlatformConfig.uwrit` and -`t5_reserve_allocation`, and both JSON files still carry values for them. The -contracts they targeted (`sysio.reserv`, `sysio.uwrit`) have been removed, so no -bootstrap action consumes them; only `test_bootstrap_platform_config.cpp` still -parses and validates them (V6–V9). +`ReserveSpec`, `UwritConfig`, `reserves`, `uwrit` and `t5_reserve_allocation` are removed from the +pre-launch schema. Authored JSON uses schema version 1; strict parsing rejects removed keys. +The DEX earmark alone is subtracted from the T5 allotment. Regenerate matching OPP models and +bootstrap clients for a fresh deployment. ## T5 DEX earmark @@ -127,7 +125,7 @@ validator in `test_bootstrap_platform_config.cpp` enforces: | V3 | chain codes unique; exactly one `CHAIN_KIND_WIRE` chain, code `WIRE` | | V4 | token codes unique; `chain_code` declared; `precision` ∈ 1..9 (the depot frame); native ⇔ kind `NATIVE` + empty address; non-native address well-formed for the chain kind (EVM `0x`+40 hex; SVM base58 → 32 bytes) | | V5 | exactly one native token per non-depot chain | -| V6–V9 | checks on the unconsumed `reserves` / `uwrit` / `t5_reserve_allocation` fields (see [Defined but not consumed](#defined-but-not-consumed)) | +| V6–V9 | retired with the external-reserve and underwriting configuration | | V10 | `t5_dex_allocation > 0` when any liq pool is seeded; Σ `liq_pools[].initial_wire_amount` ≤ `t5_dex_allocation` | | V11 | each liq pool references a declared `TOKEN_KIND_LIQ` token on its chain and is unique; `pair_symbol` 1..7 characters `[A-Z]`; seeds > 0; `fee ≤ 9999`; `conversion_horizon_sec > 0`; `depth_cap_bps` ∈ 1..10000; `clip_floor > 0` | | V12 | each syndication references a declared liq token on its chain; `pubkey` fits the chain family; `amount > 0` | diff --git a/docs/sysio-synd.md b/docs/sysio-synd.md index 0cbbd4e735..b9f4109858 100644 --- a/docs/sysio-synd.md +++ b/docs/sysio-synd.md @@ -261,8 +261,8 @@ outstanding down, so none of them raises a false alarm: - yield is claimed into custody before it is reported, and reported before it is released; - an envelope ruled INVALID or dropped with `dropenv` is burned on the depot (`sysio.synd.cpp:819`, `:1260`) while the outpost keeps the custody; -- a `recredit` of a skipped desyndication returns shadow the outpost never paid out (see - [The recredit rule](#desyndication)); +- a request-keyed `refundreturn` restores shadow only after definitive external cancellation (see + [Desyndication](#desyndication)); - anyone can send tokens to a custody account. Every writer of the depot outstanding is matched by custody, or is a launch or governance act: @@ -273,7 +273,8 @@ Every writer of the depot outstanding is matched by custody, or is a launch or g | `mintyield` (pending) | `:138-150` | the yield the outpost claimed into custody before it reported it | | `queueyield` | `:205` | moves pending into supply; the outstanding is unchanged | | `burn` | `:118` | lowers the outstanding: a desyndication, an INVALID ruling, `dropenv` | -| `recredit` | `:91` | governance: custody the outpost never paid out (the recredit rule) | +| `mint` (from `refundreturn`) | governance | restores a tracked return only after irreversible external cancellation; erases the return atomically | +| `recredit` | privileged repair | exceptional repair only; never recover a tracked return through this action | | `regliqpool` | `:355` | launch: the LCO liquidity, "already in outpost custody" (`:353-354`) | ### What happens on each side @@ -320,10 +321,12 @@ and the latest `(chain_code, token_code)` incident must be reconciled explicitly message's emission after repair and its actual admission. `admit_sequence` drops any sequence at or below the cursor, so a cursor already past the message plus no incident does not alone prove that message was admitted: verify its admission trace or observe a later post-repair message being admitted. -Only after this check passes for every repaired outpost may the depot cord clear. Mismatch rows for -earlier pre-repair sequences reporting the known shortfall are expected and must be recorded as part -of the existing incident, not treated as a new incident. A mismatch on the post-repair message still -requires investigation before clearing. +Governance then calls `reconcile(chain_code, token_code, reported)` for each affected pair with +verified live custody covering current outstanding shadow; snapshot the incident before this erases it. +`reconcile` leaves the cord pulled. Only after all affected incidents are erased and these admission +checks pass may the depot cord clear. A late pre-repair report can overwrite the same pair incident +and pull the cord again; snapshot its evidence and reconcile the pair again after verifying healthy +live custody. A shortfall on a post-repair report requires investigation before clearing. ### What counts as slack @@ -492,7 +495,7 @@ Envelope states: OPEN, WAITING, REQUESTED, RELEASABLE, HELD, INVALID, DONE. ## Configuration `setconfig(chain_code, token_code, synd_fee_bps, desynd_fee_bps, synd_burst, synd_refill, -desynd_burst, desynd_refill, window_sec, bounty, challenge_extra)`, signed by `sysio`. It replaces the +desynd_burst, desynd_refill, window_sec, bounty, challenge_extra, min_desyndicate)`, signed by `sysio`. It replaces the pair's whole row. | Knob | Default (no row) | Meaning | @@ -500,6 +503,7 @@ pair's whole row. | `synd_fee_bps` | 0 | fee on each released syndication tranche, at most 10000 | | `desynd_fee_bps` | 0 | fee on each desyndication, at most 10000 | | `synd_burst`, `synd_refill` | unset: nothing releases | syndication bucket size, and refill per depot epoch, in base units | +| `min_desyndicate` | explicit bootstrap value: 1,000,000 | shared gross return floor (0.001 SOL at nine decimals), intentionally arbitrary; at most a nonzero desyndication burst; rejected before moving custody | | `desynd_burst`, `desynd_refill` | unset: nothing desyndicates | desyndication bucket size and refill | | `window_sec` | 10800 | challenge window of each request; above 0 | | `bounty` | 0 | bounty posted on each request, paid from the fee pot | diff --git a/etc/config/dex/dex-config.dev.json b/etc/config/dex/dex-config.dev.json index b105b87873..13aef70947 100644 --- a/etc/config/dex/dex-config.dev.json +++ b/etc/config/dex/dex-config.dev.json @@ -1,7 +1,6 @@ { "schema_version": 1, "network": "dev-cluster", - "t5_reserve_allocation": "100000000000", "t5_dex_allocation": "20000000000", "chains": [ { "kind": "CHAIN_KIND_WIRE", "code": "WIRE", "external_chain_id": 0, @@ -52,38 +51,6 @@ "contract_address": "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", "is_native": false } ], - "reserves": [ - { "code": "PRIMARY", "chain_code": "ETHEREUM", "token_code": "ETH", - "name": "ETHEREUM-ETH/WIRE primary reserve", - "description": "Bootstrap-seeded native ETH ↔ WIRE reserve", - "initial_chain_amount": "10000000000", "initial_wire_amount": "10000000000", - "connector_weight_bps": 5000, "is_private": false, "owner": "" }, - { "code": "PRIMARY", "chain_code": "ETHEREUM", "token_code": "USDC", - "name": "ETHEREUM-USDC/WIRE primary reserve", - "description": "Bootstrap-seeded USDC ↔ WIRE reserve (mock ERC-20)", - "initial_chain_amount": "10000000000", "initial_wire_amount": "10000000000", - "connector_weight_bps": 5000, "is_private": false, "owner": "" }, - { "code": "PRIMARY", "chain_code": "ETHEREUM", "token_code": "USDT", - "name": "ETHEREUM-USDT/WIRE primary reserve", - "description": "Bootstrap-seeded USDT ↔ WIRE reserve (mock ERC-20)", - "initial_chain_amount": "10000000000", "initial_wire_amount": "10000000000", - "connector_weight_bps": 5000, "is_private": false, "owner": "" }, - { "code": "PRIMARY", "chain_code": "SOLANA", "token_code": "SOL", - "name": "SOLANA-SOL/WIRE primary reserve", - "description": "Bootstrap-seeded native SOL ↔ WIRE reserve", - "initial_chain_amount": "10000000000", "initial_wire_amount": "10000000000", - "connector_weight_bps": 5000, "is_private": false, "owner": "" }, - { "code": "PRIMARY", "chain_code": "SOLANA", "token_code": "USDCSOL", - "name": "SOLANA-USDCSOL/WIRE primary reserve", - "description": "Bootstrap-seeded USDC ↔ WIRE reserve on Solana (mock SPL)", - "initial_chain_amount": "10000000000", "initial_wire_amount": "10000000000", - "connector_weight_bps": 5000, "is_private": false, "owner": "" }, - { "code": "PRIMARY", "chain_code": "SOLANA", "token_code": "USDTSOL", - "name": "SOLANA-USDTSOL/WIRE primary reserve", - "description": "Bootstrap-seeded USDT ↔ WIRE reserve on Solana (mock SPL)", - "initial_chain_amount": "10000000000", "initial_wire_amount": "10000000000", - "connector_weight_bps": 5000, "is_private": false, "owner": "" } - ], "liq_pools": [ { "chain_code": "ETHEREUM", "token_code": "LIQETH", "pair_symbol": "LIQETHP", "name": "LIQETH/WIRE yield pool", @@ -108,9 +75,5 @@ { "chain_code": "SOLANA", "token_code": "LIQSOL", "pubkey": "11111111111111111111111111111111", "amount": "3000000000" } - ], - "uwrit": { - "fee_bps": 10, - "collateral_lock_duration_ms": "43200000" - } + ] } diff --git a/etc/config/dex/dex-config.launch.example.json b/etc/config/dex/dex-config.launch.example.json index e487aacc3e..27dbab51d3 100644 --- a/etc/config/dex/dex-config.launch.example.json +++ b/etc/config/dex/dex-config.launch.example.json @@ -1,7 +1,6 @@ { "schema_version": 1, "network": "mainnet-launch", - "t5_reserve_allocation": "10000000000000000", "chains": [ { "kind": "CHAIN_KIND_WIRE", "code": "WIRE", "external_chain_id": 0, "name": "Wire Network", "description": "WIRE depot chain" }, @@ -30,35 +29,5 @@ "precision": 9, "chain_code": "ETHEREUM", "contract_address": "0xdAC17F958D2ee523a2206206994597C13D831ec7", "is_native": false } - ], - "reserves": [ - { "code": "PRIMARY", "chain_code": "ETHEREUM", "token_code": "ETH", - "name": "ETH primary reserve", - "description": "TBD-BY-ECONOMICS: placeholder amounts", - "initial_chain_amount": "1000000000000", - "initial_wire_amount": "2000000000000000", - "connector_weight_bps": 5000, "is_private": false, "owner": "" }, - { "code": "PRIMARY", "chain_code": "ETHEREUM", "token_code": "USDC", - "name": "USDC primary reserve", - "description": "TBD-BY-ECONOMICS: placeholder amounts", - "initial_chain_amount": "2000000000000000", - "initial_wire_amount": "2000000000000000", - "connector_weight_bps": 5000, "is_private": false, "owner": "" }, - { "code": "PRIMARY", "chain_code": "ETHEREUM", "token_code": "USDT", - "name": "USDT primary reserve", - "description": "TBD-BY-ECONOMICS: placeholder amounts", - "initial_chain_amount": "2000000000000000", - "initial_wire_amount": "2000000000000000", - "connector_weight_bps": 5000, "is_private": false, "owner": "" }, - { "code": "PRIMARY", "chain_code": "SOLANA", "token_code": "SOL", - "name": "SOL primary reserve", - "description": "TBD-BY-ECONOMICS: placeholder amounts", - "initial_chain_amount": "10000000000000", - "initial_wire_amount": "2000000000000000", - "connector_weight_bps": 5000, "is_private": false, "owner": "" } - ], - "uwrit": { - "fee_bps": 10, - "collateral_lock_duration_ms": "43200000" - } + ] } diff --git a/libraries/opp/include/sysio/opp/opp.hpp b/libraries/opp/include/sysio/opp/opp.hpp index 4533dbcbbe..6bf650614a 100644 --- a/libraries/opp/include/sysio/opp/opp.hpp +++ b/libraries/opp/include/sysio/opp/opp.hpp @@ -96,13 +96,11 @@ FC_REFLECT_ENUM(sysio::opp::types::AttestationType, (ATTESTATION_TYPE_OPERATORS) (ATTESTATION_TYPE_BATCH_OPERATOR_GROUPS) (ATTESTATION_TYPE_NODE_OWNER_REG) - (ATTESTATION_TYPE_STAKING_REWARD) (ATTESTATION_TYPE_STAKE_RESULT) (ATTESTATION_TYPE_ATTESTATION_PROCESSING_ERROR) (ATTESTATION_TYPE_UNDERWRITE_INTENT_COMMIT) (ATTESTATION_TYPE_SWAP_REVERT) (ATTESTATION_TYPE_DEPOSIT_REVERT) - (ATTESTATION_TYPE_RESERVE_CREATE) (ATTESTATION_TYPE_RESERVE_CREATE_CANCEL) (ATTESTATION_TYPE_RESERVE_CREATE_CANCELLED) (ATTESTATION_TYPE_RESERVE_READY) diff --git a/libraries/opp/proto/sysio/opp/attestations/attestations.proto b/libraries/opp/proto/sysio/opp/attestations/attestations.proto index ab0e071780..756c0656af 100644 --- a/libraries/opp/proto/sysio/opp/attestations/attestations.proto +++ b/libraries/opp/proto/sysio/opp/attestations/attestations.proto @@ -371,46 +371,6 @@ message NodeOwnerReg { sysio.opp.types.ChainAddress nft_address = 3; } -// Staking reward distribution (Outpost -> Depot), per staker per source-chain -// reward period. -// -// Emitted by an outpost when a staker's slice of a validator-reward -// distribution has been computed. The amount arrives already -// WIRE-denominated; the native -> WIRE conversion and the per-staker -// proration are done outpost-side. `sysio.msgch` dispatches the per-staker -// body to `sysio.dclaim::onreward`, which credits the staker's claim ledger -// directly without re-splitting (`pending_claims` if the staker has an -// AuthX-linked WIRE account, otherwise parked in `unmapped_tokens` keyed by -// the native address until they link). -message StakingReward { - // Chain code emitting the reward — identifies the (chain, validator) context. - uint64 chain_code = 1; - // The staker's WIRE account. May be empty when the staker has not yet - // completed AuthX linking; in that case the depot parks the reward by - // `staker_native_address` until the link sweep moves it to the claim ledger. - sysio.opp.types.WireAccount staker_wire_account = 2; - // The staker's share of the reward in basis points (10000 = 100%). - // Informational / audit only — `reward_amount` is already the staker's - // absolute, already-prorated portion (the depot does not re-split). - uint32 share_bps = 3; - // WIRE epoch index in which this reward is being recorded. Informational for - // roll-up / audit; dedupe and pruning key off `external_epoch_ref`. - uint32 reward_epoch_index = 4; - // Source-chain reward-period reference (the external chain's epoch / period - // number for this distribution). Monotonic per (outpost, staker); the depot - // dedupes and rolls up against it and rejects anything at or below the last - // processed value, so no per-period history is retained (data-leak safe). - uint64 external_epoch_ref = 5; - // Absolute reward amount being credited, already WIRE-denominated: the outpost - // does the native -> WIRE conversion and source-chain precision scaling, so the - // depot credits reward_amount.amount as WIRE directly. - sysio.opp.types.TokenAmount reward_amount = 6; - // The staker's native (ETH / SOL / ...) address — the address they staked - // from. Always populated so a not-yet-linked staker's reward can be parked - // and made claimable the moment they complete AuthX linking. - sysio.opp.types.ChainAddress staker_native_address = 7; -} - message StakeResult { sysio.opp.types.ChainAddress owner_address = 1; sysio.opp.types.TokenAmount amount = 2; @@ -425,67 +385,7 @@ message AttestationProcessingError { string error_message = 4; } -// --------------------------------------------------------------------------- -// Reserve creation flow (outpost-initiated, depot-matched) -// -// Lifecycle: -// 1. User calls outpost `create_reserve(...)`. Outpost takes external_token_amount -// custody, inserts a local PENDING reserve record, queues RESERVE_CREATE. -// 2. Depot receives RESERVE_CREATE → `sysio.reserv::oncrtreserve(...)` inserts -// a row with status=PENDING (creator_addr remembered for cancel refund). -// 3. Any WIRE account calls `sysio.reserv::matchreserve(...)` putting up -// requested_wire_amount WIRE → status flips to ACTIVE → RESERVE_READY queued. -// 4. Outpost dispatches RESERVE_READY → marks local reserve ACTIVE; reserve -// is now usable for swap routing. -// -// Cancel path (anywhere before match wins): -// a. Creator calls outpost `cancel_create_reserve(...)`. Outpost queues -// RESERVE_CREATE_CANCEL. DOES NOT mutate local state. -// b. Depot receives RESERVE_CREATE_CANCEL: -// - If status == PENDING: set to CANCELLED, queue RESERVE_CREATE_CANCELLED. -// - Else (race lost to match): silent no-op per -// feedback_opp_handlers_never_throw.md — no attestation emitted. -// c. Outpost dispatches RESERVE_CREATE_CANCELLED → refunds the original -// external_token_amount to creator_addr; marks local reserve CANCELLED. -// --------------------------------------------------------------------------- - -// Outpost -> Depot. Open a new PENDING reserve. -message ReserveCreate { - // (fields 1-3 + 6 retired — the reserve identity and the custodied - // amount now travel together in `external_amount`, see below.) - reserved 1, 2, 3, 6; - reserved "chain_code", "token_code", "reserve_code", "external_token_amount"; - string name = 4; - string description = 5; - uint64 requested_wire_amount = 7; - uint32 connector_weight_bps = 8; // Bancor WIRE-side weight in bps (1..9999); 10000 is rejected by oncrtreserve (zero token-side weight = dead reserve). - sysio.opp.types.ChainAddress creator_addr = 9; // refund target on cancel - // Private reserves restrict swap counterparts to reserve_codes owned by the - // same WIRE account (ownership = the authex-linked account that matches). - // Immutable after create. - bool is_private = 10; - // The creator's chain public key, used by the depot to resolve the creator's - // authex link (`sysio.authex::links.bypubkey`). EVM chains: REQUIRED — the - // 33-byte compressed secp256k1 key, verified on-outpost to derive to - // msg.sender (a 20-byte EVM address cannot be reverse-mapped to a key). - // SVM chains: may be empty — `creator_addr.address` already IS the signer's - // 32-byte ed25519 public key and the depot falls back to it. - bytes creator_pub_key = 11; - // Reserve identity — (chain_code = the outpost's own chain, reserve_code, - // amount.token_code) — plus the custodied external-token amount, in the - // depot's canonical 9-decimal frame. The outpost converts its chain-native - // escrow units at the boundary (EVM `PrecisionLib.toDepot`, SVM - // `precision::to_depot`) exactly like the swap paths; the outpost's LOCAL - // reserve record keeps the raw chain-native amount for refunds. - sysio.opp.types.ReserveAmount external_amount = 12; - // Depot-frame decimal precision of the reserve's source token side = min(native, 9): - // the outpost downscales anything above the 9-dec frame cap at its boundary - // and reports the frame precision it used here, so the depot's reserve row is - // self-describing and never assumes 9. The WIRE/target side is always the WIRE - // token's 9 (WIRE_PRECISION), so it is not carried. - uint32 source_token_precision = 13; -} - +// Retained carriers for the retired reserve cancellation/matching lifecycle. // Outpost -> Depot. Creator cancels their PENDING reserve. message ReserveCreateCancel { uint64 chain_code = 1; diff --git a/libraries/opp/proto/sysio/opp/bootstrap/bootstrap.proto b/libraries/opp/proto/sysio/opp/bootstrap/bootstrap.proto index b46e5df1af..81e4624147 100644 --- a/libraries/opp/proto/sysio/opp/bootstrap/bootstrap.proto +++ b/libraries/opp/proto/sysio/opp/bootstrap/bootstrap.proto @@ -80,10 +80,7 @@ message TokenSpec { // anything above MAX_TOKEN_PRECISION (9), so a higher value would pass config // validation and then abort the irreversible bootstrap. A token whose native // precision exceeds the frame (ETH at 18) declares the frame value here and is - // downscaled at the outpost boundary; the cap is what keeps every uint64/int64 - // amount field in range. This value also supplies `regreserve`'s - // `source_token_precision` for any reserve pairing this token — see - // `ReserveSpec`. + // downscaled at the outpost boundary; the cap keeps amount fields in range. uint32 precision = 5; // Chain this token binds to — must reference a declared `ChainSpec.code`. string chain_code = 6; @@ -94,56 +91,6 @@ message TokenSpec { bool is_native = 8; } -// A launch reserve to seed via -// `sysio.reserv::regreserve(chain_code, token_code, reserve_code, name, -// description, initial_chain_amount, initial_wire_amount, -// source_token_precision, connector_weight_bps, is_private, owner)`. -// -// ELEVEN arguments. `source_token_precision` — the 8th — has no field in this -// message; it is the paired `TokenSpec.precision` (see `token_code` below). -// -// The WIRE side (`initial_wire_amount`) is drained from the `sysio` emissions -// treasury into reserve custody at registration time (real WIRE transfer; see -// `BootstrapPlatformConfig.t5_reserve_allocation`). -message ReserveSpec { - // Reserve codename slug, conventionally "PRIMARY". - string code = 1; - // Outpost chain this reserve lives on — references a declared non-WIRE - // `ChainSpec.code`. - string chain_code = 2; - // Token paired against WIRE — must reference a `TokenSpec` bound to - // `chain_code`. - // - // That referenced spec ALSO supplies `regreserve`'s `source_token_precision` - // argument, which has no field of its own here: it is the paired token's - // depot-frame `TokenSpec.precision` (`min(native, 9)`). Resolve - // `(chain_code, token_code)` to its `TokenSpec` and pass that precision; - // `regreserve` rejects anything above 9 (`WIRE_PRECISION`). - string token_code = 3; - // Human-readable display name. - string name = 4; - // Free-form description / provenance note. - string description = 5; - // Outpost-side token seed, in that token's subunits (quote in JSON). - uint64 initial_chain_amount = 6; - // WIRE-side seed, in WIRE subunits, drained from the earmark (quote in JSON). - uint64 initial_wire_amount = 7; - // Bancor connector weight (WIRE-side) in basis points: 1..9999; 5000 = - // 50/50 pure constant product. Honored by the weighted swap curve. 10000 is - // rejected by regreserve: it zeroes the token-side weight, so every quote is 0 - // (a dead, unpriceable reserve). - uint32 connector_weight_bps = 8; - - // Private-reserve support (live as of the reserve-and-swap beta). A private - // reserve pairs same-owner only and is excluded from WIRE-endpoint swaps. - // Post-bootstrap creation derives ownership from the authex-linked matcher; - // the bootstrap `regreserve` path seeds it directly via these two fields. - bool is_private = 9; - // Owning WIRE account name (regreserve's `owner`, a sysio::name). EMPTY for - // public reserves; REQUIRED (and a valid account name) when `is_private`. - string owner = 10; -} - // A shadow-liq yield pool to seed via // `sysio.liq::regliqpool(chain_code, token_code, pair_symbol, // initial_chain_amount, initial_wire_amount, fee, locked_shares, @@ -214,74 +161,19 @@ message SyndicationSpec { uint64 amount = 4; } -// Global swap settings applied once via `sysio.uwrit::setconfig`, which takes -// SIX arguments: `(fee_bps, collateral_lock_duration_ms, min_fromwire_amount, -// fromwire_revert_fee_bps, uwreq_pending_timeout_epochs, -// uwreq_retention_epochs)`. This message pins only the first two; a bootstrap -// caller supplies the remaining four itself, using the contract's `uw_config` -// in-struct defaults (5 WIRE floor, 500 bps revert fee, and the two uwreq -// lifecycle windows) unless it has a reason to override them. -// -// NO fee-DISTRIBUTION shares are configured here. The routing is a two-stage -// split of the network fee plus an independent per-reserve owner fee: -// -// * `fee_bps` is the NETWORK fee, taken out of the WIRE leg of every swap. -// `sysio.reserv` splits that component 50/50 between the swap's WINNING -// UNDERWRITER (claimable via `claimuwfee`) and a REWARDS POOL — the fixed -// protocol constant `sysio.reserv::FEE_UNDERWRITER_SHARE_BPS`. -// * The rewards pool then splits again by the OPTIONAL governance dial -// `sysio.reserv::reserve_config.fee_emissions_share_bps` (`reservcfg`): that -// share is transferred to the `sysio` emissions treasury, the remainder -// accrues to the rewards bucket for batch operators. Nothing seeds -// `reservcfg`, so the dial reads ZERO until `sysio.reserv::setconfig` first -// persists a row — at which default the whole pool is allocated to batch -// operators and no fee leaves `sysio.reserv`'s custody at settlement. -// * Each participating non-WIRE leg's reserve ALSO charges its own -// `owner_fee_bps` off the same WIRE leg, INDEPENDENTLY of `fee_bps` -// (`ReserveSpec` does not carry it; it is set post-bootstrap via -// `sysio.reserv::setrsvfee`). So `fee_bps` is not the whole effective swap -// fee: a chain-to-chain swap pays two owner fees plus the network fee. -message UwritConfig { - // Per-spoke depot swap fee in basis points (<= 9999; a 100% fee is rejected), - // taken out of the WIRE leg of every swap. - uint32 fee_bps = 1; - // Wall-clock collateral lock window in MILLISECONDS (> 0). Contract default - // is 43_200_000 (12h). - uint64 collateral_lock_duration_ms = 2; - - // Slots 3-5 were fee_split_winner_pct / fee_split_other_uw_pct / - // fee_split_batch_op_pct (whole-percent distribution shares) — removed. Field - // numbers stay reserved so the wire format cannot reuse them. Distribution is - // no longer configured here at all: see the two-stage split described above. - reserved 3, 4, 5; - reserved "fee_split_winner_pct", "fee_split_other_uw_pct", "fee_split_batch_op_pct"; -} - -// Top-level launch platform bootstrap configuration. One JSON instance per -// deployment. (This is the WIRE *platform* bootstrap — chains, tokens, reserves -// and global swap settings — not a DEX: it stands up the launch-day registry -// state, of which the swap reserves are one part.) +// Top-level launch chains, tokens, LIQ yield pools and syndicated positions. message BootstrapPlatformConfig { - // Schema version; bump on a breaking shape change. Must be 1. + // Launch schema version; must be 1. uint32 schema_version = 1; // Deployment label ("mainnet-launch" / "dev-cluster"); the bootstrap tool // sanity-checks it against its target. string network = 2; - // WIRE subunits earmarked from the T5 allotment to back the WIRE side of - // launch reserves. Sits OUTSIDE `emission_config.t5_distributable`: at - // genesis, t5_distributable = T5_allotment - t5_reserve_allocation - - // t5_dex_allocation, so the sysio.epoch emissions readiness gate never sees a - // short treasury balance after `regreserve` drains the reserve backing. - // Invariant: sum(reserves[].initial_wire_amount) <= t5_reserve_allocation. - uint64 t5_reserve_allocation = 3; - // WIRE subunits earmarked from the T5 allotment to back the WIRE side of the // launch shadow-liq yield pools (`liq_pools`), drained by - // `sysio.liq::regliqpool`. Like `t5_reserve_allocation`, this sits OUTSIDE - // `t5_distributable`, so the total T5 carve-out is - // `t5_reserve_allocation + t5_dex_allocation`; the figure is set + // `sysio.liq::regliqpool`. This sits OUTSIDE `t5_distributable`; + // the figure is set // per-deployment (Kyle / Ken own the launch number). // Invariant: sum(liq_pools[].initial_wire_amount) <= t5_dex_allocation. uint64 t5_dex_allocation = 8; @@ -290,10 +182,6 @@ message BootstrapPlatformConfig { repeated ChainSpec chains = 4; // Tokens to register, each with its single chain binding. repeated TokenSpec tokens = 5; - // Reserves to seed (WIRE side drained from the earmark). - repeated ReserveSpec reserves = 6; - // Global swap / underwriting settings. - UwritConfig uwrit = 7; // Shadow-liq yield pools to seed, one per liq token (WIRE side drained from // `t5_dex_allocation`). repeated LiqPoolSpec liq_pools = 9; diff --git a/libraries/opp/proto/sysio/opp/types/types.proto b/libraries/opp/proto/sysio/opp/types/types.proto index 6b6402a4dc..c181e2f4fc 100644 --- a/libraries/opp/proto/sysio/opp/types/types.proto +++ b/libraries/opp/proto/sysio/opp/types/types.proto @@ -215,8 +215,7 @@ message Reserve { // canonical 9-decimal frame: outpost-side producers convert chain-native // units at the boundary (EVM `PrecisionLib.toDepot`, SVM // `precision::to_depot`) before shipping, exactly like the swap-request -// paths. Carried by RESERVE_CREATE (custodied external amount), -// RESERVE_BALANCE_SHEET (per-reserve balance entries), and SLASH routing +// paths. Carried by RESERVE_BALANCE_SHEET (per-reserve balance entries) and SLASH routing // (seized-funds destination). message ReserveAmount { uint64 chain_code = 1; @@ -256,7 +255,6 @@ enum AttestationType { // 60948 was ATTESTATION_TYPE_REMIT_CONFIRM — removed; do not reuse. ATTESTATION_TYPE_BATCH_OPERATOR_GROUPS = 60943; ATTESTATION_TYPE_NODE_OWNER_REG = 60949; - ATTESTATION_TYPE_STAKING_REWARD = 60950; ATTESTATION_TYPE_STAKE_RESULT = 60951; ATTESTATION_TYPE_ATTESTATION_PROCESSING_ERROR = 60952; @@ -270,9 +268,8 @@ enum AttestationType { // --------------------------------------------------------------------------- // Reserve-flow attestations (data-model refactor, 2026-05-19) - // Outpost-initiated reserve creation with depot-side matching handshake. + // Retained cancellation and matching carriers for the retired reserve flow. // --------------------------------------------------------------------------- - ATTESTATION_TYPE_RESERVE_CREATE = 60958; // 0xEE1E outpost → depot ATTESTATION_TYPE_RESERVE_CREATE_CANCEL = 60959; // 0xEE1F outpost → depot ATTESTATION_TYPE_RESERVE_CREATE_CANCELLED = 60960; // 0xEE20 depot → outpost ATTESTATION_TYPE_RESERVE_READY = 60961; // 0xEE21 depot → outpost diff --git a/libraries/opp/test/test_bootstrap_platform_config.cpp b/libraries/opp/test/test_bootstrap_platform_config.cpp index 9a3348a6d5..cbd19e75d7 100644 --- a/libraries/opp/test/test_bootstrap_platform_config.cpp +++ b/libraries/opp/test/test_bootstrap_platform_config.cpp @@ -29,7 +29,6 @@ #include #include #include -#include #include namespace gpb = google::protobuf; @@ -108,18 +107,6 @@ bool pair_symbol_ok(const std::string& s) { return true; } -/// Lightweight Antelope account-name check for the private-reserve `owner`. -/// The contract is the authority; this catches gross authoring mistakes -/// (charset `.a-z1-5`, non-empty, <= 13 chars). -bool account_name_ok(const std::string& s) { - if (s.empty() || s.size() > 13) return false; - for (char c : s) { - const bool ok = (c >= 'a' && c <= 'z') || (c >= '1' && c <= '5') || c == '.'; - if (!ok) return false; - } - return true; -} - /// Validate a parsed config against the launch invariants V1..V13. Returns a /// list of human-readable failures (empty == valid). std::vector validate(const BootstrapPlatformConfig& c) { @@ -147,7 +134,6 @@ std::vector validate(const BootstrapPlatformConfig& c) { // tokens: V2 slug, V4 binding + precision + address well-formedness std::set token_codes; std::map native_per_chain; - std::set> bindings; std::set> liq_bindings; // the TOKEN_KIND_LIQ subset for (const auto& t : c.tokens()) { if (!slug_ok(t.code())) e.push_back("V2 token code: " + t.code()); @@ -165,7 +151,6 @@ std::vector validate(const BootstrapPlatformConfig& c) { // precision exceeds the frame (ETH at 18) declares min(native, 9) and is // downscaled at the outpost boundary. if (t.precision() < 1 || t.precision() > 9) e.push_back("V4 token precision: " + t.code()); - bindings.insert({t.chain_code(), t.code()}); if (t.is_native()) { ++native_per_chain[t.chain_code()]; if (t.kind() != TokenKind::TOKEN_KIND_NATIVE || !t.contract_address().empty()) @@ -183,48 +168,6 @@ std::vector validate(const BootstrapPlatformConfig& c) { if (ch.kind() != ChainKind::CHAIN_KIND_WIRE && native_per_chain[ch.code()] != 1) e.push_back("V5 expected exactly one native token on chain " + ch.code()); - // reserves: V6 uniqueness/binding/weight/amount, V7 earmark, V8 owner - std::set> triples; - unsigned __int128 sum_wire = 0; - for (const auto& r : c.reserves()) { - if (!slug_ok(r.code())) e.push_back("V2 reserve code: " + r.code()); - const auto key = std::make_tuple(r.chain_code(), r.token_code(), r.code()); - if (!triples.insert(key).second) - e.push_back("V6 duplicate reserve: " + r.chain_code() + "/" + r.token_code() + "/" + r.code()); - if (!bindings.count({r.chain_code(), r.token_code()})) - e.push_back("V6 reserve references undeclared binding: " + r.chain_code() + "/" + r.token_code()); - const auto it = chain_kind.find(r.chain_code()); - if (it != chain_kind.end() && it->second == ChainKind::CHAIN_KIND_WIRE) - e.push_back("V6 reserve cannot live on the depot chain"); - // 1..9999 only: 10000 zeroes the token-side weight (dead reserve) and is - // rejected on-chain by regreserve/oncrtreserve (MAX_CONNECTOR_WEIGHT_BPS). - if (!(r.connector_weight_bps() > 0 && r.connector_weight_bps() <= 9999)) - e.push_back("V6 connector_weight_bps out of range"); - if (r.initial_chain_amount() == 0 || r.initial_wire_amount() == 0) - e.push_back("V6 reserve amounts must be > 0"); - sum_wire += r.initial_wire_amount(); - if (r.is_private() && !account_name_ok(r.owner())) - e.push_back("V8 private reserve needs a valid owner account name"); - if (!r.is_private() && !r.owner().empty()) - e.push_back("V8 public reserve must not name an owner"); - } - - // V7 — WIRE earmark covers the reserve WIRE sides - if (c.t5_reserve_allocation() == 0) e.push_back("V7 t5_reserve_allocation must be > 0"); - if (sum_wire > static_cast(c.t5_reserve_allocation())) - e.push_back("V7 sum(initial_wire_amount) exceeds t5_reserve_allocation"); - - // V9 — uwrit config - if (!c.has_uwrit()) { - e.push_back("V9 uwrit config missing"); - } else { - const auto& u = c.uwrit(); - // fee_bps <= 9999: a 10000 (100%) fee zeroes the post-fee WIRE leg and is - // rejected on-chain by sysio.uwrit::setconfig (MAX_FEE_BPS). - if (u.fee_bps() > 9999) e.push_back("V9 fee_bps > 9999 (100%)"); - if (u.collateral_lock_duration_ms() == 0) e.push_back("V9 collateral_lock_duration_ms must be > 0"); - } - // liq pools: V11 binding / uniqueness / parameters, V10 earmark std::map, unsigned __int128> pool_seed; unsigned __int128 sum_pool_wire = 0; @@ -296,7 +239,6 @@ BOOST_AUTO_TEST_CASE(launch_example_parses_and_validates) { BOOST_REQUIRE_MESSAGE(parse_strict(slurp(CONFIG_DIR + "/dex-config.launch.example.json"), cfg, err), err); for (const auto& v : validate(cfg)) BOOST_ERROR(v); BOOST_CHECK_EQUAL(cfg.chains_size(), 3); - BOOST_CHECK_EQUAL(cfg.reserves_size(), 4); } /// The dev-cluster mirror parses strictly and satisfies every invariant. @@ -306,7 +248,6 @@ BOOST_AUTO_TEST_CASE(dev_config_parses_and_validates) { BOOST_REQUIRE_MESSAGE(parse_strict(slurp(CONFIG_DIR + "/dex-config.dev.json"), cfg, err), err); for (const auto& v : validate(cfg)) BOOST_ERROR(v); BOOST_CHECK_EQUAL(cfg.tokens_size(), 9); - BOOST_CHECK_EQUAL(cfg.reserves_size(), 6); // the two liq tokens are pools, not reserves BOOST_CHECK_EQUAL(cfg.liq_pools_size(), 2); BOOST_CHECK_EQUAL(cfg.syndications_size(), 3); } @@ -337,6 +278,20 @@ BOOST_AUTO_TEST_CASE(strict_parse_rejects_unknown_field) { BOOST_CHECK(!parse_strict(bad, cfg, err)); } +/// Retired configuration keys must fail strict parsing instead of being silently ignored. +BOOST_AUTO_TEST_CASE(strict_parse_rejects_removed_fields_and_unsupported_version) { + for (const char* field : {R"("reserves": [])", R"("uwrit": {})", R"("t5_reserve_allocation": "1")"}) { + BootstrapPlatformConfig cfg; + std::string err; + BOOST_CHECK(!parse_strict(std::string{R"({"schema_version":1,"network":"x",)"} + field + "}", cfg, err)); + } + BootstrapPlatformConfig cfg; + std::string err; + BOOST_REQUIRE(parse_strict(slurp(CONFIG_DIR + "/dex-config.dev.json"), cfg, err)); + cfg.set_schema_version(2); + BOOST_CHECK(!validate(cfg).empty()); +} + /// Each single-field mutation of the valid dev config trips at least one /// invariant (one mutation per targeted check). BOOST_AUTO_TEST_CASE(validator_rejects_mutations) { @@ -347,15 +302,6 @@ BOOST_AUTO_TEST_CASE(validator_rejects_mutations) { { auto c = base; c.mutable_chains(1)->set_code("TOOLONG99"); // 9 chars > 8 BOOST_CHECK(!validate(c).empty()); } // V2 over-length slug - // Reserve codes get the same V2 grammar as chain and token codes; regreserve - // refuses the digit- and underscore-leading ones outright. - for (const char* bad : {"1BAD", "_BAD", ""}) { - auto c = base; c.mutable_reserves(0)->set_code(bad); - const auto errs = validate(c); - BOOST_CHECK_MESSAGE(std::any_of(errs.begin(), errs.end(), - [](const std::string& s) { return s.starts_with("V2 reserve code"); }), - std::string("reserve code should be rejected: '") + bad + "'"); - } { auto c = base; c.mutable_chains(2)->set_kind(ChainKind::CHAIN_KIND_WIRE); BOOST_CHECK(!validate(c).empty()); } // V3 two depots { auto c = base; // V5 second native on ETHEREUM @@ -375,14 +321,6 @@ BOOST_AUTO_TEST_CASE(validator_rejects_mutations) { BOOST_CHECK(!validate(c).empty()); } // V4 native ETH precision, not downscaled { auto c = base; c.mutable_tokens(0)->set_precision(9); BOOST_CHECK(validate(c).empty()); } // V4 the frame itself stays valid - { auto c = base; c.mutable_reserves(0)->set_connector_weight_bps(10000); - BOOST_CHECK(!validate(c).empty()); } // V6 weight 10000 rejected (zero token-side weight) - { auto c = base; c.set_t5_reserve_allocation(1); - BOOST_CHECK(!validate(c).empty()); } // V7 earmark too small - { auto c = base; c.mutable_reserves(0)->set_is_private(true); // V8 private without owner - BOOST_CHECK(!validate(c).empty()); } - { auto c = base; c.mutable_uwrit()->set_fee_bps(10000); - BOOST_CHECK(!validate(c).empty()); } // V9 fee_bps 10000 rejected (100% zeroes post-fee WIRE) { auto c = base; c.set_t5_dex_allocation(1); BOOST_CHECK(!validate(c).empty()); } // V10 dex earmark too small { auto c = base; c.mutable_liq_pools(0)->set_token_code("USDC"); // V11 an ERC-20 is not a liq token