From e61b434f502ca82932d19bf3aa1919ff29014c68 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Tue, 12 May 2026 10:52:43 -0500 Subject: [PATCH 01/12] sysio.cap: scaffold depot-side WIRE distribution / claim ledger MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New contract on `sysio.cap` that holds pending-WIRE balances owed to LIQ-token stakers and pre-launch pretoken purchasers. The outpost owns share computation; wire-sysio just credits, holds, and pays out. Tables: positions (lifecycle metadata only — shares and principal live at the outpost), pclaims (per-Wire-account WIRE owed), unmapped (per-(chain, native_pubkey) WIRE for unlinked addresses), cdqueue (cooldown queue mirroring opreg::wtdwqueue), capcfg (singleton with the imported_complete one-way flag for the forthcoming importseed action), capcounters (monotonic id counters). Actions: setconfig (initialize the singleton — placeholder while concrete fields are decided), claim (user drains pclaims via inline sysio.token transfer), linkswept (internal sweep from unmapped to pclaims on successful AuthX link), flushcd (drain matured cdqueue rows on sysio.epoch::advance), availstake (readonly rollup; returns 0 today since position-side principal is not tracked yet). Follows part3 conventions: public *_t table types exported from the header, composite uint128 indexes for (account/chain, addr) lookups, helpers in anonymous namespace for shared composite-key construction. Wires into contracts/CMakeLists.txt and the contracts_unit_test fixture; skeleton sysio.cap_tests.cpp covers setconfig and a no-row claim rejection. --- contracts/CMakeLists.txt | 1 + contracts/sysio.cap/CMakeLists.txt | 45 ++ .../sysio.cap/include/sysio.cap/sysio.cap.hpp | 262 +++++++++++ contracts/sysio.cap/src/sysio.cap.cpp | 121 +++++ contracts/sysio.cap/sysio.cap.abi | 424 ++++++++++++++++++ contracts/sysio.cap/sysio.cap.wasm | Bin 0 -> 17452 bytes contracts/tests/contracts.hpp.in | 2 + contracts/tests/sysio.cap_tests.cpp | 76 ++++ 8 files changed, 931 insertions(+) create mode 100644 contracts/sysio.cap/CMakeLists.txt create mode 100644 contracts/sysio.cap/include/sysio.cap/sysio.cap.hpp create mode 100644 contracts/sysio.cap/src/sysio.cap.cpp create mode 100644 contracts/sysio.cap/sysio.cap.abi create mode 100755 contracts/sysio.cap/sysio.cap.wasm create mode 100644 contracts/tests/sysio.cap_tests.cpp diff --git a/contracts/CMakeLists.txt b/contracts/CMakeLists.txt index ac7ca0c1c1..be02484448 100644 --- a/contracts/CMakeLists.txt +++ b/contracts/CMakeLists.txt @@ -51,6 +51,7 @@ add_subdirectory(sysio.msgch) add_subdirectory(sysio.uwrit) add_subdirectory(sysio.chalg) add_subdirectory(sysio.reserv) +add_subdirectory(sysio.cap) add_subdirectory(sysio.token) add_subdirectory(sysio.wrap) diff --git a/contracts/sysio.cap/CMakeLists.txt b/contracts/sysio.cap/CMakeLists.txt new file mode 100644 index 0000000000..6a6bf9efcb --- /dev/null +++ b/contracts/sysio.cap/CMakeLists.txt @@ -0,0 +1,45 @@ +set(contract_name sysio.cap) +bootstrap_contract(${contract_name}) + +if(BUILD_SYSTEM_CONTRACTS) + find_cdt_magic_enum() + file(GLOB_RECURSE SOURCES src/*.cpp) + file(GLOB_RECURSE HEADERS include/*.hpp) + add_contract(${contract_name} ${contract_name} ${SOURCES}) + set(targets ${contract_name}) + + if("native-module" IN_LIST SYSIO_WASM_RUNTIMES) + list(APPEND targets ${contract_name}_native) + add_native_contract( + TARGET ${contract_name}_native + SOURCES ${SOURCES} + INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/include + CONTRACT_CLASS "sysio::cap" + HEADERS ${HEADERS} + ABI_FILE ${CMAKE_BINARY_DIR}/contracts/${contract_name}/${contract_name}.abi + ) + endif() + + foreach(target ${targets}) + if(NOT TARGET ${target}) + message(WARNING "Target ${target} not found, skipping include directory setup") + continue() + endif() + + target_include_directories(${target} + PUBLIC + $ + $ + $ + $ + $ + $ + $ + ) + + target_link_libraries(${target} + INTERFACE + magic_enum::magic_enum + ) + endforeach() +endif() diff --git a/contracts/sysio.cap/include/sysio.cap/sysio.cap.hpp b/contracts/sysio.cap/include/sysio.cap/sysio.cap.hpp new file mode 100644 index 0000000000..7d2f9ad8ba --- /dev/null +++ b/contracts/sysio.cap/include/sysio.cap/sysio.cap.hpp @@ -0,0 +1,262 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace sysio { + + /** + * @brief sysio.cap — depot-side WIRE distribution and claim ledger. + * + * Holds the pending-WIRE balances owed to LIQ-token stakers and pre-launch + * pretoken purchasers. The outpost owns share computation (per-staker + * `share_bps` arrives pre-computed in `StakingReward`); wire-sysio just + * credits, holds, and pays out. + * + * - `pending_claims` is the per-Wire-account ledger of WIRE owed. + * Users call `claim` to drain via inline transfer. + * - `unmapped_tokens` is the per-(chain, native_pubkey) ledger for + * stakers / purchasers who don't have a Wire account yet. Completing + * AuthX linking inline-calls `linkswept` which moves the credit into + * `pending_claims`. + * - `positions` carries lifecycle metadata only — status, owner mapping, + * timestamps. Shares and principal live at the outpost. + * - `cooldown_queue` mirrors opreg's `withdraw_queue`. Eligibility-cursor + * driven; drained by `flushcd` on each `sysio.epoch::advance` tick. + * Cooldown begins when the staking-withdrawal envelope reaches this + * contract. + * - `available_stake` is the read-only rollup analogous to opreg's + * `available()`. + */ + class [[sysio::contract("sysio.cap")]] cap : public contract { + public: + using contract::contract; + + // Well-known accounts. + static constexpr name AUTHEX_ACCOUNT = "sysio.authex"_n; + static constexpr name MSGCH_ACCOUNT = "sysio.msgch"_n; + static constexpr name EPOCH_ACCOUNT = "sysio.epoch"_n; + static constexpr name TOKEN_ACCOUNT = "sysio.token"_n; + + // WIRE token symbol. 9 decimals system-wide. + static constexpr symbol WIRE_SYM = symbol("WIRE", 9); + + // Cooldown wait epochs. Mirrors opreg::WITHDRAW_WAIT_EPOCHS. + static constexpr uint32_t COOLDOWN_WAIT_EPOCHS = 2; + + // ----------------------------------------------------------------------- + // Actions + // ----------------------------------------------------------------------- + + /// Set cap-staking configuration. Placeholder while concrete fields are + /// still being decided; calling once initializes the singleton. + [[sysio::action]] + void setconfig(); + + /// User-callable: drain the caller's `pending_claims` row via an inline + /// transfer of WIRE from `sysio.cap` to `wire_account`. Erases the row. + /// Reverts if no row exists or the balance is zero. + [[sysio::action]] + void claim(name wire_account); + + /// Internal: sweep an `unmapped_tokens` entry into `pending_claims` when + /// the staker / purchaser completes AuthX linking. Called inline by + /// `sysio.authex` after a successful link. No-op if no matching unmapped + /// row exists. + [[sysio::action]] + void linkswept(name wire_account, + opp::types::ChainKind chain, + std::vector native_pubkey); + + /// Internal: drain matured rows from `cooldown_queue`. Called inline + /// from `sysio.epoch::advance` each tick. + [[sysio::action]] + void flushcd(uint32_t current_epoch); + + /// Read-only rollup of the staker's spendable (yield-earning) stake on + /// a given chain. Returns 0 today since position-side principal is not + /// tracked yet; once lifecycle handlers land, will return + /// `sum(open_principal) - sum(active_cooldown_amount)`. + [[sysio::action, sysio::read_only]] + uint64_t availstake(name wire_account, opp::types::ChainKind chain); + + // ----------------------------------------------------------------------- + // Tables + // ----------------------------------------------------------------------- + + /// Position lifecycle metadata. Outpost owns shares and principal; + /// wire-sysio stores only what it uniquely tracks. + struct position_key { + uint64_t id; + uint64_t primary_key() const { return id; } + SYSLIB_SERIALIZE(position_key, (id)) + }; + + struct [[sysio::table("positions")]] position { + uint64_t id = 0; + opp::types::ChainKind chain_kind = opp::types::ChainKind::CHAIN_KIND_UNKNOWN; + std::vector native_address; + uint64_t outpost_position_id = 0; + opp::types::StakeStatus status = opp::types::StakeStatus::STAKE_STATUS_UNKNOWN; + name wire_account; + uint64_t opened_at_ms = 0; + uint64_t last_status_ms = 0; + + uint64_t primary_key() const { return id; } + uint64_t by_wire_account() const { return wire_account.value; } + uint64_t by_status() const { return static_cast(status); } + + /// Composite: (chain << 64 | first 8 bytes of native_address). + /// 8-byte prefix gives a 2^64 namespace per chain — collisions on + /// 20-byte ETH addresses or 32-byte SOL pubkeys are negligible at + /// expected user counts. + uint128_t by_chain_addr() const { + if (native_address.empty()) return 0; + uint64_t prefix = 0; + const size_t n = native_address.size() < sizeof(uint64_t) + ? native_address.size() : sizeof(uint64_t); + std::memcpy(&prefix, native_address.data(), n); + return (static_cast(chain_kind) << 64) | prefix; + } + + SYSLIB_SERIALIZE(position, + (id)(chain_kind)(native_address)(outpost_position_id) + (status)(wire_account)(opened_at_ms)(last_status_ms)) + }; + + using positions_t = sysio::kv::table<"positions"_n, position_key, position, + sysio::kv::index<"bywire"_n, + sysio::const_mem_fun>, + sysio::kv::index<"bystatus"_n, + sysio::const_mem_fun>, + sysio::kv::index<"bychainad"_n, + sysio::const_mem_fun> + >; + + /// Pending-claims: per-Wire-account WIRE owed. + struct pclaim_key { + uint64_t wire_account; + uint64_t primary_key() const { return wire_account; } + SYSLIB_SERIALIZE(pclaim_key, (wire_account)) + }; + + struct [[sysio::table("pclaims")]] pending_claim { + name wire_account; + asset balance = asset{0, WIRE_SYM}; + + uint64_t primary_key() const { return wire_account.value; } + + SYSLIB_SERIALIZE(pending_claim, (wire_account)(balance)) + }; + + using pclaims_t = sysio::kv::table<"pclaims"_n, pclaim_key, pending_claim>; + + /// Unmapped-tokens: per-(chain, native_pubkey) WIRE owed for stakers / + /// purchasers who don't have a Wire account yet. + struct unmapped_key { + uint64_t id; + uint64_t primary_key() const { return id; } + SYSLIB_SERIALIZE(unmapped_key, (id)) + }; + + struct [[sysio::table("unmapped")]] unmapped_token { + uint64_t id = 0; + opp::types::ChainKind chain_kind = opp::types::ChainKind::CHAIN_KIND_UNKNOWN; + std::vector native_pubkey; + asset balance = asset{0, WIRE_SYM}; + + uint64_t primary_key() const { return id; } + + uint128_t by_chain_addr() const { + if (native_pubkey.empty()) return 0; + uint64_t prefix = 0; + const size_t n = native_pubkey.size() < sizeof(uint64_t) + ? native_pubkey.size() : sizeof(uint64_t); + std::memcpy(&prefix, native_pubkey.data(), n); + return (static_cast(chain_kind) << 64) | prefix; + } + + SYSLIB_SERIALIZE(unmapped_token, (id)(chain_kind)(native_pubkey)(balance)) + }; + + using unmapped_t = sysio::kv::table<"unmapped"_n, unmapped_key, unmapped_token, + sysio::kv::index<"bychainad"_n, + sysio::const_mem_fun> + >; + + /// Cooldown queue. Mirrors opreg::withdraw_request layout. + struct cd_key { + uint64_t request_id; + uint64_t primary_key() const { return request_id; } + SYSLIB_SERIALIZE(cd_key, (request_id)) + }; + + struct [[sysio::table("cdqueue")]] cooldown_entry { + uint64_t request_id = 0; + uint64_t position_id = 0; + name wire_account; + opp::types::ChainKind chain_kind = opp::types::ChainKind::CHAIN_KIND_UNKNOWN; + asset amount = asset{0, WIRE_SYM}; + uint32_t eligible_at_epoch = 0; + uint32_t requested_at_epoch = 0; + + uint64_t primary_key() const { return request_id; } + + /// (wire_account, chain_kind) composite for `available_stake` scans. + uint128_t by_wire_chain() const { + return (static_cast(wire_account.value) << 64) + | static_cast(chain_kind); + } + uint64_t by_eligible() const { return static_cast(eligible_at_epoch); } + uint64_t by_position() const { return position_id; } + + SYSLIB_SERIALIZE(cooldown_entry, + (request_id)(position_id)(wire_account)(chain_kind)(amount) + (eligible_at_epoch)(requested_at_epoch)) + }; + + using cdqueue_t = sysio::kv::table<"cdqueue"_n, cd_key, cooldown_entry, + sysio::kv::index<"bywirechn"_n, + sysio::const_mem_fun>, + sysio::kv::index<"byeligible"_n, + sysio::const_mem_fun>, + sysio::kv::index<"byposition"_n, + sysio::const_mem_fun> + >; + + /// Cap-staking config singleton. `imported_complete` is the one-way flag + /// protecting the bootstrap `importseed` action (added in a follow-up). + struct [[sysio::table("capcfg")]] cap_config { + bool imported_complete = false; + + SYSLIB_SERIALIZE(cap_config, (imported_complete)) + }; + + using capcfg_t = sysio::kv::global<"capcfg"_n, cap_config>; + + /// Monotonic id counters for position / unmapped / cooldown rows. + struct [[sysio::table("capcounters")]] cap_counters { + uint64_t next_position_id = 1; + uint64_t next_unmapped_id = 1; + uint64_t next_cd_id = 1; + + SYSLIB_SERIALIZE(cap_counters, (next_position_id)(next_unmapped_id)(next_cd_id)) + }; + + using capcounters_t = sysio::kv::global<"capcounters"_n, cap_counters>; + + private: + using ChainKind = opp::types::ChainKind; + using StakeStatus = opp::types::StakeStatus; + using TokenKind = opp::types::TokenKind; + }; + +} // namespace sysio diff --git a/contracts/sysio.cap/src/sysio.cap.cpp b/contracts/sysio.cap/src/sysio.cap.cpp new file mode 100644 index 0000000000..7a658ca4c8 --- /dev/null +++ b/contracts/sysio.cap/src/sysio.cap.cpp @@ -0,0 +1,121 @@ +#include + +#include +#include +#include + +namespace sysio { + +namespace { + +using opp::types::ChainKind; + +uint128_t make_chain_addr_key(ChainKind chain, const std::vector& addr) { + if (addr.empty()) return 0; + uint64_t prefix = 0; + const size_t n = addr.size() < sizeof(uint64_t) ? addr.size() : sizeof(uint64_t); + std::memcpy(&prefix, addr.data(), n); + return (static_cast(chain) << 64) | prefix; +} + +uint128_t make_wire_chain_key(name wire_account, ChainKind chain) { + return (static_cast(wire_account.value) << 64) + | static_cast(chain); +} + +} // anonymous namespace + +// --------------------------------------------------------------------------- +// setconfig +// --------------------------------------------------------------------------- +void cap::setconfig() { + require_auth(get_self()); + capcfg_t cfg(get_self()); + if (!cfg.exists()) { + cfg.set(cap_config{}, get_self()); + } +} + +// --------------------------------------------------------------------------- +// claim +// --------------------------------------------------------------------------- +void cap::claim(name wire_account) { + require_auth(wire_account); + + pclaims_t pclaims(get_self()); + auto it = pclaims.find(pclaim_key{wire_account.value}); + check(it != pclaims.end(), "no pending claim"); + const asset payout = it->balance; + check(payout.amount > 0, "zero pending balance"); + pclaims.erase(it); + + action( + permission_level{ get_self(), "active"_n }, + TOKEN_ACCOUNT, + "transfer"_n, + std::make_tuple(get_self(), wire_account, payout, std::string("sysio.cap claim")) + ).send(); +} + +// --------------------------------------------------------------------------- +// linkswept +// --------------------------------------------------------------------------- +void cap::linkswept(name wire_account, ChainKind chain, std::vector native_pubkey) { + require_auth(AUTHEX_ACCOUNT); + + unmapped_t unmapped(get_self()); + auto idx = unmapped.template get_index<"bychainad"_n>(); + auto it = idx.find(make_chain_addr_key(chain, native_pubkey)); + if (it == idx.end()) return; + + const asset credit = it->balance; + const uint64_t row_id = it->id; + unmapped.erase(unmapped_key{row_id}); + + pclaims_t pclaims(get_self()); + auto pit = pclaims.find(pclaim_key{wire_account.value}); + if (pit == pclaims.end()) { + pending_claim row; + row.wire_account = wire_account; + row.balance = credit; + pclaims.emplace(get_self(), pclaim_key{wire_account.value}, row); + } else { + pclaims.modify(same_payer, pclaim_key{wire_account.value}, [&](auto& row) { + row.balance += credit; + }); + } +} + +// --------------------------------------------------------------------------- +// flushcd +// --------------------------------------------------------------------------- +void cap::flushcd(uint32_t current_epoch) { + require_auth(EPOCH_ACCOUNT); + + cdqueue_t cdqueue(get_self()); + auto idx = cdqueue.template get_index<"byeligible"_n>(); + auto it = idx.begin(); + while (it != idx.end() && it->eligible_at_epoch <= current_epoch) { + const uint64_t row_id = it->request_id; + ++it; + cdqueue.erase(cd_key{row_id}); + } +} + +// --------------------------------------------------------------------------- +// available_stake +// --------------------------------------------------------------------------- +uint64_t cap::availstake(name wire_account, ChainKind chain) { + uint64_t cooldown_locked = 0; + cdqueue_t cdqueue(get_self()); + auto idx = cdqueue.template get_index<"bywirechn"_n>(); + const uint128_t key = make_wire_chain_key(wire_account, chain); + for (auto it = idx.lower_bound(key); it != idx.end(); ++it) { + if (it->wire_account != wire_account || it->chain_kind != chain) break; + cooldown_locked += static_cast(it->amount.amount); + } + (void)cooldown_locked; + return 0; +} + +} // namespace sysio diff --git a/contracts/sysio.cap/sysio.cap.abi b/contracts/sysio.cap/sysio.cap.abi new file mode 100644 index 0000000000..56be7b858c --- /dev/null +++ b/contracts/sysio.cap/sysio.cap.abi @@ -0,0 +1,424 @@ +{ + "____comment": "This file was generated with sysio-abigen. DO NOT EDIT ", + "version": "sysio::abi/1.2", + "types": [], + "structs": [ + { + "name": "availstake", + "base": "", + "fields": [ + { + "name": "wire_account", + "type": "name" + }, + { + "name": "chain", + "type": "ChainKind" + } + ] + }, + { + "name": "cap_config", + "base": "", + "fields": [ + { + "name": "imported_complete", + "type": "bool" + } + ] + }, + { + "name": "cap_counters", + "base": "", + "fields": [ + { + "name": "next_position_id", + "type": "uint64" + }, + { + "name": "next_unmapped_id", + "type": "uint64" + }, + { + "name": "next_cd_id", + "type": "uint64" + } + ] + }, + { + "name": "cd_key", + "base": "", + "fields": [ + { + "name": "request_id", + "type": "uint64" + } + ] + }, + { + "name": "claim", + "base": "", + "fields": [ + { + "name": "wire_account", + "type": "name" + } + ] + }, + { + "name": "cooldown_entry", + "base": "", + "fields": [ + { + "name": "request_id", + "type": "uint64" + }, + { + "name": "position_id", + "type": "uint64" + }, + { + "name": "wire_account", + "type": "name" + }, + { + "name": "chain_kind", + "type": "ChainKind" + }, + { + "name": "amount", + "type": "asset" + }, + { + "name": "eligible_at_epoch", + "type": "uint32" + }, + { + "name": "requested_at_epoch", + "type": "uint32" + } + ] + }, + { + "name": "flushcd", + "base": "", + "fields": [ + { + "name": "current_epoch", + "type": "uint32" + } + ] + }, + { + "name": "linkswept", + "base": "", + "fields": [ + { + "name": "wire_account", + "type": "name" + }, + { + "name": "chain", + "type": "ChainKind" + }, + { + "name": "native_pubkey", + "type": "bytes" + } + ] + }, + { + "name": "pclaim_key", + "base": "", + "fields": [ + { + "name": "wire_account", + "type": "uint64" + } + ] + }, + { + "name": "pending_claim", + "base": "", + "fields": [ + { + "name": "wire_account", + "type": "name" + }, + { + "name": "balance", + "type": "asset" + } + ] + }, + { + "name": "position", + "base": "", + "fields": [ + { + "name": "id", + "type": "uint64" + }, + { + "name": "chain_kind", + "type": "ChainKind" + }, + { + "name": "native_address", + "type": "bytes" + }, + { + "name": "outpost_position_id", + "type": "uint64" + }, + { + "name": "status", + "type": "StakeStatus" + }, + { + "name": "wire_account", + "type": "name" + }, + { + "name": "opened_at_ms", + "type": "uint64" + }, + { + "name": "last_status_ms", + "type": "uint64" + } + ] + }, + { + "name": "position_key", + "base": "", + "fields": [ + { + "name": "id", + "type": "uint64" + } + ] + }, + { + "name": "setconfig", + "base": "", + "fields": [] + }, + { + "name": "unmapped_key", + "base": "", + "fields": [ + { + "name": "id", + "type": "uint64" + } + ] + }, + { + "name": "unmapped_token", + "base": "", + "fields": [ + { + "name": "id", + "type": "uint64" + }, + { + "name": "chain_kind", + "type": "ChainKind" + }, + { + "name": "native_pubkey", + "type": "bytes" + }, + { + "name": "balance", + "type": "asset" + } + ] + } + ], + "actions": [ + { + "name": "availstake", + "type": "availstake", + "ricardian_contract": "" + }, + { + "name": "claim", + "type": "claim", + "ricardian_contract": "" + }, + { + "name": "flushcd", + "type": "flushcd", + "ricardian_contract": "" + }, + { + "name": "linkswept", + "type": "linkswept", + "ricardian_contract": "" + }, + { + "name": "setconfig", + "type": "setconfig", + "ricardian_contract": "" + } + ], + "tables": [ + { + "name": "capcfg", + "type": "cap_config", + "index_type": "i64", + "key_names": ["name"], + "key_types": ["name"], + "table_id": 41893 + }, + { + "name": "capcounters", + "type": "cap_counters", + "index_type": "i64", + "key_names": ["name"], + "key_types": ["name"], + "table_id": 40418 + }, + { + "name": "cdqueue", + "type": "cooldown_entry", + "index_type": "i64", + "key_names": ["request_id"], + "key_types": ["uint64"], + "table_id": 24354, + "secondary_indexes": [ + { + "name": "bywirechn", + "key_type": "uint128", + "table_id": 36786 + }, + { + "name": "byeligible", + "key_type": "uint64", + "table_id": 62191 + }, + { + "name": "byposition", + "key_type": "uint64", + "table_id": 6853 + } + ] + }, + { + "name": "pclaims", + "type": "pending_claim", + "index_type": "i64", + "key_names": ["wire_account"], + "key_types": ["uint64"], + "table_id": 16291 + }, + { + "name": "positions", + "type": "position", + "index_type": "i64", + "key_names": ["id"], + "key_types": ["uint64"], + "table_id": 39131, + "secondary_indexes": [ + { + "name": "bywire", + "key_type": "uint64", + "table_id": 45669 + }, + { + "name": "bystatus", + "key_type": "uint64", + "table_id": 6749 + }, + { + "name": "bychainad", + "key_type": "uint128", + "table_id": 65189 + } + ] + }, + { + "name": "unmapped", + "type": "unmapped_token", + "index_type": "i64", + "key_names": ["id"], + "key_types": ["uint64"], + "table_id": 60901, + "secondary_indexes": [ + { + "name": "bychainad", + "key_type": "uint128", + "table_id": 48559 + } + ] + } + ], + "ricardian_clauses": [], + "variants": [], + "action_results": [ + { + "name": "availstake", + "result_type": "uint64" + } + ], + "enums": [ + { + "name": "ChainKind", + "type": "int32", + "values": [ + { + "name": "CHAIN_KIND_UNKNOWN", + "value": 0 + }, + { + "name": "CHAIN_KIND_WIRE", + "value": 1 + }, + { + "name": "CHAIN_KIND_ETHEREUM", + "value": 2 + }, + { + "name": "CHAIN_KIND_SOLANA", + "value": 3 + }, + { + "name": "CHAIN_KIND_SUI", + "value": 4 + } + ] + }, + { + "name": "StakeStatus", + "type": "int32", + "values": [ + { + "name": "STAKE_STATUS_UNKNOWN", + "value": 0 + }, + { + "name": "STAKE_STATUS_WARMUP", + "value": 1 + }, + { + "name": "STAKE_STATUS_COOLDOWN", + "value": 2 + }, + { + "name": "STAKE_STATUS_ACTIVE", + "value": 3 + }, + { + "name": "STAKE_STATUS_TERMINATED", + "value": 240 + }, + { + "name": "STAKE_STATUS_SLASHED", + "value": 241 + } + ] + } + ] +} \ No newline at end of file diff --git a/contracts/sysio.cap/sysio.cap.wasm b/contracts/sysio.cap/sysio.cap.wasm new file mode 100755 index 0000000000000000000000000000000000000000..3b79f9185fb1fba48887bc9659290c98db9e3351 GIT binary patch literal 17452 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 literal 0 HcmV?d00001 diff --git a/contracts/tests/contracts.hpp.in b/contracts/tests/contracts.hpp.in index 184a8da35a..07700308b8 100644 --- a/contracts/tests/contracts.hpp.in +++ b/contracts/tests/contracts.hpp.in @@ -30,6 +30,8 @@ struct contracts { static std::vector chalg_abi() { return read_abi("${CMAKE_BINARY_DIR}/contracts/sysio.chalg/sysio.chalg.abi"); } static std::vector reserve_wasm() { return read_wasm("${CMAKE_BINARY_DIR}/contracts/sysio.reserv/sysio.reserv.wasm"); } static std::vector reserve_abi() { return read_abi("${CMAKE_BINARY_DIR}/contracts/sysio.reserv/sysio.reserv.abi"); } + static std::vector cap_wasm() { return read_wasm("${CMAKE_BINARY_DIR}/contracts/sysio.cap/sysio.cap.wasm"); } + static std::vector cap_abi() { return read_abi("${CMAKE_BINARY_DIR}/contracts/sysio.cap/sysio.cap.abi"); } struct util { static std::vector reject_all_wasm() { return read_wasm("${CMAKE_SOURCE_DIR}/contracts/test_contracts/reject_all.wasm"); } diff --git a/contracts/tests/sysio.cap_tests.cpp b/contracts/tests/sysio.cap_tests.cpp new file mode 100644 index 0000000000..1009b80ef3 --- /dev/null +++ b/contracts/tests/sysio.cap_tests.cpp @@ -0,0 +1,76 @@ +#include +#include +#include + +#include + +#include "contracts.hpp" +#include + +using namespace sysio::testing; +using namespace sysio; +using namespace sysio::chain; +using namespace fc; +using namespace sysio::opp::types; + +using mvo = fc::mutable_variant_object; + +/// Test fixture for sysio.cap. Creates the contract account, deploys WASM / +/// ABI, and loads an ABI serializer. Reusable across `sysio.cap_tests` cases +/// that need the contract live but don't yet exercise inbound handlers +/// (those land once Q1 / Q5 resolutions arrive and the StakeUpdate / +/// StakingReward inbound paths are implemented). +class sysio_cap_tester : public tester { +public: + static constexpr auto CAP_ACCOUNT = "sysio.cap"_n; + static constexpr auto AUTHEX_ACCOUNT = "sysio.authex"_n; + static constexpr auto EPOCH_ACCOUNT = "sysio.epoch"_n; + static constexpr auto TOKEN_ACCOUNT = "sysio.token"_n; + + sysio_cap_tester() { + produce_blocks(2); + + create_accounts({ + CAP_ACCOUNT, EPOCH_ACCOUNT, TOKEN_ACCOUNT, + "alice"_n, "bob"_n, + }); + produce_blocks(2); + + set_code(CAP_ACCOUNT, contracts::cap_wasm()); + set_abi(CAP_ACCOUNT, contracts::cap_abi().data()); + set_privileged(CAP_ACCOUNT); + + produce_blocks(); + + const auto* accnt = control->find_account_metadata(CAP_ACCOUNT); + BOOST_REQUIRE(accnt != nullptr); + abi_def abi; + BOOST_REQUIRE_EQUAL(abi_serializer::to_abi(accnt->abi, abi), true); + cap_abi_ser.set_abi(std::move(abi), abi_serializer::create_yield_function(abi_serializer_max_time)); + } + + action_result push_cap_action(name signer, name action_name, const variant_object& data) { + try { + base_tester::push_action(CAP_ACCOUNT, action_name, signer, data); + return success(); + } catch (const fc::exception& ex) { + return error(ex.top_message()); + } + } + + abi_serializer cap_abi_ser; +}; + +BOOST_AUTO_TEST_SUITE(sysio_cap_tests) + +BOOST_FIXTURE_TEST_CASE(setconfig_initializes_singleton, sysio_cap_tester) { try { + const auto result = push_cap_action(CAP_ACCOUNT, "setconfig"_n, mvo{}); + BOOST_REQUIRE_EQUAL(result, success()); +} FC_LOG_AND_RETHROW() } + +BOOST_FIXTURE_TEST_CASE(claim_rejects_empty_ledger, sysio_cap_tester) { try { + const auto result = push_cap_action("alice"_n, "claim"_n, mvo()("wire_account", "alice")); + BOOST_REQUIRE_NE(result, success()); +} FC_LOG_AND_RETHROW() } + +BOOST_AUTO_TEST_SUITE_END() From 1b345d90f93a653ef95081dd11661187734020e6 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Tue, 12 May 2026 12:39:17 -0500 Subject: [PATCH 02/12] sysio.cap: add importseed/importdone bootstrap actions `importseed(chain, credits)` is the privileged, batched action that seeds `unmapped_tokens` for pre-launch ETH/SOL holders. Each `import_credit { native_address, wire_atomic }` row credits an entry; same address across batches sums into the existing row. The off-chain converter floors the source pretoken value (1e18 wei-style) at the 1e9 boundary; sub-atomic dust is dropped (9-digit WIRE precision system-wide; per-row dust < 1 atomic WIRE). `importdone()` flips the one-way `imported_complete` flag on `cap_config`; subsequent `importseed` calls revert. Together they let the bios bring-down script chunk the import across as many transactions as the source data requires. Hash-collision-safe address lookup: `importseed` uses `lower_bound` on the composite (chain, first 8 bytes) index and walks forward verifying the full `native_pubkey` matches before merging. Tests cover happy-path, negative-atomic rejection, and the post-importdone lock. --- .../sysio.cap/include/sysio.cap/sysio.cap.hpp | 25 ++++++ contracts/sysio.cap/src/sysio.cap.cpp | 72 +++++++++++++++++- contracts/sysio.cap/sysio.cap.abi | 43 +++++++++++ contracts/sysio.cap/sysio.cap.wasm | Bin 17452 -> 23571 bytes contracts/tests/sysio.cap_tests.cpp | 36 +++++++++ 5 files changed, 172 insertions(+), 4 deletions(-) diff --git a/contracts/sysio.cap/include/sysio.cap/sysio.cap.hpp b/contracts/sysio.cap/include/sysio.cap/sysio.cap.hpp index 7d2f9ad8ba..bf53cec5e7 100644 --- a/contracts/sysio.cap/include/sysio.cap/sysio.cap.hpp +++ b/contracts/sysio.cap/include/sysio.cap/sysio.cap.hpp @@ -81,6 +81,31 @@ namespace sysio { [[sysio::action]] void flushcd(uint32_t current_epoch); + /// 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 + /// atomic units; the off-chain converter floors the source pretoken + /// value at the 1e9 boundary (sub-atomic dust dropped — total dust + /// pool bounded at < num_users * 1 atomic WIRE). + struct import_credit { + std::vector native_address; + int64_t wire_atomic = 0; + + SYSLIB_SERIALIZE(import_credit, (native_address)(wire_atomic)) + }; + + /// Bootstrap import: privileged, batched insert/update of + /// `unmapped_tokens` rows for pre-launch holders. Same `native_address` + /// across batches sums into the existing row. Aborts once + /// `cap_config::imported_complete` is true (set by `importdone`). + [[sysio::action]] + void importseed(opp::types::ChainKind chain, std::vector credits); + + /// Finalize the bootstrap import. Flips `imported_complete` to true; + /// subsequent `importseed` calls revert. + [[sysio::action]] + void importdone(); + /// Read-only rollup of the staker's spendable (yield-earning) stake on /// a given chain. Returns 0 today since position-side principal is not /// tracked yet; once lifecycle handlers land, will return diff --git a/contracts/sysio.cap/src/sysio.cap.cpp b/contracts/sysio.cap/src/sysio.cap.cpp index 7a658ca4c8..868c1c98e9 100644 --- a/contracts/sysio.cap/src/sysio.cap.cpp +++ b/contracts/sysio.cap/src/sysio.cap.cpp @@ -68,8 +68,8 @@ void cap::linkswept(name wire_account, ChainKind chain, std::vector native auto it = idx.find(make_chain_addr_key(chain, native_pubkey)); if (it == idx.end()) return; - const asset credit = it->balance; - const uint64_t row_id = it->id; + const asset credit_balance = it->balance; + const uint64_t row_id = it->id; unmapped.erase(unmapped_key{row_id}); pclaims_t pclaims(get_self()); @@ -77,11 +77,11 @@ void cap::linkswept(name wire_account, ChainKind chain, std::vector native if (pit == pclaims.end()) { pending_claim row; row.wire_account = wire_account; - row.balance = credit; + row.balance = credit_balance; pclaims.emplace(get_self(), pclaim_key{wire_account.value}, row); } else { pclaims.modify(same_payer, pclaim_key{wire_account.value}, [&](auto& row) { - row.balance += credit; + row.balance += credit_balance; }); } } @@ -102,6 +102,70 @@ void cap::flushcd(uint32_t current_epoch) { } } +// --------------------------------------------------------------------------- +// importseed +// --------------------------------------------------------------------------- +void cap::importseed(ChainKind chain, std::vector credits) { + require_auth(get_self()); + + capcfg_t cfg(get_self()); + const cap_config current_cfg = cfg.get_or_default(cap_config{}); + check(!current_cfg.imported_complete, "import already finalized"); + + if (credits.empty()) return; + + capcounters_t cnt_tbl(get_self()); + cap_counters counters = cnt_tbl.get_or_default(cap_counters{}); + + unmapped_t unmapped(get_self()); + auto idx = unmapped.template get_index<"bychainad"_n>(); + + for (const auto& credit : credits) { + check(credit.wire_atomic >= 0, "negative wire_atomic"); + check(!credit.native_address.empty(), "empty native_address"); + if (credit.wire_atomic == 0) continue; + + const uint128_t lookup_key = make_chain_addr_key(chain, credit.native_address); + const asset add_balance{credit.wire_atomic, WIRE_SYM}; + + bool merged = false; + for (auto it = idx.lower_bound(lookup_key); it != idx.end(); ++it) { + if (it->by_chain_addr() != lookup_key) break; + if (it->chain_kind == chain && it->native_pubkey == credit.native_address) { + const uint64_t row_id = it->id; + unmapped.modify(same_payer, unmapped_key{row_id}, [&](auto& row) { + row.balance += add_balance; + }); + merged = true; + break; + } + } + + if (!merged) { + unmapped_token row; + row.id = counters.next_unmapped_id++; + row.chain_kind = chain; + row.native_pubkey = credit.native_address; + row.balance = add_balance; + unmapped.emplace(get_self(), unmapped_key{row.id}, row); + } + } + + cnt_tbl.set(counters, get_self()); +} + +// --------------------------------------------------------------------------- +// importdone +// --------------------------------------------------------------------------- +void cap::importdone() { + require_auth(get_self()); + capcfg_t cfg(get_self()); + cap_config current = cfg.get_or_default(cap_config{}); + check(!current.imported_complete, "import already finalized"); + current.imported_complete = true; + cfg.set(current, get_self()); +} + // --------------------------------------------------------------------------- // available_stake // --------------------------------------------------------------------------- diff --git a/contracts/sysio.cap/sysio.cap.abi b/contracts/sysio.cap/sysio.cap.abi index 56be7b858c..50b649ae98 100644 --- a/contracts/sysio.cap/sysio.cap.abi +++ b/contracts/sysio.cap/sysio.cap.abi @@ -109,6 +109,39 @@ } ] }, + { + "name": "import_credit", + "base": "", + "fields": [ + { + "name": "native_address", + "type": "bytes" + }, + { + "name": "wire_atomic", + "type": "int64" + } + ] + }, + { + "name": "importdone", + "base": "", + "fields": [] + }, + { + "name": "importseed", + "base": "", + "fields": [ + { + "name": "chain", + "type": "ChainKind" + }, + { + "name": "credits", + "type": "import_credit[]" + } + ] + }, { "name": "linkswept", "base": "", @@ -253,6 +286,16 @@ "type": "flushcd", "ricardian_contract": "" }, + { + "name": "importdone", + "type": "importdone", + "ricardian_contract": "" + }, + { + "name": "importseed", + "type": "importseed", + "ricardian_contract": "" + }, { "name": "linkswept", "type": "linkswept", diff --git a/contracts/sysio.cap/sysio.cap.wasm b/contracts/sysio.cap/sysio.cap.wasm index 3b79f9185fb1fba48887bc9659290c98db9e3351..e1edd421f9351e5863b65f69456a2613e4d06469 100755 GIT binary patch literal 23571 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 KE@|RuApQsRZkXu+ diff --git a/contracts/tests/sysio.cap_tests.cpp b/contracts/tests/sysio.cap_tests.cpp index 1009b80ef3..bfb8341d03 100644 --- a/contracts/tests/sysio.cap_tests.cpp +++ b/contracts/tests/sysio.cap_tests.cpp @@ -73,4 +73,40 @@ BOOST_FIXTURE_TEST_CASE(claim_rejects_empty_ledger, sysio_cap_tester) { try { BOOST_REQUIRE_NE(result, success()); } FC_LOG_AND_RETHROW() } +BOOST_FIXTURE_TEST_CASE(importseed_accepts_credit_batch, sysio_cap_tester) { try { + std::vector addr(20, char(0xAB)); + const auto result = push_cap_action(CAP_ACCOUNT, "importseed"_n, mvo + ("chain", ChainKind::CHAIN_KIND_ETHEREUM) + ("credits", fc::variants{ + mvo()("native_address", addr)("wire_atomic", 982953049502) + }) + ); + BOOST_REQUIRE_EQUAL(result, success()); +} FC_LOG_AND_RETHROW() } + +BOOST_FIXTURE_TEST_CASE(importseed_rejects_negative_atomic, sysio_cap_tester) { try { + std::vector addr(20, char(0xCD)); + const auto result = push_cap_action(CAP_ACCOUNT, "importseed"_n, mvo + ("chain", ChainKind::CHAIN_KIND_ETHEREUM) + ("credits", fc::variants{ + mvo()("native_address", addr)("wire_atomic", -1) + }) + ); + BOOST_REQUIRE_NE(result, success()); +} FC_LOG_AND_RETHROW() } + +BOOST_FIXTURE_TEST_CASE(importdone_locks_subsequent_importseed, sysio_cap_tester) { try { + BOOST_REQUIRE_EQUAL( + push_cap_action(CAP_ACCOUNT, "importdone"_n, mvo{}), + success()); + std::vector addr(20, char(0xEF)); + const auto result = push_cap_action(CAP_ACCOUNT, "importseed"_n, mvo + ("chain", ChainKind::CHAIN_KIND_ETHEREUM) + ("credits", fc::variants{ + mvo()("native_address", addr)("wire_atomic", 1) + }) + ); + BOOST_REQUIRE_NE(result, success()); +} FC_LOG_AND_RETHROW() } + BOOST_AUTO_TEST_SUITE_END() From 268ab705cbf60b99f5e78c9bfa7258aac22670f8 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Tue, 12 May 2026 13:03:55 -0500 Subject: [PATCH 03/12] sysio.cap: add importseed converter for indexer JSON dumps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `contracts/sysio.cap/tools/convert_import.py` reads the indexer's snapshot JSON (purchasers + stakers arrays per response_*.json) and emits `sysio.cap::importseed` action arg batches as JSON on stdout, with a configurable per-batch cap (default 50). Per-address sum: `purchaser.totalPretokens` (already net of yieldClaimed) plus `staker.pretokenYield - staker.yieldClaimed`. Source values are 1e18-scale; floored at DUST_BASE = 1e9 to produce 9-decimal atomic-WIRE. Sub-atomic dust is summarized on stderr (bounded < num_users × 1 atomic WIRE — well under 10⁻⁷ WIRE at current scale). Output is ready for `clio push action sysio.cap importseed `; a sibling wrapper that drives clio can land later if useful. --- contracts/sysio.cap/tools/convert_import.py | 112 ++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100755 contracts/sysio.cap/tools/convert_import.py diff --git a/contracts/sysio.cap/tools/convert_import.py b/contracts/sysio.cap/tools/convert_import.py new file mode 100755 index 0000000000..275df036b4 --- /dev/null +++ b/contracts/sysio.cap/tools/convert_import.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +"""Convert the indexer JSON dump into sysio.cap::importseed action batches. + +Schema (current snapshot, may evolve as the indexer stabilizes): + metadata bookkeeping; not consumed by the contract + purchasers[] {address, totalPretokens, yieldClaimed, ...} + owed = totalPretokens (already net of yieldClaimed) + stakers[] {address, pretokenYield, yieldClaimed, ...} + owed = pretokenYield - yieldClaimed + +Per-address conversion: + total_pretokens = sum(purchaser.totalPretokens) + + sum(staker.pretokenYield - staker.yieldClaimed) + wire_atomic = total_pretokens // 1_000_000_000 (floor, drop sub-atomic dust) + +Rows with wire_atomic == 0 are filtered. Output is a JSON array of importseed +action arg objects, each batched up to --batch-size credits per call. + +Usage: + ./convert_import.py response_1778592566067.json > batches.json + ./convert_import.py response_*.json --batch-size 100 --chain CHAIN_KIND_ETHEREUM +""" + +import argparse +import json +import sys +from collections import defaultdict +from pathlib import Path + +# WIRE has 9 decimals; source pretoken values are 1e18 scale (wei-style). +# Flooring source by DUST_BASE drops the sub-atomic remainder. +DUST_BASE = 1_000_000_000 + + +def parse_args() -> argparse.Namespace: + ap = argparse.ArgumentParser( + description="Emit sysio.cap::importseed batches from the indexer JSON.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=__doc__, + ) + ap.add_argument("input", type=Path, + help="Indexer JSON file (response_*.json shape)") + ap.add_argument("--batch-size", type=int, default=50, + help="Credits per importseed call (default: 50)") + ap.add_argument("--chain", default="CHAIN_KIND_ETHEREUM", + help="ChainKind enum name (default: CHAIN_KIND_ETHEREUM)") + return ap.parse_args() + + +def accumulate(data: dict) -> dict[str, int]: + """Return {hex_address_without_prefix: total_pretokens_owed}.""" + acc: dict[str, int] = defaultdict(int) + for row in data.get("purchasers") or []: + addr = row["address"].lower().removeprefix("0x") + acc[addr] += int(row["totalPretokens"]) + for row in data.get("stakers") or []: + addr = row["address"].lower().removeprefix("0x") + owed = int(row["pretokenYield"]) - int(row.get("yieldClaimed", "0")) + if owed > 0: + acc[addr] += owed + return acc + + +def to_credits(accumulator: dict[str, int]) -> tuple[list[dict], int]: + """Floor each total at DUST_BASE; return (credits, total_dropped_dust).""" + credits = [] + dropped_dust = 0 + for addr, total in sorted(accumulator.items()): + atomic, dust = divmod(total, DUST_BASE) + dropped_dust += dust + if atomic <= 0: + continue + credits.append({"native_address": addr, "wire_atomic": atomic}) + return credits, dropped_dust + + +def batched(credits: list[dict], chain: str, batch_size: int) -> list[dict]: + return [ + {"chain": chain, "credits": credits[i:i + batch_size]} + for i in range(0, len(credits), batch_size) + ] + + +def main() -> int: + args = parse_args() + + data = json.loads(args.input.read_text()) + accumulator = accumulate(data) + credits, dropped_dust = to_credits(accumulator) + batches = batched(credits, args.chain, args.batch_size) + + json.dump(batches, sys.stdout, indent=2) + sys.stdout.write("\n") + + total_atomic = sum(c["wire_atomic"] for c in credits) + print( + f"input: {args.input}", + f"unique addresses: {len(accumulator)}", + f"non-zero credits: {len(credits)}", + f"batches: {len(batches)} (size {args.batch_size})", + f"total credited: {total_atomic} atomic WIRE" + f" ({total_atomic / DUST_BASE:.6f} WIRE)", + f"dropped dust: {dropped_dust} sub-atomic units" + f" ({dropped_dust / 10**18:.2e} WIRE)", + sep="\n", + file=sys.stderr, + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From aafd1fc03156b71330f7c629968fd74ecab43022 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Wed, 13 May 2026 10:40:54 -0500 Subject: [PATCH 04/12] sysio.cap: add explicit -I for magic_enum Mirrors the fix in abdffcc4aa applied to the other OPP-using contracts. sysio.cap.hpp transitively includes types.pb.hpp, which depends on magic_enum. cdt-cpp does not honor -isystem, so the magic_enum::magic_enum INTERFACE include must be re-injected as -I via target_include_directories PRIVATE. Required after rebasing feature/sysio-cap onto feature/emissions-configurable. --- contracts/sysio.cap/CMakeLists.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contracts/sysio.cap/CMakeLists.txt b/contracts/sysio.cap/CMakeLists.txt index 6a6bf9efcb..9eb89894b8 100644 --- a/contracts/sysio.cap/CMakeLists.txt +++ b/contracts/sysio.cap/CMakeLists.txt @@ -35,6 +35,9 @@ if(BUILD_SYSTEM_CONTRACTS) $ $ $ + # cdt-cpp ignores -isystem; add vcpkg root explicitly as -I so magic_enum resolves. + PRIVATE + ${CDT_CONTRACT_INCLUDE_PATH} ) target_link_libraries(${target} From e1d9b454c9bce8c954ebb6329985e56be8364c40 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Wed, 13 May 2026 10:47:40 -0500 Subject: [PATCH 05/12] sysio.cap: point converter at live indexer endpoint, print metadata Verified the converter against today's /opp/balances response (https://index.wire.foundation/opp/balances) -- schema and per-address values match response_1778592566067.json exactly (47 addresses, 217,941.104919 WIRE total, identical batch output). No conversion logic change needed. Two doc/UX-only updates: - Point the docstring at the live endpoint so future runs know where to fetch from, not just the captured snapshot filename. - Print metadata (generatedAt, totalMessages, yieldDust) on stderr so the operator can confirm which snapshot was processed and cross-check dropped_dust against the indexer's own dust counter. --- contracts/sysio.cap/tools/convert_import.py | 24 ++++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/contracts/sysio.cap/tools/convert_import.py b/contracts/sysio.cap/tools/convert_import.py index 275df036b4..1e90a47f16 100755 --- a/contracts/sysio.cap/tools/convert_import.py +++ b/contracts/sysio.cap/tools/convert_import.py @@ -1,8 +1,12 @@ #!/usr/bin/env python3 """Convert the indexer JSON dump into sysio.cap::importseed action batches. -Schema (current snapshot, may evolve as the indexer stabilizes): - metadata bookkeeping; not consumed by the contract +Live source (Ethereum balances): + curl -H 'x-api-key: ' https://index.wire.foundation/opp/balances + +Schema (verified against the /opp/balances endpoint as of 2026-05-13): + metadata bookkeeping; not consumed by the contract. Notable fields: + generatedAt, totalMessages, yieldDust (indexer-side dust ledger) purchasers[] {address, totalPretokens, yieldClaimed, ...} owed = totalPretokens (already net of yieldClaimed) stakers[] {address, pretokenYield, yieldClaimed, ...} @@ -17,8 +21,8 @@ action arg objects, each batched up to --batch-size credits per call. Usage: - ./convert_import.py response_1778592566067.json > batches.json - ./convert_import.py response_*.json --batch-size 100 --chain CHAIN_KIND_ETHEREUM + ./convert_import.py balances.json > batches.json + ./convert_import.py balances.json --batch-size 100 --chain CHAIN_KIND_ETHEREUM """ import argparse @@ -93,8 +97,13 @@ def main() -> int: sys.stdout.write("\n") total_atomic = sum(c["wire_atomic"] for c in credits) - print( + meta = data.get("metadata") or {} + lines = [ f"input: {args.input}", + f"generatedAt: {meta.get('generatedAt', '')}", + f"totalMessages: {meta.get('totalMessages', '')}", + f"indexer yieldDust: {meta.get('yieldDust', '')}" + " (indexer-side ledger; informational)", f"unique addresses: {len(accumulator)}", f"non-zero credits: {len(credits)}", f"batches: {len(batches)} (size {args.batch_size})", @@ -102,9 +111,8 @@ def main() -> int: f" ({total_atomic / DUST_BASE:.6f} WIRE)", f"dropped dust: {dropped_dust} sub-atomic units" f" ({dropped_dust / 10**18:.2e} WIRE)", - sep="\n", - file=sys.stderr, - ) + ] + print(*lines, sep="\n", file=sys.stderr) return 0 From 237356f937d2b5f6450f310bcd65e35e2e18d44d Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Wed, 13 May 2026 11:41:23 -0500 Subject: [PATCH 06/12] sysio.cap: remove cooldown machinery (no withdrawals in v1) Per Jack (2026-05-13): no cooldown scenarios in the v1 outpost. Stripping the wire-sysio-side scaffolding since it's dead code at launch and re-adding it later is cheap. Removed: - cdqueue table, cd_key, cooldown_entry struct (3-index kv::table) - flushcd action (epoch-tick maturation drain) - availstake read-only rollup (its only job was principal-minus-cooldown) - COOLDOWN_WAIT_EPOCHS constant - EPOCH_ACCOUNT well-known-account constant (only flushcd's auth) - cap_counters::next_cd_id field - make_wire_chain_key helper (only availstake used it) - EPOCH_ACCOUNT from the test fixture's create_accounts WASM 23,571 -> 18,950 bytes; ABI 12,139 -> 9,174 bytes. When withdrawals come online post-launch, the opreg::withdraw_queue pattern (eligible_at_epoch + epoch-driven flush) is the reference shape to add back. --- .../sysio.cap/include/sysio.cap/sysio.cap.hpp | 72 ++--------- contracts/sysio.cap/src/sysio.cap.cpp | 37 ------ contracts/sysio.cap/sysio.cap.abi | 114 +----------------- contracts/sysio.cap/sysio.cap.wasm | Bin 23571 -> 18950 bytes contracts/tests/sysio.cap_tests.cpp | 3 +- 5 files changed, 9 insertions(+), 217 deletions(-) diff --git a/contracts/sysio.cap/include/sysio.cap/sysio.cap.hpp b/contracts/sysio.cap/include/sysio.cap/sysio.cap.hpp index bf53cec5e7..1d0e3d6b38 100644 --- a/contracts/sysio.cap/include/sysio.cap/sysio.cap.hpp +++ b/contracts/sysio.cap/include/sysio.cap/sysio.cap.hpp @@ -29,12 +29,11 @@ namespace sysio { * `pending_claims`. * - `positions` carries lifecycle metadata only — status, owner mapping, * timestamps. Shares and principal live at the outpost. - * - `cooldown_queue` mirrors opreg's `withdraw_queue`. Eligibility-cursor - * driven; drained by `flushcd` on each `sysio.epoch::advance` tick. - * Cooldown begins when the staking-withdrawal envelope reaches this - * contract. - * - `available_stake` is the read-only rollup analogous to opreg's - * `available()`. + * + * No cooldown/withdrawal machinery for v1 (Jack 2026-05-13: outpost has + * no cooldown scenarios in this wave). When withdrawals come online + * post-launch, the cooldown-queue + maturation-flush pattern can be + * added back — see opreg's `withdraw_queue` for reference. */ class [[sysio::contract("sysio.cap")]] cap : public contract { public: @@ -43,15 +42,11 @@ namespace sysio { // Well-known accounts. static constexpr name AUTHEX_ACCOUNT = "sysio.authex"_n; static constexpr name MSGCH_ACCOUNT = "sysio.msgch"_n; - static constexpr name EPOCH_ACCOUNT = "sysio.epoch"_n; static constexpr name TOKEN_ACCOUNT = "sysio.token"_n; // WIRE token symbol. 9 decimals system-wide. static constexpr symbol WIRE_SYM = symbol("WIRE", 9); - // Cooldown wait epochs. Mirrors opreg::WITHDRAW_WAIT_EPOCHS. - static constexpr uint32_t COOLDOWN_WAIT_EPOCHS = 2; - // ----------------------------------------------------------------------- // Actions // ----------------------------------------------------------------------- @@ -76,11 +71,6 @@ namespace sysio { opp::types::ChainKind chain, std::vector native_pubkey); - /// Internal: drain matured rows from `cooldown_queue`. Called inline - /// from `sysio.epoch::advance` each tick. - [[sysio::action]] - void flushcd(uint32_t current_epoch); - /// 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 @@ -106,13 +96,6 @@ namespace sysio { [[sysio::action]] void importdone(); - /// Read-only rollup of the staker's spendable (yield-earning) stake on - /// a given chain. Returns 0 today since position-side principal is not - /// tracked yet; once lifecycle handlers land, will return - /// `sum(open_principal) - sum(active_cooldown_amount)`. - [[sysio::action, sysio::read_only]] - uint64_t availstake(name wire_account, opp::types::ChainKind chain); - // ----------------------------------------------------------------------- // Tables // ----------------------------------------------------------------------- @@ -217,46 +200,6 @@ namespace sysio { sysio::const_mem_fun> >; - /// Cooldown queue. Mirrors opreg::withdraw_request layout. - struct cd_key { - uint64_t request_id; - uint64_t primary_key() const { return request_id; } - SYSLIB_SERIALIZE(cd_key, (request_id)) - }; - - struct [[sysio::table("cdqueue")]] cooldown_entry { - uint64_t request_id = 0; - uint64_t position_id = 0; - name wire_account; - opp::types::ChainKind chain_kind = opp::types::ChainKind::CHAIN_KIND_UNKNOWN; - asset amount = asset{0, WIRE_SYM}; - uint32_t eligible_at_epoch = 0; - uint32_t requested_at_epoch = 0; - - uint64_t primary_key() const { return request_id; } - - /// (wire_account, chain_kind) composite for `available_stake` scans. - uint128_t by_wire_chain() const { - return (static_cast(wire_account.value) << 64) - | static_cast(chain_kind); - } - uint64_t by_eligible() const { return static_cast(eligible_at_epoch); } - uint64_t by_position() const { return position_id; } - - SYSLIB_SERIALIZE(cooldown_entry, - (request_id)(position_id)(wire_account)(chain_kind)(amount) - (eligible_at_epoch)(requested_at_epoch)) - }; - - using cdqueue_t = sysio::kv::table<"cdqueue"_n, cd_key, cooldown_entry, - sysio::kv::index<"bywirechn"_n, - sysio::const_mem_fun>, - sysio::kv::index<"byeligible"_n, - sysio::const_mem_fun>, - sysio::kv::index<"byposition"_n, - sysio::const_mem_fun> - >; - /// Cap-staking config singleton. `imported_complete` is the one-way flag /// protecting the bootstrap `importseed` action (added in a follow-up). struct [[sysio::table("capcfg")]] cap_config { @@ -267,13 +210,12 @@ namespace sysio { using capcfg_t = sysio::kv::global<"capcfg"_n, cap_config>; - /// Monotonic id counters for position / unmapped / cooldown rows. + /// Monotonic id counters for position / unmapped rows. struct [[sysio::table("capcounters")]] cap_counters { uint64_t next_position_id = 1; uint64_t next_unmapped_id = 1; - uint64_t next_cd_id = 1; - SYSLIB_SERIALIZE(cap_counters, (next_position_id)(next_unmapped_id)(next_cd_id)) + SYSLIB_SERIALIZE(cap_counters, (next_position_id)(next_unmapped_id)) }; using capcounters_t = sysio::kv::global<"capcounters"_n, cap_counters>; diff --git a/contracts/sysio.cap/src/sysio.cap.cpp b/contracts/sysio.cap/src/sysio.cap.cpp index 868c1c98e9..89c63b3dbd 100644 --- a/contracts/sysio.cap/src/sysio.cap.cpp +++ b/contracts/sysio.cap/src/sysio.cap.cpp @@ -18,11 +18,6 @@ uint128_t make_chain_addr_key(ChainKind chain, const std::vector& addr) { return (static_cast(chain) << 64) | prefix; } -uint128_t make_wire_chain_key(name wire_account, ChainKind chain) { - return (static_cast(wire_account.value) << 64) - | static_cast(chain); -} - } // anonymous namespace // --------------------------------------------------------------------------- @@ -86,22 +81,6 @@ void cap::linkswept(name wire_account, ChainKind chain, std::vector native } } -// --------------------------------------------------------------------------- -// flushcd -// --------------------------------------------------------------------------- -void cap::flushcd(uint32_t current_epoch) { - require_auth(EPOCH_ACCOUNT); - - cdqueue_t cdqueue(get_self()); - auto idx = cdqueue.template get_index<"byeligible"_n>(); - auto it = idx.begin(); - while (it != idx.end() && it->eligible_at_epoch <= current_epoch) { - const uint64_t row_id = it->request_id; - ++it; - cdqueue.erase(cd_key{row_id}); - } -} - // --------------------------------------------------------------------------- // importseed // --------------------------------------------------------------------------- @@ -166,20 +145,4 @@ void cap::importdone() { cfg.set(current, get_self()); } -// --------------------------------------------------------------------------- -// available_stake -// --------------------------------------------------------------------------- -uint64_t cap::availstake(name wire_account, ChainKind chain) { - uint64_t cooldown_locked = 0; - cdqueue_t cdqueue(get_self()); - auto idx = cdqueue.template get_index<"bywirechn"_n>(); - const uint128_t key = make_wire_chain_key(wire_account, chain); - for (auto it = idx.lower_bound(key); it != idx.end(); ++it) { - if (it->wire_account != wire_account || it->chain_kind != chain) break; - cooldown_locked += static_cast(it->amount.amount); - } - (void)cooldown_locked; - return 0; -} - } // namespace sysio diff --git a/contracts/sysio.cap/sysio.cap.abi b/contracts/sysio.cap/sysio.cap.abi index 50b649ae98..ce0ffa81a5 100644 --- a/contracts/sysio.cap/sysio.cap.abi +++ b/contracts/sysio.cap/sysio.cap.abi @@ -3,20 +3,6 @@ "version": "sysio::abi/1.2", "types": [], "structs": [ - { - "name": "availstake", - "base": "", - "fields": [ - { - "name": "wire_account", - "type": "name" - }, - { - "name": "chain", - "type": "ChainKind" - } - ] - }, { "name": "cap_config", "base": "", @@ -38,20 +24,6 @@ { "name": "next_unmapped_id", "type": "uint64" - }, - { - "name": "next_cd_id", - "type": "uint64" - } - ] - }, - { - "name": "cd_key", - "base": "", - "fields": [ - { - "name": "request_id", - "type": "uint64" } ] }, @@ -65,50 +37,6 @@ } ] }, - { - "name": "cooldown_entry", - "base": "", - "fields": [ - { - "name": "request_id", - "type": "uint64" - }, - { - "name": "position_id", - "type": "uint64" - }, - { - "name": "wire_account", - "type": "name" - }, - { - "name": "chain_kind", - "type": "ChainKind" - }, - { - "name": "amount", - "type": "asset" - }, - { - "name": "eligible_at_epoch", - "type": "uint32" - }, - { - "name": "requested_at_epoch", - "type": "uint32" - } - ] - }, - { - "name": "flushcd", - "base": "", - "fields": [ - { - "name": "current_epoch", - "type": "uint32" - } - ] - }, { "name": "import_credit", "base": "", @@ -271,21 +199,11 @@ } ], "actions": [ - { - "name": "availstake", - "type": "availstake", - "ricardian_contract": "" - }, { "name": "claim", "type": "claim", "ricardian_contract": "" }, - { - "name": "flushcd", - "type": "flushcd", - "ricardian_contract": "" - }, { "name": "importdone", "type": "importdone", @@ -324,31 +242,6 @@ "key_types": ["name"], "table_id": 40418 }, - { - "name": "cdqueue", - "type": "cooldown_entry", - "index_type": "i64", - "key_names": ["request_id"], - "key_types": ["uint64"], - "table_id": 24354, - "secondary_indexes": [ - { - "name": "bywirechn", - "key_type": "uint128", - "table_id": 36786 - }, - { - "name": "byeligible", - "key_type": "uint64", - "table_id": 62191 - }, - { - "name": "byposition", - "key_type": "uint64", - "table_id": 6853 - } - ] - }, { "name": "pclaims", "type": "pending_claim", @@ -400,12 +293,7 @@ ], "ricardian_clauses": [], "variants": [], - "action_results": [ - { - "name": "availstake", - "result_type": "uint64" - } - ], + "action_results": [], "enums": [ { "name": "ChainKind", diff --git a/contracts/sysio.cap/sysio.cap.wasm b/contracts/sysio.cap/sysio.cap.wasm index e1edd421f9351e5863b65f69456a2613e4d06469..3d19d32cc90356f4533d815cbb343371ec26be49 100755 GIT binary patch delta 4270 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 Xxs2{19H-O}bsA8y^Vm-prTBjV Date: Wed, 13 May 2026 12:48:49 -0500 Subject: [PATCH 07/12] sysio.cap: make converter chain-aware (add Solana) Jack delivered the Solana balances endpoint at /opp/solana/balances (2026-05-13). Three differences from the ETH side that the converter now handles: 1. Address encoding: base58 (case-sensitive), 32 raw bytes, vs ETH's 0x-prefixed lowercase hex 20 bytes. Wrong case on a base58 string gives a different key, so a single .lower() pre-fold cannot be shared with ETH. 2. Source precision: 9 decimals (lamport-aligned with WIRE atomic) vs ETH's 18 decimals. Per-chain divisor is now 10^(src - 9): 1 for SOL, 1e9 for ETH. Solana yields zero dust by construction. 3. yieldClaimed is absent on SOL stakers/purchasers; the existing dict.get("yieldClaimed", "0") fallback already handles that. Also: per Jack, a Solana address may appear in both `purchasers` and `stakers` (9 such overlaps in today's snapshot). The accumulator is now keyed by decoded raw address bytes, so per-array contributions sum correctly regardless of source string form. Verified: ETH output byte-identical to pre-refactor (47 addresses, 217,941.104919 WIRE, 1 batch). SOL: 31 unique addresses (24 + 16 - 9 overlap), 40,652.206359 WIRE, zero dust (divisor 1), all 9 overlap addresses summed correctly. Output `native_address` is the hex spelling of raw decoded bytes (40 chars ETH / 64 chars SOL), which the sysio.cap ABI consumes as `bytes`. --- contracts/sysio.cap/tools/convert_import.py | 148 ++++++++++++++------ 1 file changed, 109 insertions(+), 39 deletions(-) diff --git a/contracts/sysio.cap/tools/convert_import.py b/contracts/sysio.cap/tools/convert_import.py index 1e90a47f16..b6a2e04c8f 100755 --- a/contracts/sysio.cap/tools/convert_import.py +++ b/contracts/sysio.cap/tools/convert_import.py @@ -1,28 +1,46 @@ #!/usr/bin/env python3 """Convert the indexer JSON dump into sysio.cap::importseed action batches. -Live source (Ethereum balances): - curl -H 'x-api-key: ' https://index.wire.foundation/opp/balances +Live sources: + ETH: curl -H 'x-api-key: ' https://index.wire.foundation/opp/balances + SOL: curl -H 'x-api-key: ' https://index.wire.foundation/opp/solana/balances -Schema (verified against the /opp/balances endpoint as of 2026-05-13): +Schema (verified 2026-05-13): metadata bookkeeping; not consumed by the contract. Notable fields: - generatedAt, totalMessages, yieldDust (indexer-side dust ledger) - purchasers[] {address, totalPretokens, yieldClaimed, ...} - owed = totalPretokens (already net of yieldClaimed) - stakers[] {address, pretokenYield, yieldClaimed, ...} - owed = pretokenYield - yieldClaimed + generatedAt, totalMessages, yieldDust (ETH; not present on SOL). + purchasers[] {address, totalPretokens, yieldClaimed?, ...} + owed = totalPretokens (already net of yieldClaimed; field + absent on SOL). + stakers[] {address, pretokenYield, yieldClaimed?, ...} + owed = pretokenYield - yieldClaimed (yieldClaimed absent + on SOL -> defaults to 0). + +A given address may appear in both arrays (~9 such addresses in current +SOL snapshot); contributions sum per address. + +Per-chain conventions: + CHAIN_KIND_ETHEREUM + address 0x-prefixed lowercase hex, 20 raw bytes + source 18 decimals (wei-style) + divisor 10^9 (source 1e18 -> WIRE atomic 1e9) + CHAIN_KIND_SOLANA + address base58 (case-sensitive), 32 raw bytes + source 9 decimals (lamport-style; same as WIRE atomic) + divisor 1 (no scaling needed) Per-address conversion: - total_pretokens = sum(purchaser.totalPretokens) - + sum(staker.pretokenYield - staker.yieldClaimed) - wire_atomic = total_pretokens // 1_000_000_000 (floor, drop sub-atomic dust) + total = sum(purchaser.totalPretokens) + + sum(staker.pretokenYield - staker.yieldClaimed) + wire_atomic = total // divisor (floor; drop sub-atomic dust) -Rows with wire_atomic == 0 are filtered. Output is a JSON array of importseed -action arg objects, each batched up to --batch-size credits per call. +Rows with wire_atomic == 0 are filtered. Output is a JSON array of +importseed action arg objects, each batched up to --batch-size credits +per call. `native_address` is emitted as the hex spelling of the raw +bytes (no 0x prefix), which the sysio.cap ABI consumes as `bytes`. Usage: - ./convert_import.py balances.json > batches.json - ./convert_import.py balances.json --batch-size 100 --chain CHAIN_KIND_ETHEREUM + ./convert_import.py eth_balances.json --chain CHAIN_KIND_ETHEREUM > eth.json + ./convert_import.py sol_balances.json --chain CHAIN_KIND_SOLANA > sol.json """ import argparse @@ -30,12 +48,51 @@ import sys from collections import defaultdict from pathlib import Path - -# WIRE has 9 decimals; source pretoken values are 1e18 scale (wei-style). -# Flooring source by DUST_BASE drops the sub-atomic remainder. -DUST_BASE = 1_000_000_000 - - +from typing import Callable + + +# --------------------------------------------------------------------------- +# Base58 decoder (Bitcoin / Solana alphabet). Stdlib has no base58. +# --------------------------------------------------------------------------- +B58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" +B58_INDEX = {c: i for i, c in enumerate(B58_ALPHABET)} + + +def base58_decode(s: str) -> bytes: + n = 0 + for c in s: + if c not in B58_INDEX: + raise ValueError(f"invalid base58 character: {c!r}") + n = n * 58 + B58_INDEX[c] + leading_ones = len(s) - len(s.lstrip("1")) + payload = n.to_bytes((n.bit_length() + 7) // 8, "big") if n else b"" + return b"\x00" * leading_ones + payload + + +def eth_decode(addr: str) -> bytes: + return bytes.fromhex(addr.lower().removeprefix("0x")) + + +# --------------------------------------------------------------------------- +# Chain configuration +# --------------------------------------------------------------------------- +CHAIN_CONFIG = { + "CHAIN_KIND_ETHEREUM": { + "decoder": eth_decode, + "addr_len": 20, + "divisor": 10**9, # source 1e18 -> WIRE atomic 1e9 + }, + "CHAIN_KIND_SOLANA": { + "decoder": base58_decode, + "addr_len": 32, + "divisor": 1, # source 1e9 already matches WIRE atomic + }, +} + + +# --------------------------------------------------------------------------- +# Pipeline +# --------------------------------------------------------------------------- def parse_args() -> argparse.Namespace: ap = argparse.ArgumentParser( description="Emit sysio.cap::importseed batches from the indexer JSON.", @@ -43,38 +100,49 @@ def parse_args() -> argparse.Namespace: epilog=__doc__, ) ap.add_argument("input", type=Path, - help="Indexer JSON file (response_*.json shape)") + help="Indexer JSON file (ETH or SOL /opp/[solana/]balances shape)") ap.add_argument("--batch-size", type=int, default=50, help="Credits per importseed call (default: 50)") ap.add_argument("--chain", default="CHAIN_KIND_ETHEREUM", + choices=sorted(CHAIN_CONFIG.keys()), help="ChainKind enum name (default: CHAIN_KIND_ETHEREUM)") return ap.parse_args() -def accumulate(data: dict) -> dict[str, int]: - """Return {hex_address_without_prefix: total_pretokens_owed}.""" - acc: dict[str, int] = defaultdict(int) +def accumulate(data: dict, decoder: Callable[[str], bytes], addr_len: int) -> dict[bytes, int]: + """Return {raw_address_bytes: total_pretokens_owed}. + + Addresses appearing in both `purchasers` and `stakers` sum together. + The accumulator is keyed by decoded raw bytes so that case / format + normalization happens exactly once at decode time. + """ + acc: dict[bytes, int] = defaultdict(int) + + def addr_bytes(s: str) -> bytes: + b = decoder(s) + if len(b) != addr_len: + raise ValueError(f"address {s!r} decoded to {len(b)} bytes, expected {addr_len}") + return b + for row in data.get("purchasers") or []: - addr = row["address"].lower().removeprefix("0x") - acc[addr] += int(row["totalPretokens"]) + acc[addr_bytes(row["address"])] += int(row["totalPretokens"]) for row in data.get("stakers") or []: - addr = row["address"].lower().removeprefix("0x") owed = int(row["pretokenYield"]) - int(row.get("yieldClaimed", "0")) if owed > 0: - acc[addr] += owed + acc[addr_bytes(row["address"])] += owed return acc -def to_credits(accumulator: dict[str, int]) -> tuple[list[dict], int]: - """Floor each total at DUST_BASE; return (credits, total_dropped_dust).""" +def to_credits(accumulator: dict[bytes, int], divisor: int) -> tuple[list[dict], int]: + """Floor each total at `divisor`; return (credits, dropped_dust_total).""" credits = [] dropped_dust = 0 - for addr, total in sorted(accumulator.items()): - atomic, dust = divmod(total, DUST_BASE) + for addr_bytes, total in sorted(accumulator.items()): + atomic, dust = divmod(total, divisor) dropped_dust += dust if atomic <= 0: continue - credits.append({"native_address": addr, "wire_atomic": atomic}) + credits.append({"native_address": addr_bytes.hex(), "wire_atomic": atomic}) return credits, dropped_dust @@ -87,10 +155,11 @@ def batched(credits: list[dict], chain: str, batch_size: int) -> list[dict]: def main() -> int: args = parse_args() + cfg = CHAIN_CONFIG[args.chain] data = json.loads(args.input.read_text()) - accumulator = accumulate(data) - credits, dropped_dust = to_credits(accumulator) + accumulator = accumulate(data, cfg["decoder"], cfg["addr_len"]) + credits, dropped_dust = to_credits(accumulator, cfg["divisor"]) batches = batched(credits, args.chain, args.batch_size) json.dump(batches, sys.stdout, indent=2) @@ -100,17 +169,18 @@ def main() -> int: meta = data.get("metadata") or {} lines = [ f"input: {args.input}", + f"chain: {args.chain}", f"generatedAt: {meta.get('generatedAt', '')}", f"totalMessages: {meta.get('totalMessages', '')}", f"indexer yieldDust: {meta.get('yieldDust', '')}" - " (indexer-side ledger; informational)", + " (indexer-side ledger; ETH only)", f"unique addresses: {len(accumulator)}", f"non-zero credits: {len(credits)}", f"batches: {len(batches)} (size {args.batch_size})", f"total credited: {total_atomic} atomic WIRE" - f" ({total_atomic / DUST_BASE:.6f} WIRE)", + f" ({total_atomic / 10**9:.6f} WIRE)", f"dropped dust: {dropped_dust} sub-atomic units" - f" ({dropped_dust / 10**18:.2e} WIRE)", + f" (divisor {cfg['divisor']})", ] print(*lines, sep="\n", file=sys.stderr) return 0 From 833eb3a63cdc17119c79829c3d42abbe91984cc0 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Wed, 13 May 2026 15:08:48 -0500 Subject: [PATCH 08/12] sysio.cap: remove speculative positions table Per Jack (2026-05-13) on Q4: the depot owns the proportional split of bulk WIRE allocations using pretoken amounts + pretoken yield as the weight, snapshot-based at allocation time. Shares are LIQ-supply accounting, irrelevant to wire emissions. No time-weighting. The positions table I scaffolded carried outpost_position_id, status, lifecycle timestamps, and a 3-index kv::table -- none of which fits a pretoken-balance-weight model. Strip it now (same pattern as the cooldown removal in 237356f937); a per-user pretoken-balance ledger gets added when the StakingReward inbound handler lands and we know which attestation(s) maintain those balances. Removed: - position struct, position_key, positions_t kv::table - cap_counters::next_position_id field - private `using StakeStatus = opp::types::StakeStatus;` (no remaining caller after dropping position.status) WASM 18,950 -> 18,906 bytes; ABI 9,174 -> 6,184 bytes (-33%). --- .../sysio.cap/include/sysio.cap/sysio.cap.hpp | 68 ++--------- contracts/sysio.cap/sysio.cap.abi | 107 ------------------ contracts/sysio.cap/sysio.cap.wasm | Bin 18950 -> 18906 bytes 3 files changed, 10 insertions(+), 165 deletions(-) diff --git a/contracts/sysio.cap/include/sysio.cap/sysio.cap.hpp b/contracts/sysio.cap/include/sysio.cap/sysio.cap.hpp index 1d0e3d6b38..0c01357158 100644 --- a/contracts/sysio.cap/include/sysio.cap/sysio.cap.hpp +++ b/contracts/sysio.cap/include/sysio.cap/sysio.cap.hpp @@ -17,9 +17,10 @@ namespace sysio { * @brief sysio.cap — depot-side WIRE distribution and claim ledger. * * Holds the pending-WIRE balances owed to LIQ-token stakers and pre-launch - * pretoken purchasers. The outpost owns share computation (per-staker - * `share_bps` arrives pre-computed in `StakingReward`); wire-sysio just - * credits, holds, and pays out. + * pretoken purchasers. The depot owns proportional distribution of bulk + * WIRE allocations (per Jack 2026-05-13: outpost only records events, the + * depot splits proportionally using pretoken amounts + pretoken yield as + * the weight; no time-weighting, snapshot-based at allocation time). * * - `pending_claims` is the per-Wire-account ledger of WIRE owed. * Users call `claim` to drain via inline transfer. @@ -27,13 +28,15 @@ namespace sysio { * stakers / purchasers who don't have a Wire account yet. Completing * AuthX linking inline-calls `linkswept` which moves the credit into * `pending_claims`. - * - `positions` carries lifecycle metadata only — status, owner mapping, - * timestamps. Shares and principal live at the outpost. * * No cooldown/withdrawal machinery for v1 (Jack 2026-05-13: outpost has * no cooldown scenarios in this wave). When withdrawals come online * post-launch, the cooldown-queue + maturation-flush pattern can be * added back — see opreg's `withdraw_queue` for reference. + * + * Per-user pretoken-balance state needed for ongoing `StakingReward` + * proration is deferred -- add when the StakingReward handler lands and + * we know exactly which attestation(s) maintain the balances. */ class [[sysio::contract("sysio.cap")]] cap : public contract { public: @@ -100,55 +103,6 @@ namespace sysio { // Tables // ----------------------------------------------------------------------- - /// Position lifecycle metadata. Outpost owns shares and principal; - /// wire-sysio stores only what it uniquely tracks. - struct position_key { - uint64_t id; - uint64_t primary_key() const { return id; } - SYSLIB_SERIALIZE(position_key, (id)) - }; - - struct [[sysio::table("positions")]] position { - uint64_t id = 0; - opp::types::ChainKind chain_kind = opp::types::ChainKind::CHAIN_KIND_UNKNOWN; - std::vector native_address; - uint64_t outpost_position_id = 0; - opp::types::StakeStatus status = opp::types::StakeStatus::STAKE_STATUS_UNKNOWN; - name wire_account; - uint64_t opened_at_ms = 0; - uint64_t last_status_ms = 0; - - uint64_t primary_key() const { return id; } - uint64_t by_wire_account() const { return wire_account.value; } - uint64_t by_status() const { return static_cast(status); } - - /// Composite: (chain << 64 | first 8 bytes of native_address). - /// 8-byte prefix gives a 2^64 namespace per chain — collisions on - /// 20-byte ETH addresses or 32-byte SOL pubkeys are negligible at - /// expected user counts. - uint128_t by_chain_addr() const { - if (native_address.empty()) return 0; - uint64_t prefix = 0; - const size_t n = native_address.size() < sizeof(uint64_t) - ? native_address.size() : sizeof(uint64_t); - std::memcpy(&prefix, native_address.data(), n); - return (static_cast(chain_kind) << 64) | prefix; - } - - SYSLIB_SERIALIZE(position, - (id)(chain_kind)(native_address)(outpost_position_id) - (status)(wire_account)(opened_at_ms)(last_status_ms)) - }; - - using positions_t = sysio::kv::table<"positions"_n, position_key, position, - sysio::kv::index<"bywire"_n, - sysio::const_mem_fun>, - sysio::kv::index<"bystatus"_n, - sysio::const_mem_fun>, - sysio::kv::index<"bychainad"_n, - sysio::const_mem_fun> - >; - /// Pending-claims: per-Wire-account WIRE owed. struct pclaim_key { uint64_t wire_account; @@ -210,19 +164,17 @@ namespace sysio { using capcfg_t = sysio::kv::global<"capcfg"_n, cap_config>; - /// Monotonic id counters for position / unmapped rows. + /// Monotonic id counter for unmapped rows. struct [[sysio::table("capcounters")]] cap_counters { - uint64_t next_position_id = 1; uint64_t next_unmapped_id = 1; - SYSLIB_SERIALIZE(cap_counters, (next_position_id)(next_unmapped_id)) + SYSLIB_SERIALIZE(cap_counters, (next_unmapped_id)) }; using capcounters_t = sysio::kv::global<"capcounters"_n, cap_counters>; private: using ChainKind = opp::types::ChainKind; - using StakeStatus = opp::types::StakeStatus; using TokenKind = opp::types::TokenKind; }; diff --git a/contracts/sysio.cap/sysio.cap.abi b/contracts/sysio.cap/sysio.cap.abi index ce0ffa81a5..635bad9a35 100644 --- a/contracts/sysio.cap/sysio.cap.abi +++ b/contracts/sysio.cap/sysio.cap.abi @@ -17,10 +17,6 @@ "name": "cap_counters", "base": "", "fields": [ - { - "name": "next_position_id", - "type": "uint64" - }, { "name": "next_unmapped_id", "type": "uint64" @@ -112,54 +108,6 @@ } ] }, - { - "name": "position", - "base": "", - "fields": [ - { - "name": "id", - "type": "uint64" - }, - { - "name": "chain_kind", - "type": "ChainKind" - }, - { - "name": "native_address", - "type": "bytes" - }, - { - "name": "outpost_position_id", - "type": "uint64" - }, - { - "name": "status", - "type": "StakeStatus" - }, - { - "name": "wire_account", - "type": "name" - }, - { - "name": "opened_at_ms", - "type": "uint64" - }, - { - "name": "last_status_ms", - "type": "uint64" - } - ] - }, - { - "name": "position_key", - "base": "", - "fields": [ - { - "name": "id", - "type": "uint64" - } - ] - }, { "name": "setconfig", "base": "", @@ -250,31 +198,6 @@ "key_types": ["uint64"], "table_id": 16291 }, - { - "name": "positions", - "type": "position", - "index_type": "i64", - "key_names": ["id"], - "key_types": ["uint64"], - "table_id": 39131, - "secondary_indexes": [ - { - "name": "bywire", - "key_type": "uint64", - "table_id": 45669 - }, - { - "name": "bystatus", - "key_type": "uint64", - "table_id": 6749 - }, - { - "name": "bychainad", - "key_type": "uint128", - "table_id": 65189 - } - ] - }, { "name": "unmapped", "type": "unmapped_token", @@ -320,36 +243,6 @@ "value": 4 } ] - }, - { - "name": "StakeStatus", - "type": "int32", - "values": [ - { - "name": "STAKE_STATUS_UNKNOWN", - "value": 0 - }, - { - "name": "STAKE_STATUS_WARMUP", - "value": 1 - }, - { - "name": "STAKE_STATUS_COOLDOWN", - "value": 2 - }, - { - "name": "STAKE_STATUS_ACTIVE", - "value": 3 - }, - { - "name": "STAKE_STATUS_TERMINATED", - "value": 240 - }, - { - "name": "STAKE_STATUS_SLASHED", - "value": 241 - } - ] } ] } \ No newline at end of file diff --git a/contracts/sysio.cap/sysio.cap.wasm b/contracts/sysio.cap/sysio.cap.wasm index 3d19d32cc90356f4533d815cbb343371ec26be49..30aff6ba56f0352b74ecc26c4b5f3408d949d6dc 100755 GIT binary patch delta 792 zcmYLH&ubG=5Pm;4n@u-qUz&(9A>DafrCGg*f_u$dP>U3_sGy+#zy=TTEJP1#dhp`w zp(hbR6b~ZgC`bw(MDS)2O2i<7KTf#_-gIV@*2~N{^WOVr=6!Q-lkZyo>Wvh??hl6Y z+d=nZ(GfjiUo^VCz5r?d@Dy!dq-sq&AVEg5FA@$;Bh1Be!a*i1mOsz~izEx(SdKY} zmT+{Q9Mc}XGGm^e5+g3!;6eUOiA_mOE!Bk?ET(!6;X5~_tPeeD9#3&`mc|<;gp@@% zkFb^9T|k5#njuTInq~=>5vtU9sa}+ADjZ3b^e&f}VP<2Sshm$qY>|v}Hp@TS&qr0M zE@XYG6gYUQjdLv8Yt2KojA-NK(}z2righKch-PFTG}rmaerc{QS8tlXynC8BwE$-p zKy?q4LD_Rl*ZG2dzH~LHf^3PkP;|0RvYtYxgB@Xm0;(1Q&8s#Ej`Fd+5u7+O4<&UT zd5bt4_-OFwW67Wh{ywVMx505f9{divyoXYenvhHQEs|(LLd=0jLZI9G19fq#ug>{U zfXa-kKvgRLtB}t60I|h#3wMIF1&qy>GusvOdC1fyEP-Qficr|^y1e2NtB2Z#xB)nu zpDm1beSA6HvI+9IgL-&{H&rJThgoua{OSF(4^MWsnU*H69f|*8Ji8vAT>1K%2Eqsx zCZ}qlW7sMVgzJZ4q3)wsgP+SL$CW)6HMnPY-5>y-of;yb4bKSBDOtxz4emyF;2&D6 BoKFA% delta 831 zcmYLHL2DCH5PtLCZZ^p#?Q2#rHYS<3wU`KkD43gMBWe!?6&373f57GxZ-S6|k@P0M zDm_UNMDQSmcoQYPhzQw}Wd2E&p1 zc{Hq@V`>7eRBOKqg_v7F`uiqRg#Q8p$pkbG`9eEx-$(aV+4q5Gy1EhSK) zlb}5y1!8t7q97`CTWSI*8`3cu4#IeJrJ@PM{qu4RvW*^QE*D9ID@EnlWK&p(O^)EF zOv2^~Zcd?}vh<*kvIL6+8_CldLaa3sv6-E+3^hT`Lzy|%rn(osrqZ4Mt)hrTw6-m@ z>vK2MBOlI{gn#~@r^nZlHg$}Eo^I0|bM|!gSUJ&Id-L+iPFA-}DXB=u*xs#Phq2wN zE-eK2qJ8LHVo4*>NfQhXfW}!{sojAYd%w0)4^TDaawxk=BS}l4)1`)}KZ$|;Q!^0Q zpneJ__Co#aVi61aD(U*{SpLu!AM>d}K9=m$`WcuEKh}HDS55UB*c-MQ-3~lx5kcxX z^wl#`2S0;R**UM%#z(@Rzv_RqTfHgy^j9e*OIF%Zr_DVN~Q9N71i{%62h4ck;(O zo`Nzmmx9)1+D)-UbI|5dnw$42;_&@K Date: Thu, 14 May 2026 08:23:46 -0500 Subject: [PATCH 09/12] sysio.cap: bump converter default batch-size to 10000 The accrue-then-claim model means importseed does pure table writes (zero inline transfers) -- the per-row cost is small and we have generous headroom inside the 150ms execution / 500KB trx envelope. Concretely, at today's per-credit estimate, a single trx fits ~12-17K credits before either limit bites. Default --batch-size moves from 50 to 10000. Effect: - today (78 users total): 1 batch per chain (was 1 already) - 50K users per chain: ~5 batches (was 1000) - 500K users per chain: ~50 batches (was 10000) Most launches collapse to a single trx per chain. Operators only need to lower --batch-size if a real on-chain measurement shows a trx hitting the size or execution limit. Docstring updated to call out the one-shot intent and the envelope the default is tuned against. --- contracts/sysio.cap/tools/convert_import.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/contracts/sysio.cap/tools/convert_import.py b/contracts/sysio.cap/tools/convert_import.py index b6a2e04c8f..31f817530e 100755 --- a/contracts/sysio.cap/tools/convert_import.py +++ b/contracts/sysio.cap/tools/convert_import.py @@ -38,6 +38,14 @@ per call. `native_address` is emitted as the hex spelling of the raw bytes (no 0x prefix), which the sysio.cap ABI consumes as `bytes`. +Batching: default is 10,000 credits per trx, which fits well inside +the 150ms execution / 500KB transaction-size envelope. At launch +scale (under ~50K users per chain) this typically produces a single +batch; importseed accrues into `unmapped_tokens` with zero inline +transfers, so the only cost driver is the per-row table write. +Operators only need to lower --batch-size if a trx hits the size or +execution limit at very high user counts. + Usage: ./convert_import.py eth_balances.json --chain CHAIN_KIND_ETHEREUM > eth.json ./convert_import.py sol_balances.json --chain CHAIN_KIND_SOLANA > sol.json @@ -101,8 +109,10 @@ def parse_args() -> argparse.Namespace: ) ap.add_argument("input", type=Path, help="Indexer JSON file (ETH or SOL /opp/[solana/]balances shape)") - ap.add_argument("--batch-size", type=int, default=50, - help="Credits per importseed call (default: 50)") + ap.add_argument("--batch-size", type=int, default=10_000, + help="Credits per importseed call (default: 10000; " + "fits the 150ms/500KB trx envelope with headroom). " + "Lower this only if you hit a trx size or execution limit.") ap.add_argument("--chain", default="CHAIN_KIND_ETHEREUM", choices=sorted(CHAIN_CONFIG.keys()), help="ChainKind enum name (default: CHAIN_KIND_ETHEREUM)") From 6d39d1f844a6c5fba9b9ede268c0be7cd3a3e5d3 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Mon, 18 May 2026 12:01:06 -0500 Subject: [PATCH 10/12] sysio.cap: per-staker reward payout and configurable claimable window StakingReward becomes a per-staker, per-source-chain-period message. Drop period_start_ms/period_end_ms; add reward_epoch_index (audit) and external_epoch_ref (dedupe/pruning) and staker_native_address so an unlinked staker's reward can be parked and swept on AuthX link. share_bps is informational; reward_amount is the staker's absolute prorated portion. sysio.msgch now dispatches STAKING_REWARD two ways: the aggregate native amount to sysio.reserv::onreward (unchanged), plus the per-staker body to the new sysio.cap::onreward. sysio.reserv's constant-product pricing (cp_output/to_unsigned/quote) moves into a shared static header helper; swapquote delegates to it. sysio.cap prices native -> WIRE through reserve::quote against reserv's published reserves table, since there is no synchronous inter-contract call. sysio.cap gains: - onreward: cursor-dedupes on external_epoch_ref, prices the reward, and credits pending_claims (AuthX-linked) or unmapped_tokens (not), staging in native units when no reserve quote exists yet. - retryconvert: bounded crank that re-prices staged rewards once a quote is available. - flushexpired: bounded crank that prunes rows past expires_at_sec; the WIRE stays in the sysio.cap balance, reverting to the capital fund. - setwindow + cap_config.claim_window_sec (default 180 days). - native_stage and reward_cursors tables; expires_at_sec on the claim ledgers; shared chain/outpost key derivation and credit/dedupe helpers. importseed now flows through the shared credit path. Test fixture corrected: sysio.authex is created by the tester bootstrap (no longer re-created), setreserve uses sysio.reserv's flat ABI, and each applied action closes its own block so an intentional replay reaches the contract instead of being rejected chain-side as a duplicate. Regenerates the wasm for all OPP-consuming system contracts (deterministic output of the attestations.proto change). --- contracts/sysio.authex/sysio.authex.wasm | Bin 39490 -> 39582 bytes contracts/sysio.bios/sysio.bios.wasm | Bin 40900 -> 40992 bytes contracts/sysio.cap/CMakeLists.txt | 1 + .../sysio.cap/include/sysio.cap/sysio.cap.hpp | 265 +++++++++++-- contracts/sysio.cap/src/sysio.cap.cpp | 368 +++++++++++++++--- contracts/sysio.cap/sysio.cap.abi | 272 +++++++++++++ contracts/sysio.cap/sysio.cap.wasm | Bin 18906 -> 38787 bytes contracts/sysio.chalg/sysio.chalg.wasm | Bin 25762 -> 25854 bytes contracts/sysio.epoch/sysio.epoch.wasm | Bin 67202 -> 68047 bytes contracts/sysio.msgch/src/sysio.msgch.cpp | 33 +- contracts/sysio.msgch/sysio.msgch.wasm | Bin 126553 -> 128333 bytes contracts/sysio.msig/sysio.msig.wasm | Bin 28391 -> 28483 bytes .../sysio.opp.common/opp_table_types.hpp | 14 +- contracts/sysio.opreg/sysio.opreg.wasm | Bin 78450 -> 78542 bytes .../include/sysio.reserv/sysio.reserv.hpp | 100 +++++ contracts/sysio.reserv/src/sysio.reserv.cpp | 74 +--- contracts/sysio.reserv/sysio.reserv.wasm | Bin 10266 -> 10649 bytes contracts/sysio.roa/sysio.roa.wasm | Bin 44021 -> 44113 bytes contracts/sysio.system/sysio.system.wasm | Bin 161840 -> 162689 bytes contracts/sysio.uwrit/sysio.uwrit.wasm | Bin 64935 -> 65384 bytes contracts/tests/sysio.cap_tests.cpp | 284 ++++++++++++-- .../sysio/opp/attestations/attestations.proto | 37 +- 22 files changed, 1224 insertions(+), 224 deletions(-) diff --git a/contracts/sysio.authex/sysio.authex.wasm b/contracts/sysio.authex/sysio.authex.wasm index ee52f41e5fb43c4ecd3dbd247cc0ae901a77b0d5..6e8ec671c1fbf0f9686ffd4b99cec34ca2ba1fe4 100755 GIT binary patch delta 1632 zcmbtUYfMx}6uxKf-CgX0ds*o661dBwdv|#(qCjZ{_bL!%m)IIy7YqS|B(S6^sMu%= zlCl(#tl&tsO``-&S}`QH1Clf*K9Z*VX>8JJ(}u*R{X>jtLz|{Ft+8h=P_|9~HMu!= z=FFMzeBYcib8m)RnkAzhV_jKBskf)6R~Q?w5C;sIA1%xSogsFR=|iH$$BM0ox}!U~ zo7xY)-gNZX;jUJ}N^`_?h@)Q7VSNmXAeiwp1DIhWnB&=7;v`se-X?=2Z-_ki!zHo^ zk|4i8OAMbv7JX{SvN+5}K@sHam7kLp7^9#!2+*@buuH^4Jc_726KZ1woRerUb4QGz zfL&UhXxqJfuH6w7v158rA&NMiAr17~i2{l=pvU1z)I7oI0Xs1(fr!YgZGZf+Ru}W98=++U8dBP~spp#=5RdSKYYwmtncaMPV zq^I-ZbJiAx^|h$KcNDBUU&Q#^dA6#x_|XWkiwMTaW9knh+&9+U??fG|5-Z|>ohBprUbLF6)o6fay|?1$m5H*cK+95 zprTAd7VlIm2s2lW37oX5=3_{t`FqySpy|$KULmL2}_h4%$xw#ZfN(7RoAJ4!i`L=$87eajD7J zPzIIslMKS^jw;#7dI#W+$%egXmm8X}T>|^91k!0o;~`Mkw~b*SE;Ie2Wfx^lzhcm} zW(=Ym4m4os!2^2;WYD>m^N_`S2QLF;vnQ=(1n;7&Z3^$=gSKt@SfPdO9-JR-U*3=} zaB3hs=&@iC)?_MJ0lDm_;5hGx&}-cyR`ky!ofa%N(aXJB>^C~Q#Z4&aJ{0)+DYs;BlqHEnnbHk2*>!lY4JZv)D%%zB9bv!aa WXRSH>mI|v+xj9CVGgHd3Fs;-pcWKxrHW$%TqHs; zfDQ~o4qMEUDO=1!FfnWwME^KkbS^Ol6PM`^5~s;BwjWD~S+c|rd`|(X(Z41)?>*;v zKhATW^PcnmJh?JQ#-mzIe_vn!L?n0O^j`k7)_HOH8YCr#!_CK!yxbMp&WUu2gW(7k?g1`EQZ8 zNx@0-(1Ww&HfT68kILGwAcx-5It{X*<0MYZUH>VnU6%mrq~5CoS^YAZ0?0IEcpemW z!El95UN`Osg??c?gX3pSRXCnDtwN!CCZ&NuA-(_b5;|6POZ1j`6-)e(maN5A(swcH^(^a_Tdi3x3 zPvgogZj~92gWubNr{myRglSWO88v&Ypp4O*FRDS-+Tw4~eqVeP+-hL^Crr%WN@mbr zC|z1t0U3)9eT=)L_mNadkCn!JRxB~?W08k;e1w{=?br($>*QrJMdC6(BrHXsVIQte zyLTS5Z0_-T9KmHIsIKj-B*?IG*EYakec2fDQCD`$02XR2{|OgcEf1n+zhBRoR__@# z#*R*EcXARK1Uw#`H()A%`wNFbAC4c1uJw2RhsiSys&+BadbPa>k5epWRD>2zD|4c=wv zsrR6n{!yETNc72B6g;E)m;a-#diAL{m~w$b`*^S+Z!?{$%K{f&th1PH|0GH_h~BGf zg$x=zd=0}gLuoiJ4b36NyCFAPbGq}A|Nk`qZG8rTe!de4& ztlp^41I+ld;Tg2S#xQo&T%(DAM8AHnLvK%)c>g$|h8I?-GdxAy0?lu!poXUV7_Du_ zXxiG`fH{lJPZP+dDJ_$bqfWPc0ExK*rrv~hC!J_Bt8Hyr1TSw#M4}gtjNk<>itLKb zn{d1od64Mk7+(-&`dxb|o}n%p0FPP~J;hq<=T~}oY>p?!D(JaR*?^ru{H%L6dgsSZ zbSt!`+lDsU%@Sw3>sX@xgpEgqvU>+1OP%f=)S;16usPQ zR*T;(;lV*)8mdHlX{Z)0KdgX5Ef~H|%nEy80!%-^2Zs2_0hCcuEhjCkeTzmqk;388 z0|_7zyn=Y?ejRx+2Gy)zOhi*1nwa{MhCNRj%)UI@Ph$H!`meoxJ~qWR5}pXMh&eiM V%4ZJ?xADjd$(-m^dndDXe*x)oeZc?# diff --git a/contracts/sysio.bios/sysio.bios.wasm b/contracts/sysio.bios/sysio.bios.wasm index c576c2a08a2bf341f634a9f0e2bed7bed7d14da3..70b21176ab45218912024a78c347abfb9e9fc9bd 100755 GIT binary patch delta 1677 zcmc&#TTC2f6#maY!bo&G4C*<}2u`pW!wT0C5EnUKB+Dezm@Z>xP86ae5mhB4z8J z^Bn+>G1cM&h3&EIge>-vWdUS1X3Y^@f`!OLazgleKX45q*0W= zOsYaTMf`)yRVb?wFY1%hy8yX*$4UpFf5`Cx%XPd6E`7++wE|TAe(p8Qt?PMDQPl3P z`vQVF>4Go}zzxXQrJv$BG+Rg`K1z@nNV!g`b?>mFV$OqVLM3)gHSvIx(;y_d*q`pb zNTRvG4LR&&K|0D83fvh++BA&Ph?`6|YS2?=CmK=dOrxZ-MH8IiF~EM$0StW4lZ||5 zJsYxBLD+9j#fW@hL>hq_6dMZBrgSvqU0zn%V-9T>71&E@N)b7iS=> zx7Y=kXRM?jmCVvPSc!=YF4v40L7T@2wzRP=MKu@0O^~J0rc6~D=CI9!Pk)9CN&@K# zs>~_4B6f1RG$YxBTjOx6N^OXa1RRLI5cn4I^;d&^ctGq_xg(0^+{7D8&Z)Er(ekF` z`iFI^<1UxROfkAK$yYg%^5_6RO zuf(cZZ^hXtIXsiX$^j0ibZM&(&un4qU6jAu_5(Wf?KKo{c(}ESRo5sMr_qMYdR|Fo zZ`E|7V|#51I9XC{;6LT$x+f6xdSey~)xEshob4l8m^bbKZyYr*Ihizg+rENz?3=dUWzSv=n`;*z#aYjH^^46ht)3hVa%!pQp z)0{q)c^^&1Lr~epzB?Fv?(iZK8|rs)7TjGT9A-LX_!7ybrkJClq6 delta 1745 zcmd5-YfKzf6u#%qx(l5yGql_7g2>M9(tYq)R%i;P2ux%V1PinxC@(2Ap@br(wNxpO zR&8vf7JE#pjTU~GCMIe%(-;tvB2nXyQc36!5;bW;2$-}bjm8)gA7^H_tfu}o@z2b; z-~GPtoO{o`GvhP-%d33!2Vv2a`svMb?%v;XG`_X9?Qm!JkpsDGi?9q!zyPE~U?xeD z08H90p%>zp$M_P+Oit6Tm^uKQm}K4oZmcoafr6LJtKh=3mNdyHnwi36PvjNASxcpM zEUrQTAI^$X^c>pw4Lu*Twm<+cSuau?PAH@JwS)^4`=m;W4@-9iuZ!bH(q+w__yK@d zo7rRonqRSfh$n3O!Kba*4kkiCtMg8hw@<9P*HV zdMKA9Eg=`hBcV#duZ5h1+zQ6-bBHUlfjiT8! zV~&zcbGL3RkiVZZN=z9Pff5T=hE0Vj83xf3GqL!2oNR#R7-%*?gpvg+tT$}aek|$a z!}s!+Mz=!n^QwrHiVDTR{B`hEh@qpf4%fs`>Qr=9$70|}jM^DP9lh(^rWo2my;yy8 zh3cFyby)(UBBlMR-K#RaH&iN8nuDz36 zZ3P8Xe>WY0(kE`m(-afW0syWJ72uKG7f2&j*K)epZMEIt!RxgzCAr*OG0FC1#iWjy zD(IAP{it!-gI#-y|D)Nyz1yjVwK^w$z4!SoMo%AzW;f~CO|nO;dGaC{q=|ng;Pg`k zQ4@Q82Q6lx{t;tg9{j%kl-Z;AHiUCW9XQg^WDphnz2O8Aho63%Hp20YHRA6?d%Xjj z8^dBm)0M`=e!P0hjo&tQ6S1%f@d=!ES5^#I6?@ z*Gt>bHx(DWH|K~RX;76ey19{~exC+^dhs-Q4-Swwt_@Vvv-o(s5tl2}V?UQ8N{Vc5 zW9R8C?BEV+Kmf(TTO|H<@D3HWGUOFVmvC~N2A5C1tADtwUjCB?Rj>S^{|PEjPtm|~ zrqD#M(l_>S!p@I=CQ`vH`s2_V-h5Bj{c>)~YMjhT5QJeE;wYW5Yd6pdkgEOsR;KtD D)~Js( diff --git a/contracts/sysio.cap/CMakeLists.txt b/contracts/sysio.cap/CMakeLists.txt index 9eb89894b8..6a14948542 100644 --- a/contracts/sysio.cap/CMakeLists.txt +++ b/contracts/sysio.cap/CMakeLists.txt @@ -33,6 +33,7 @@ if(BUILD_SYSTEM_CONTRACTS) $ $ $ + $ $ $ # cdt-cpp ignores -isystem; add vcpkg root explicitly as -I so magic_enum resolves. diff --git a/contracts/sysio.cap/include/sysio.cap/sysio.cap.hpp b/contracts/sysio.cap/include/sysio.cap/sysio.cap.hpp index 0c01357158..28e81ec233 100644 --- a/contracts/sysio.cap/include/sysio.cap/sysio.cap.hpp +++ b/contracts/sysio.cap/include/sysio.cap/sysio.cap.hpp @@ -8,8 +8,12 @@ #include #include #include +#include // reserve::reserves_t + shared quote() #include +#include +#include +#include namespace sysio { @@ -17,26 +21,42 @@ namespace sysio { * @brief sysio.cap — depot-side WIRE distribution and claim ledger. * * Holds the pending-WIRE balances owed to LIQ-token stakers and pre-launch - * pretoken purchasers. The depot owns proportional distribution of bulk - * WIRE allocations (per Jack 2026-05-13: outpost only records events, the - * depot splits proportionally using pretoken amounts + pretoken yield as - * the weight; no time-weighting, snapshot-based at allocation time). + * pretoken purchasers, and the per-staker WIRE-side leg of the + * `STAKING_REWARD` flow. * - * - `pending_claims` is the per-Wire-account ledger of WIRE owed. - * Users call `claim` to drain via inline transfer. - * - `unmapped_tokens` is the per-(chain, native_pubkey) ledger for - * stakers / purchasers who don't have a Wire account yet. Completing - * AuthX linking inline-calls `linkswept` which moves the credit into - * `pending_claims`. + * Inbound `STAKING_REWARD` attestations route through `sysio.msgch`, which + * credits the outpost-side reserve (`sysio.reserv::onreward`) and dispatches + * the per-staker body here via `onreward`. `onreward` prices the native + * reward into WIRE off `sysio.reserv`'s published `reserves` table (there is + * no synchronous inter-contract call, so the read-only `swapquote` action is + * not usable on-chain — the shared `reserve::quote` helper is used instead), + * then credits the staker's claim ledger. * - * No cooldown/withdrawal machinery for v1 (Jack 2026-05-13: outpost has - * no cooldown scenarios in this wave). When withdrawals come online - * post-launch, the cooldown-queue + maturation-flush pattern can be - * added back — see opreg's `withdraw_queue` for reference. + * Ledgers: + * - `pending_claims` — per-Wire-account WIRE owed. `claim` drains a row via + * inline transfer. + * - `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`. + * - `native_stage` — rewards received while `sysio.reserv` had no quote + * (reserve not yet provisioned). Held in native units; `retryconvert` + * re-prices and promotes them once a quote is available. Nothing is + * dropped. + * - `reward_cursors` — per-(outpost_id, 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). * - * Per-user pretoken-balance state needed for ongoing `StakingReward` - * proration is deferred -- add when the StakingReward handler lands and - * we know exactly which attestation(s) maintain the balances. + * Claimable lifespan: every credited / staged balance carries an + * `expires_at_sec`. `flushexpired` prunes anything past it; the WIRE stays + * in the `sysio.cap` account balance — i.e. it reverts into the staking + * capital fund for redistribution. The window is configurable + * (`setwindow`), defaulting to 180 days. + * + * No cooldown/withdrawal machinery for v1 (no withdrawal flow in this + * wave). When withdrawals come online post-launch, the cooldown-queue + + * maturation-flush pattern can be added back — see opreg's + * `withdraw_queue` for reference. */ class [[sysio::contract("sysio.cap")]] cap : public contract { public: @@ -46,19 +66,29 @@ namespace sysio { 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; // WIRE token symbol. 9 decimals system-wide. static constexpr symbol WIRE_SYM = symbol("WIRE", 9); + // Default claimable-reward lifespan: 180 days, in seconds. Configurable + // per deployment via `setwindow`. + static constexpr uint32_t DEFAULT_CLAIM_WINDOW_SEC = 180u * 24u * 60u * 60u; + // ----------------------------------------------------------------------- // Actions // ----------------------------------------------------------------------- - /// Set cap-staking configuration. Placeholder while concrete fields are - /// still being decided; calling once initializes the singleton. + /// Initialize the config singleton (idempotent). The claimable-reward + /// window defaults to `DEFAULT_CLAIM_WINDOW_SEC`. [[sysio::action]] void setconfig(); + /// Set the claimable-reward window (seconds). Unclaimed balances older + /// than this revert to the capital fund on `flushexpired`. Auth=self. + [[sysio::action]] + void setwindow(uint32_t window_sec); + /// User-callable: drain the caller's `pending_claims` row via an inline /// transfer of WIRE from `sysio.cap` to `wire_account`. Erases the row. /// Reverts if no row exists or the balance is zero. @@ -66,14 +96,62 @@ namespace sysio { void claim(name wire_account); /// Internal: sweep an `unmapped_tokens` entry into `pending_claims` when - /// the staker / purchaser completes AuthX linking. Called inline by - /// `sysio.authex` after a successful link. No-op if no matching unmapped - /// row exists. + /// the staker / purchaser completes AuthX linking, and stamp the now + /// known Wire account onto any `native_stage` rows still awaiting a + /// quote (so `retryconvert` routes them to `pending_claims`). Called + /// inline by `sysio.authex` after a successful link. No-op if nothing + /// matches. Auth=sysio.authex. [[sysio::action]] void linkswept(name wire_account, 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, prices native -> WIRE off + /// `sysio.reserv`'s reserve, and credits `pending_claims` (if linked) + /// or `unmapped_tokens` (if not). If no quote is available the reward + /// is staged in native units for `retryconvert`. Auth=sysio.msgch. + /// + /// @param outpost_id Emitting outpost (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 (reserve pricing + parking). + /// @param staker_native_addr Staker's raw native address (dedupe + + /// parking key; always populated). + /// @param reward_kind Native TokenKind of the reward. + /// @param reward_amount Absolute native reward amount (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 (outpost, staker) — dedupe key. + /// @param share_bps Staker share in bps (informational only). + [[sysio::action]] + void onreward(uint64_t outpost_id, + std::string staker_wire_account, + opp::types::ChainKind reward_chain, + std::vector staker_native_addr, + opp::types::TokenKind reward_kind, + uint64_t reward_amount, + uint32_t reward_epoch_index, + uint64_t external_epoch_ref, + uint32_t share_bps); + + /// Permissionless crank: re-price up to `max_rows` `native_stage` rows + /// against the now-available reserve and promote successful ones into + /// the claim ledger. Rows that still have no quote are left for a later + /// call. Bounded so a single transaction can't be starved. + [[sysio::action]] + void retryconvert(uint32_t max_rows); + + /// Permissionless crank: prune up to `max_rows` expired ledger rows + /// (`pending_claims`, `unmapped_tokens`, `native_stage`). Erasing a + /// credited row leaves its WIRE in the `sysio.cap` balance — it reverts + /// into the staking capital fund for redistribution. Bounded. + [[sysio::action]] + void flushexpired(uint32_t max_rows); + /// 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 @@ -111,12 +189,15 @@ namespace sysio { }; struct [[sysio::table("pclaims")]] pending_claim { - name wire_account; - asset balance = asset{0, WIRE_SYM}; + name wire_account; + asset balance = asset{0, WIRE_SYM}; + /// Seconds since epoch after which `flushexpired` reverts this + /// balance to the capital fund. Refreshed on every credit. + uint32_t expires_at_sec = 0; uint64_t primary_key() const { return wire_account.value; } - SYSLIB_SERIALIZE(pending_claim, (wire_account)(balance)) + SYSLIB_SERIALIZE(pending_claim, (wire_account)(balance)(expires_at_sec)) }; using pclaims_t = sysio::kv::table<"pclaims"_n, pclaim_key, pending_claim>; @@ -134,19 +215,15 @@ namespace sysio { opp::types::ChainKind chain_kind = opp::types::ChainKind::CHAIN_KIND_UNKNOWN; std::vector native_pubkey; asset balance = asset{0, WIRE_SYM}; + uint32_t expires_at_sec = 0; uint64_t primary_key() const { return id; } uint128_t by_chain_addr() const { - if (native_pubkey.empty()) return 0; - uint64_t prefix = 0; - const size_t n = native_pubkey.size() < sizeof(uint64_t) - ? native_pubkey.size() : sizeof(uint64_t); - std::memcpy(&prefix, native_pubkey.data(), n); - return (static_cast(chain_kind) << 64) | prefix; + return chain_addr_key(chain_kind, native_pubkey); } - SYSLIB_SERIALIZE(unmapped_token, (id)(chain_kind)(native_pubkey)(balance)) + SYSLIB_SERIALIZE(unmapped_token, (id)(chain_kind)(native_pubkey)(balance)(expires_at_sec)) }; using unmapped_t = sysio::kv::table<"unmapped"_n, unmapped_key, unmapped_token, @@ -154,25 +231,135 @@ namespace sysio { sysio::const_mem_fun> >; - /// Cap-staking config singleton. `imported_complete` is the one-way flag - /// protecting the bootstrap `importseed` action (added in a follow-up). + /// Native-staged rewards awaiting a reserve quote. Held in native units; + /// `retryconvert` re-prices and promotes them. + struct stage_key { + uint64_t id; + uint64_t primary_key() const { return id; } + SYSLIB_SERIALIZE(stage_key, (id)) + }; + + struct [[sysio::table("nativestage")]] native_stage { + uint64_t id = 0; + uint64_t outpost_id = 0; + opp::types::ChainKind chain = opp::types::ChainKind::CHAIN_KIND_UNKNOWN; + std::vector native_pubkey; + /// Resolved Wire account, or default-constructed (value 0) if the + /// staker was not yet AuthX-linked when staged. `linkswept` stamps + /// this once the staker links so `retryconvert` routes to + /// `pending_claims`. + name wire_account; + // Always set explicitly when a row is created; the zero value + // (TOKEN_KIND_WIRE) is just the default-construction placeholder. + opp::types::TokenKind native_kind = opp::types::TokenKind::TOKEN_KIND_WIRE; + uint64_t native_amount = 0; + uint32_t reward_epoch_index = 0; + uint64_t external_epoch_ref = 0; + uint32_t expires_at_sec = 0; + + uint64_t primary_key() const { return id; } + + uint128_t by_chain_addr() const { + return chain_addr_key(chain, native_pubkey); + } + + SYSLIB_SERIALIZE(native_stage, + (id)(outpost_id)(chain)(native_pubkey)(wire_account) + (native_kind)(native_amount)(reward_epoch_index) + (external_epoch_ref)(expires_at_sec)) + }; + + using nativestage_t = sysio::kv::table<"nativestage"_n, stage_key, native_stage, + sysio::kv::index<"bychainad"_n, + sysio::const_mem_fun> + >; + + /// Per-(outpost_id, 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 outpost_id = 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_outpost_addr() const { + return outpost_addr_key(outpost_id, chain, native_pubkey); + } + + SYSLIB_SERIALIZE(reward_cursor, + (id)(outpost_id)(chain)(native_pubkey)(last_external_epoch_ref)) + }; + + using rwdcursors_t = sysio::kv::table<"rwdcursors"_n, rwdcur_key, reward_cursor, + sysio::kv::index<"byoutaddr"_n, + sysio::const_mem_fun> + >; + + /// Cap-staking config singleton. struct [[sysio::table("capcfg")]] cap_config { - bool imported_complete = false; + /// One-way flag protecting the bootstrap `importseed` action. + bool imported_complete = false; + /// Claimable-reward window in seconds (configurable; default 180d). + uint32_t claim_window_sec = DEFAULT_CLAIM_WINDOW_SEC; - SYSLIB_SERIALIZE(cap_config, (imported_complete)) + SYSLIB_SERIALIZE(cap_config, (imported_complete)(claim_window_sec)) }; using capcfg_t = sysio::kv::global<"capcfg"_n, cap_config>; - /// Monotonic id counter for unmapped rows. + /// Monotonic id counters. struct [[sysio::table("capcounters")]] cap_counters { - uint64_t next_unmapped_id = 1; + uint64_t next_unmapped_id = 1; + uint64_t next_stage_id = 1; + uint64_t next_cursor_id = 1; - SYSLIB_SERIALIZE(cap_counters, (next_unmapped_id)) + SYSLIB_SERIALIZE(cap_counters, (next_unmapped_id)(next_stage_id)(next_cursor_id)) }; using capcounters_t = sysio::kv::global<"capcounters"_n, cap_counters>; + // ----------------------------------------------------------------------- + // Shared key derivation + // + // uint128 secondary keys only NARROW a scan; the full identity is + // re-checked by an exact predicate (see scan_find in the .cpp), so an + // address-prefix collision is resolved deterministically rather than + // risking a hash collision on a 64-bit primary key. + // ----------------------------------------------------------------------- + + /// (chain, native address) -> uint128 narrowing key. High 64 bits = + /// chain; low 64 = first 8 bytes of the address. + static uint128_t chain_addr_key(opp::types::ChainKind chain, + const std::vector& addr) { + if (addr.empty()) return 0; + uint64_t prefix = 0; + const size_t n = addr.size() < sizeof(uint64_t) ? addr.size() : sizeof(uint64_t); + std::memcpy(&prefix, addr.data(), n); + return (static_cast(chain) << 64) | prefix; + } + + /// (outpost_id, chain, native address) -> uint128 narrowing key. High 64 + /// bits = outpost_id; low 64 = chain (high 32) xored with the first 4 + /// address bytes. + static uint128_t outpost_addr_key(uint64_t outpost_id, + 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(outpost_id) << 64) | lo; + } + private: using ChainKind = opp::types::ChainKind; using TokenKind = opp::types::TokenKind; diff --git a/contracts/sysio.cap/src/sysio.cap.cpp b/contracts/sysio.cap/src/sysio.cap.cpp index 89c63b3dbd..655b07fdb0 100644 --- a/contracts/sysio.cap/src/sysio.cap.cpp +++ b/contracts/sysio.cap/src/sysio.cap.cpp @@ -1,21 +1,141 @@ #include -#include -#include +#include #include +#include namespace sysio { namespace { using opp::types::ChainKind; +using opp::types::TokenKind; -uint128_t make_chain_addr_key(ChainKind chain, const std::vector& addr) { - if (addr.empty()) return 0; - uint64_t prefix = 0; - const size_t n = addr.size() < sizeof(uint64_t) ? addr.size() : sizeof(uint64_t); - std::memcpy(&prefix, addr.data(), n); - return (static_cast(chain) << 64) | prefix; +/// Deterministic wall-clock seconds (block time). Used for the claimable +/// window; epoch indices carried on the attestation are for audit only. +uint32_t now_sec() { + return static_cast(current_time_point().sec_since_epoch()); +} + +/// 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). +template +auto scan_find(Index& idx, uint128_t key, KeyFn key_of, MatchFn matches) { + auto it = idx.lower_bound(key); + for (; it != idx.end() && key_of(*it) == key; ++it) { + if (matches(*it)) break; + } + if (it != idx.end() && key_of(*it) == key && matches(*it)) return it; + return idx.end(); +} + +/// Current claimable-reward window (seconds) from config, default if unset. +uint32_t config_window(name self) { + cap::capcfg_t cfg(self); + return cfg.get_or_default(cap::cap_config{}).claim_window_sec; +} + +/// Allocate the next id from one of the monotonic counters. `pick` returns a +/// reference to the field to bump. +template +uint64_t next_id(name self, Pick pick) { + cap::capcounters_t cnt(self); + cap::cap_counters c = cnt.get_or_default(cap::cap_counters{}); + uint64_t& field = pick(c); + uint64_t id = field++; + cnt.set(c, self); + return id; +} + +/// Credit `amt` WIRE to the staker. Linked (`wacct` set) -> `pending_claims`; +/// otherwise parked in `unmapped_tokens` keyed by (chain, addr). Either way +/// the row's expiry is refreshed to now + window. Shared by `onreward`, +/// `retryconvert`, `linkswept`, and `importseed` so the upsert + expiry logic +/// lives in exactly one place. +void credit_wire(name self, name wacct, ChainKind chain, + const std::vector& addr, const asset& amt, uint32_t window) { + const uint32_t exp = now_sec() + window; + + if (wacct.value != 0) { + cap::pclaims_t pclaims(self); + auto it = pclaims.find(cap::pclaim_key{wacct.value}); + if (it == pclaims.end()) { + pclaims.emplace(self, cap::pclaim_key{wacct.value}, + cap::pending_claim{ .wire_account = wacct, + .balance = amt, + .expires_at_sec = exp }); + } else { + pclaims.modify(same_payer, cap::pclaim_key{wacct.value}, [&](auto& r) { + r.balance += amt; + r.expires_at_sec = exp; + }); + } + return; + } + + cap::unmapped_t unmapped(self); + auto idx = unmapped.template get_index<"bychainad"_n>(); + auto it = scan_find(idx, cap::chain_addr_key(chain, addr), + [](const auto& r) { return r.by_chain_addr(); }, + [&](const auto& r) { + return r.chain_kind == chain && r.native_pubkey == addr; + }); + if (it == idx.end()) { + uint64_t id = next_id(self, [](cap::cap_counters& c) -> uint64_t& { + return c.next_unmapped_id; + }); + unmapped.emplace(self, cap::unmapped_key{id}, + cap::unmapped_token{ .id = id, + .chain_kind = chain, + .native_pubkey = addr, + .balance = amt, + .expires_at_sec = exp }); + } else { + uint64_t rid = it->id; + unmapped.modify(same_payer, cap::unmapped_key{rid}, [&](auto& r) { + r.balance += amt; + r.expires_at_sec = exp; + }); + } +} + +/// Dedupe at ingest. Returns true if `(outpost_id, 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 outpost_id, ChainKind chain, + const std::vector& addr, uint64_t ext_ref) { + cap::rwdcursors_t cur(self); + auto idx = cur.template get_index<"byoutaddr"_n>(); + auto it = scan_find(idx, cap::outpost_addr_key(outpost_id, chain, addr), + [](const auto& r) { return r.by_outpost_addr(); }, + [&](const auto& r) { + return r.outpost_id == outpost_id + && r.chain == chain + && r.native_pubkey == addr; + }); + if (it == idx.end()) { + uint64_t id = next_id(self, [](cap::cap_counters& c) -> uint64_t& { + return c.next_cursor_id; + }); + cur.emplace(self, cap::rwdcur_key{id}, + cap::reward_cursor{ .id = id, + .outpost_id = outpost_id, + .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, cap::rwdcur_key{rid}, [&](auto& r) { + r.last_external_epoch_ref = ext_ref; + }); + return true; } } // anonymous namespace @@ -31,6 +151,18 @@ void cap::setconfig() { } } +// --------------------------------------------------------------------------- +// setwindow +// --------------------------------------------------------------------------- +void cap::setwindow(uint32_t window_sec) { + require_auth(get_self()); + check(window_sec > 0, "window_sec must be positive"); + capcfg_t cfg(get_self()); + cap_config c = cfg.get_or_default(cap_config{}); + c.claim_window_sec = window_sec; + cfg.set(c, get_self()); +} + // --------------------------------------------------------------------------- // claim // --------------------------------------------------------------------------- @@ -53,36 +185,183 @@ void cap::claim(name wire_account) { } // --------------------------------------------------------------------------- -// linkswept +// linkswept — AuthX link completed: sweep unmapped -> pending, and stamp the +// now-known Wire account onto any still-staged native rewards. // --------------------------------------------------------------------------- void cap::linkswept(name wire_account, ChainKind chain, std::vector native_pubkey) { require_auth(AUTHEX_ACCOUNT); + const uint32_t window = config_window(get_self()); + + // 1. Sweep an unmapped balance into the staker's pending_claims row. unmapped_t unmapped(get_self()); - auto idx = unmapped.template get_index<"bychainad"_n>(); - auto it = idx.find(make_chain_addr_key(chain, native_pubkey)); - if (it == idx.end()) return; + auto uidx = unmapped.template get_index<"bychainad"_n>(); + auto uit = scan_find(uidx, chain_addr_key(chain, native_pubkey), + [](const auto& r) { return r.by_chain_addr(); }, + [&](const auto& r) { + return r.chain_kind == chain && r.native_pubkey == native_pubkey; + }); + if (uit != uidx.end()) { + const asset bal = uit->balance; + const uint64_t row_id = uit->id; + unmapped.erase(unmapped_key{row_id}); + // wire_account is set -> routed to pending_claims, expiry refreshed. + credit_wire(get_self(), wire_account, chain, native_pubkey, bal, window); + } - const asset credit_balance = it->balance; - const uint64_t row_id = it->id; - unmapped.erase(unmapped_key{row_id}); + // 2. Stamp the Wire account onto every still-unconverted native_stage row + // for this (chain, address) so a later retryconvert routes the proceeds + // to pending_claims instead of re-parking them as unmapped. + nativestage_t stg(get_self()); + auto sidx = stg.template get_index<"bychainad"_n>(); + const uint128_t skey = chain_addr_key(chain, native_pubkey); + for (auto it = sidx.lower_bound(skey); + it != sidx.end() && it->by_chain_addr() == skey; ) { + const bool hit = it->chain == chain + && it->native_pubkey == native_pubkey + && it->wire_account.value == 0; + const uint64_t rid = it->id; + ++it; // advance before the modify + if (hit) { + stg.modify(same_payer, stage_key{rid}, [&](auto& r) { + r.wire_account = wire_account; + }); + } + } +} - pclaims_t pclaims(get_self()); - auto pit = pclaims.find(pclaim_key{wire_account.value}); - if (pit == pclaims.end()) { - pending_claim row; - row.wire_account = wire_account; - row.balance = credit_balance; - pclaims.emplace(get_self(), pclaim_key{wire_account.value}, row); - } else { - pclaims.modify(same_payer, pclaim_key{wire_account.value}, [&](auto& row) { - row.balance += credit_balance; +// --------------------------------------------------------------------------- +// onreward — per-staker WIRE-side credit of a STAKING_REWARD +// --------------------------------------------------------------------------- +void cap::onreward(uint64_t outpost_id, + std::string staker_wire_account, + opp::types::ChainKind reward_chain, + std::vector staker_native_addr, + opp::types::TokenKind reward_kind, + 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; cap trusts but + // must not break the message chain on a malformed row). + if (reward_amount == 0 || staker_native_addr.empty()) return; + + // Dedupe at ingest so replays are rejected even while a prior reward for + // this staker is still staged awaiting a quote. + if (!cursor_admit(get_self(), outpost_id, reward_chain, + staker_native_addr, external_epoch_ref)) { + return; + } + + name wacct; // value 0 == not yet AuthX-linked + if (!staker_wire_account.empty()) { + wacct = name(staker_wire_account); + } + + const uint32_t window = config_window(get_self()); + + // Price native -> WIRE off sysio.reserv's published reserve. No sync + // inter-contract call exists, so the read-only swapquote action is not + // usable here; the shared reserve::quote helper runs the identical math. + reserve::reserves_t reserves(RESERV_ACCOUNT); + const uint64_t wire_raw = reserve::quote(reserves, reward_kind, reward_amount, + reward_chain, + opp::types::TokenKind::TOKEN_KIND_WIRE); + + if (wire_raw == 0) { + // No quote yet — stage in native units; retryconvert promotes it once + // the reserve is provisioned. Nothing is dropped. + uint64_t sid = next_id(get_self(), [](cap_counters& c) -> uint64_t& { + return c.next_stage_id; }); + nativestage_t stg(get_self()); + stg.emplace(get_self(), stage_key{sid}, + native_stage{ .id = sid, + .outpost_id = outpost_id, + .chain = reward_chain, + .native_pubkey = staker_native_addr, + .wire_account = wacct, + .native_kind = reward_kind, + .native_amount = reward_amount, + .reward_epoch_index = reward_epoch_index, + .external_epoch_ref = external_epoch_ref, + .expires_at_sec = now_sec() + window }); + return; + } + + credit_wire(get_self(), wacct, reward_chain, staker_native_addr, + asset{ static_cast(wire_raw), WIRE_SYM }, window); +} + +// --------------------------------------------------------------------------- +// retryconvert — re-price staged native rewards now that a quote may exist +// --------------------------------------------------------------------------- +void cap::retryconvert(uint32_t max_rows) { + nativestage_t stg(get_self()); + reserve::reserves_t reserves(RESERV_ACCOUNT); + const uint32_t window = config_window(get_self()); + + uint32_t processed = 0; + for (auto it = stg.begin(); it != stg.end() && processed < max_rows; ) { + const native_stage row = *it; // copy out before any erase + ++it; // advance before erase + ++processed; + + const uint64_t wire_raw = reserve::quote(reserves, row.native_kind, + row.native_amount, row.chain, + opp::types::TokenKind::TOKEN_KIND_WIRE); + if (wire_raw == 0) continue; // still no quote; leave for a later call + + credit_wire(get_self(), row.wire_account, row.chain, row.native_pubkey, + asset{ static_cast(wire_raw), WIRE_SYM }, window); + stg.erase(stage_key{row.id}); + } +} + +// --------------------------------------------------------------------------- +// flushexpired — prune expired rows; credited WIRE reverts to the capital +// fund (it simply stays in the sysio.cap balance once the row is erased). +// --------------------------------------------------------------------------- +void cap::flushexpired(uint32_t max_rows) { + const uint32_t cutoff = now_sec(); + uint32_t budget = max_rows; + + pclaims_t pclaims(get_self()); + for (auto it = pclaims.begin(); it != pclaims.end() && budget > 0; ) { + const pending_claim row = *it; + ++it; + if (row.expires_at_sec != 0 && cutoff >= row.expires_at_sec) { + pclaims.erase(pclaim_key{row.wire_account.value}); + --budget; + } + } + + unmapped_t unmapped(get_self()); + for (auto it = unmapped.begin(); it != unmapped.end() && budget > 0; ) { + const unmapped_token row = *it; + ++it; + if (row.expires_at_sec != 0 && cutoff >= row.expires_at_sec) { + unmapped.erase(unmapped_key{row.id}); + --budget; + } + } + + nativestage_t stg(get_self()); + for (auto it = stg.begin(); it != stg.end() && budget > 0; ) { + const native_stage row = *it; + ++it; + if (row.expires_at_sec != 0 && cutoff >= row.expires_at_sec) { + stg.erase(stage_key{row.id}); + --budget; + } } } // --------------------------------------------------------------------------- -// importseed +// importseed — bootstrap pre-launch holders into unmapped_tokens // --------------------------------------------------------------------------- void cap::importseed(ChainKind chain, std::vector credits) { require_auth(get_self()); @@ -93,44 +372,19 @@ void cap::importseed(ChainKind chain, std::vector credits) { if (credits.empty()) return; - capcounters_t cnt_tbl(get_self()); - cap_counters counters = cnt_tbl.get_or_default(cap_counters{}); - - unmapped_t unmapped(get_self()); - auto idx = unmapped.template get_index<"bychainad"_n>(); + const uint32_t window = current_cfg.claim_window_sec; for (const auto& credit : credits) { check(credit.wire_atomic >= 0, "negative wire_atomic"); check(!credit.native_address.empty(), "empty native_address"); if (credit.wire_atomic == 0) continue; - const uint128_t lookup_key = make_chain_addr_key(chain, credit.native_address); - const asset add_balance{credit.wire_atomic, WIRE_SYM}; - - bool merged = false; - for (auto it = idx.lower_bound(lookup_key); it != idx.end(); ++it) { - if (it->by_chain_addr() != lookup_key) break; - if (it->chain_kind == chain && it->native_pubkey == credit.native_address) { - const uint64_t row_id = it->id; - unmapped.modify(same_payer, unmapped_key{row_id}, [&](auto& row) { - row.balance += add_balance; - }); - merged = true; - break; - } - } - - if (!merged) { - unmapped_token row; - row.id = counters.next_unmapped_id++; - row.chain_kind = chain; - row.native_pubkey = credit.native_address; - row.balance = add_balance; - unmapped.emplace(get_self(), unmapped_key{row.id}, row); - } + // Pre-launch holders are unlinked by definition -> name{} routes the + // credit to unmapped_tokens, with the same upsert + expiry path as + // staking rewards (one implementation in credit_wire). + credit_wire(get_self(), name{}, chain, credit.native_address, + asset{ credit.wire_atomic, WIRE_SYM }, window); } - - cnt_tbl.set(counters, get_self()); } // --------------------------------------------------------------------------- diff --git a/contracts/sysio.cap/sysio.cap.abi b/contracts/sysio.cap/sysio.cap.abi index 635bad9a35..b4e29e7b36 100644 --- a/contracts/sysio.cap/sysio.cap.abi +++ b/contracts/sysio.cap/sysio.cap.abi @@ -10,6 +10,10 @@ { "name": "imported_complete", "type": "bool" + }, + { + "name": "claim_window_sec", + "type": "uint32" } ] }, @@ -20,6 +24,14 @@ { "name": "next_unmapped_id", "type": "uint64" + }, + { + "name": "next_stage_id", + "type": "uint64" + }, + { + "name": "next_cursor_id", + "type": "uint64" } ] }, @@ -33,6 +45,16 @@ } ] }, + { + "name": "flushexpired", + "base": "", + "fields": [ + { + "name": "max_rows", + "type": "uint32" + } + ] + }, { "name": "import_credit", "base": "", @@ -84,6 +106,94 @@ } ] }, + { + "name": "native_stage", + "base": "", + "fields": [ + { + "name": "id", + "type": "uint64" + }, + { + "name": "outpost_id", + "type": "uint64" + }, + { + "name": "chain", + "type": "ChainKind" + }, + { + "name": "native_pubkey", + "type": "bytes" + }, + { + "name": "wire_account", + "type": "name" + }, + { + "name": "native_kind", + "type": "TokenKind" + }, + { + "name": "native_amount", + "type": "uint64" + }, + { + "name": "reward_epoch_index", + "type": "uint32" + }, + { + "name": "external_epoch_ref", + "type": "uint64" + }, + { + "name": "expires_at_sec", + "type": "uint32" + } + ] + }, + { + "name": "onreward", + "base": "", + "fields": [ + { + "name": "outpost_id", + "type": "uint64" + }, + { + "name": "staker_wire_account", + "type": "string" + }, + { + "name": "reward_chain", + "type": "ChainKind" + }, + { + "name": "staker_native_addr", + "type": "bytes" + }, + { + "name": "reward_kind", + "type": "TokenKind" + }, + { + "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": "", @@ -105,6 +215,56 @@ { "name": "balance", "type": "asset" + }, + { + "name": "expires_at_sec", + "type": "uint32" + } + ] + }, + { + "name": "retryconvert", + "base": "", + "fields": [ + { + "name": "max_rows", + "type": "uint32" + } + ] + }, + { + "name": "reward_cursor", + "base": "", + "fields": [ + { + "name": "id", + "type": "uint64" + }, + { + "name": "outpost_id", + "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" } ] }, @@ -113,6 +273,26 @@ "base": "", "fields": [] }, + { + "name": "setwindow", + "base": "", + "fields": [ + { + "name": "window_sec", + "type": "uint32" + } + ] + }, + { + "name": "stage_key", + "base": "", + "fields": [ + { + "name": "id", + "type": "uint64" + } + ] + }, { "name": "unmapped_key", "base": "", @@ -142,6 +322,10 @@ { "name": "balance", "type": "asset" + }, + { + "name": "expires_at_sec", + "type": "uint32" } ] } @@ -152,6 +336,11 @@ "type": "claim", "ricardian_contract": "" }, + { + "name": "flushexpired", + "type": "flushexpired", + "ricardian_contract": "" + }, { "name": "importdone", "type": "importdone", @@ -167,10 +356,25 @@ "type": "linkswept", "ricardian_contract": "" }, + { + "name": "onreward", + "type": "onreward", + "ricardian_contract": "" + }, + { + "name": "retryconvert", + "type": "retryconvert", + "ricardian_contract": "" + }, { "name": "setconfig", "type": "setconfig", "ricardian_contract": "" + }, + { + "name": "setwindow", + "type": "setwindow", + "ricardian_contract": "" } ], "tables": [ @@ -190,6 +394,21 @@ "key_types": ["name"], "table_id": 40418 }, + { + "name": "nativestage", + "type": "native_stage", + "index_type": "i64", + "key_names": ["id"], + "key_types": ["uint64"], + "table_id": 20200, + "secondary_indexes": [ + { + "name": "bychainad", + "key_type": "uint128", + "table_id": 29106 + } + ] + }, { "name": "pclaims", "type": "pending_claim", @@ -198,6 +417,21 @@ "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": "byoutaddr", + "key_type": "uint128", + "table_id": 35362 + } + ] + }, { "name": "unmapped", "type": "unmapped_token", @@ -243,6 +477,44 @@ "value": 4 } ] + }, + { + "name": "TokenKind", + "type": "int32", + "values": [ + { + "name": "TOKEN_KIND_WIRE", + "value": 0 + }, + { + "name": "TOKEN_KIND_ETH", + "value": 256 + }, + { + "name": "TOKEN_KIND_ERC20", + "value": 257 + }, + { + "name": "TOKEN_KIND_ERC721", + "value": 258 + }, + { + "name": "TOKEN_KIND_ERC1155", + "value": 259 + }, + { + "name": "TOKEN_KIND_LIQETH", + "value": 496 + }, + { + "name": "TOKEN_KIND_SOL", + "value": 512 + }, + { + "name": "TOKEN_KIND_LIQSOL", + "value": 752 + } + ] } ] } \ No newline at end of file diff --git a/contracts/sysio.cap/sysio.cap.wasm b/contracts/sysio.cap/sysio.cap.wasm index 30aff6ba56f0352b74ecc26c4b5f3408d949d6dc..b0e45d16726d99fdf54d8113651a508cf8b618d1 100755 GIT binary patch literal 38787 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 literal 18906 zcmc(ne~?{gdEejjo^yZ5XJsARi?fylK6_DoA$304W*}v1XEHJm6`Tgh@;Wf+n#OC!wvUGwpczOP%ru zGimC6zR!Eky?3>e0|V2@-h0k@&-=d5^FF_y=Y8*W?X|_oIT!tD^qND?9g0r5LoPaX zC|N(Xz8EdDR!#YG!0W&K>fnH1L!@BXISfO7_N`r4u8oony!6 zRy#B88WbNeI-11C&ZLLF7O-+r(&f?kKzp{m zdeSuo=lb&9Fd%!O1!|_H&LcgyLtV4>k>1?$(#&kT*PdCM`(VdiW6i6b_Uz2iJD1u5 zhF1-#W;<>~?IW7EH*wc2j!eO`~I@ysD+UYdc2A zZb%zrjXitPJ!zWOYJBdfrL{eKc~C=|HeO9<{TpSDn{+?csNGoGaZ_z?Qj1oiD5}Mg zi|RMV>rt_>>C$yqJpDDhY;tM5K9N8yub%Yonwh{EL?VvTnVE+=?Uk9skSO-G_R7k_ zN%x89n(zE_)Jt4;bGjPE>wYSCJ6+*+r3If8{@j7w71@ba)NT1moxdROI>!23t@4h|bk)MhIk-fU*c1DF8H{{PBX!(0~ zM!CPvxe=G9xl8NJljY&^ns8~Q--uXw?lg(kU&P1m+39q5n}ZlR|IP1WFe_jVSU#T&^QezStcSb2xd9*w3xqJxX2=OA}2CC7=v-DP6tgq#9@$%pQ-NW5u#m0v} z^4KFnDz(0%XzsyaJT^izQ_MT21=I9i76S!h7QpIR2V=C7R$^GUY6rnPu*KFiE^e&@ zcc&ZiOp@fa`SDoXsktn1<>xixs@or5H~@f(W6?g^Zi!vy1e%-uaTBmn(d=fggK}`< zF4wSUJ{sN7`6sphth>*p$75OAq1mQ4^zZk7>-n#`abGmXV_R>to5$VU-|fJ3x_Mg5 z6~cTVP7*DFZux2VuAMH6#(i4+_MiA%%1=@B9N!Umaqk}G#(i2ZL?w?(u%=|}tRS^sW?JBiR; z?kNo)VS&X{zMI{;yF}f97tK7LV74O4;@vEhhbMij22=A|_FBQe;6=tM7$Pc6_@+LR zmY*-&TTRn`_p4}{H9{66NtEr_ox?WznT{rC#W(9dUXM?qh8L3gmWQhmEcMgy^IXEH zz}mCAjSvB7TbvU|inED6!6JJ-*B5+u`eVQI+n;>Cxj&XrUF4Z5bo=b!mPJ3!3`X*m zFx&KBee&yHNcP8LeoFt*f2^DRlr7*Bf2aijDX!+ajo}jf+uVg1jtCjhnDn5PFPu_;Hine$P%ng7OVdHZ<5KL^-ifzhU-Ch)npiaf|s}9z*fa@_Qc9b8oS> zhP4S5!}cS8WQL8U$6aJ{yk?7e)*f2*Y4;1(>kGmB`QqtlK*UX_dvMNvx)o<{uz}}% zSH>U_{G<>6nW&Wn_(^8)00UTT#G(8$c#_e@DsE z><%7~lb;c(Cj1>O46?@G$#@NLS<9F+;U~&hYQMGLew$+%k*unQzu-gn7_BT@p*XVDCTJ1geNIGziVe)nO7nNL`b%k(J9szSBJ4H z>d;s??!MQ|7%s@m$9af|AtTD*qlNB+q+DSU$zM%#*_D}8!GkrS> zfyDZx#}cYr5FVN5rqR5Z=;A&mq|Xvik(=jy@I0pHvHr8zn(dFXAqEIE(@E5#;=4gt z<(uhpJARDDpupUi!7_|}_C~G2Wp|E8Ou#LMJxH6Ev@w@(hR73s1%JY&M~8B36P~*C zuOpuXNzBd9F%M?0X;$dhgRR1n8op!DCX;GSpnBz6Wg2M7VOrI~<5;&Ef*rm39Sn39~P(&$%3%MzAo!-#4YBWVS}iQW|gq7M);-gocjYDy4;4@&k#2-*;E zlXX137{>ymY#(G;xnIIyLg|b2UTE!$U8@+osGH@u4W0wXVF-IPekW930sSZ??(3s5j*8ZVU4RzA$&vubr= zl1VPGSE;}yedYx8M zw&_j`eO1uvV69C!tm^}T8dr1&@&ef;D}gzciwJFo{E#QOaW}n!nZBP1gO~wWEGE^N z^kiWm;)}033B0vWnWNF%Qx^LeeiA2>WGKTZ*7C5)K{*@~VwRK@O$~yd3S2}Om0Rku zGa_Ngd5*&J%gzitko)`b42qdR!)N2I7zD)33No!gPLK4(3qPRJEcR99 zZ<<~X4R?IDG2cpv_Ah?%OMm%s#Uezx_+N{8FbQVVHWfxD#ip#nGrGrXW>ePI3^x7+ z>tUTQa$8&=ic5-Th^i8Wyjj8ZbUYjiC&dQO=01|*d%k~rX>j{ed5c%W8_K!}Oy%zd zX99FceUd6zekp_(z@;CmS9R zvGT@NhCeCSrMv8QVKCt@ZVifQ`7S}RWQ)*c(tl4hv*?RA&kcpksYPVLGUtYejVaC< z315mTCn^rSSU#PN5L`6)DfuhfU%79X)$E83khR8}Jd5EmX2Dlc1_S=BO_xmv_CFcX zE3p5JZmZ7tsH8Y!xQVf^9}xedLXW`iA>vyECk%2?qGFW66~t!OvhwZ2&dO|jALP&li@hLMi@Qo};?>DN`UR?fg zu7*&Dq^jS`82Q}L(4=?)vLeQsx{~7Giv|;^NP;a<6mksH;$>Q>U*YJ20VQZ_VK?M= zJ-%_8D44&xxDv|>kk%WMF)d2BN%Q%;caIYDSYEG|N9V`uvYe!2vg2f98e<;&zeQI| zE-d&eQwRBBSZaQSOd<*S28hvkdCI|Jp8C5jxxt9MIt2uo{B3#1L}|VhwFR3t$VG5O zvLy=JE=x%;K$>nN3S&eI^H2BzGLSb9WgrKGOSs(#8Yo3ioXA0qu!ig{eoEQVXba~f zl~D*T0FgqQ80fHOahOguf`pm`4-Z>YZq-l1j3T7wp>|Y9h0X2OL$_K;!GKaAjw)IT zhcE-uaWA)LEN^;mz?v`{2P5SJ;a`$p#2Sp*=s1i<8_GPww6CvuF6jM0-ykomSO{?q z0Re#tMQVr|0q|$8XzF2Q&-^S&PZ;fsVAd@O?9IbAy%QFiYfq6RKQpKd@ zybmqJ5}|e*TrF>&;7wKE90ZI8+6eqqFn)2qD|?5Dxjr_d$61A0Pdz>xJ^q;4^b+Tx zfohrXZ^@&wT*Qio-s;^0Eo$}?{%zFJjjzVxRx%$3_8j zhD9q-7|BzUF5TUr;0ys^V}f{1>=op^N;FR^PC(!Xjo>luszw;54(Jh|Ae22&y|Jfv z7MMEgZS%4%0Sp+&m0}GnrOLrvnQ&dGW40{5V$^f>p*JrKzd08TOk9{2 z#+W^lgg#(%2=1)}Ekt@uouwn>KT_iCUAu`iq@;BCekKY%%c*2miS-6LH>_JyiziF< zFddVcd|!G+q@n=|?ld5;O-GN)=3SCyyC!eSvT>=bO7`<&(rG_V@UbJmq2%xm@&1MZ z@6ErzD$M$%cqh5yIdI-=n^J>!hXwxI(rp+YScUkI*9D=8;cQ`ECBSBl6^K?Q{1S84 zvtLjH=>yTpfA+SO=2iPTi*F-|H(OI~;l&h?5@|~D@0AB2OsXEmsuwGTpnAlIAc2(@ z&_bv}9LCo0lx>B9p$1!wj3wUi*;r{E7(-s|DOKbwKsHt$RCP2xgeqFNlUf4Y7oWzJ zJr-Ft?Kbym&t@P+JWH0caoel89ErSbB^WKAy+>BbD>juK z+Om~X>>IRs6`)zQ3qhj7kijmG%t3J`mh7Kk&$LSYV4R;~cHoVWI;*%U7>WqU{QB9S zm(j3n4^ogMK;#7G@E5Yb=nmf`zlYc)fl4)o^S{8W^aHkE!9+q(_PrV4l55&>Te0ki zbRwS54y!2%?Wku|M7CL_HhL@Jq^tEnVG_mXe;ZXIpsnCK!ckXtmEdaYf_6Sp*?w%i z!Q*+9T5p3D!nK5^hTS2uwQ3M{lR-@z1aa#olVyHd7r7Z?^s5sW;4^Zr=h*3nd^ww+ zQ6Cmv6Si*Dq8?<*T+b)tpdMKgE0GUaFo|ZaWu%t%a%1kZ5j?X>U{JP-QN}j}zSt`t z9)HR9|4WZvn||572Xj@MZkz@lyGvpWF$=ca=)YmrP?(T}>bc&Af+uBg9w;OblMAuR zX3(M%KbO*GMA_q5rINvSkd820KW~!!MHUay8fDjtyV-klj_3$J>ABdp;iEdEjEHcS zwTQ1xNXf;uYV$IaQQ&nAsbdQ#pmg7!R)hZIk|J&-JCq*?R_nXXZRW|_X<`Fd(4=M7vQ{Aj5+t_V8IdlnzYS(WF?L*3l4OGTzCDFD z2O2c$8lxMB15`rg%b})LWdDgDCf!%s_Wh;%@9U>ibAux!q4G1mF|gZ%6C}-l-~b5% zC_`MxRwEF>R&!B>OKAgU=K07J-je)K4boO(*u)_3w4KJN2>Wf))Zwj2!b+ZV*`iQ> znIJfo4MK!4d2rO)5g1gZ>?G^OE5&MI%&nT^RytQ5%0ZP0x>N7(tO|%K2Jn3Q;F&y% z20pcihaem3>C-h|530C8Gpqcb@YqHG({{6H zQCQTd!lDKXRO~HPwV|MH)7)N$MqBE3Mh|&B3zBpiiczq~XWBb5*)Ts9_$=AMNFy8& zTH;~o1nOc)t$=57A}gWvHtF}#OUx#%2ogoFEz)O_LHJQsXDZUi0_u5j40W`2KAE()a_kO5t8PRv5ea0kvwKcN#B%Il_NES8YmE5n@XL-OgD8n2rg$agEdYy@* zPNV{l*`+wUg+)|Tm$j|eXb!{e&!Q+1Zkz5$huqydlhQ{|NZkoGT<#!M0jUXqbLhzO z$@j}HqdC%5TMfCHzk_;XScX=^2o>v5Hqy3jopG4oO=w*BhYyti{kR`K#jMsw&$>qR0lBWMvw6LH6A}u)*Q&U_x2Ow=Ehk_~&CcB>){!LKR$_RBCCc;Vu7xjJ>fiq5O zH}2hyy|C&+wfgcHoe9&6SEYV@HR>mW(8FrA1T$=#o2Hb}q?ex>fkQeLmmRV^lY}5^ zgEUN|fG_tJ&&z(6K{+vY#7Py(hC}Su<}GX37N0Y(rE4zjp#?VEI55X?<4UuEVot3Y zj&S@tRrSkG=;E!8M8t5zbs^laxOmweQ;-!MYb8ClgNbqpU960SU!$6Ar>+RP%t53- zOCSP_$mY}s-ioMD@u3VC>Bf@9ecZAnfyz~|P15wIf|L`c*rrlOxTYZ-0t6B3DSWB{ z&Ghd}Ex-6966$Yk7iy$SLM{11aQQ?Z`{H|d5MT7ae0fJVo0a&=4-Sj(fgrbtuM7a1 zui7#87s*8htPGFo$T~>wTM@^jnuF(~%0#aumKRMd+YlO+{s*M$LFPb^D4Li&wG_L@ z2pZmM#<0j95;)2^bWo86A*kf%Lq9+gS&S`aa{&?zH3_y1le_Tzd=*pbxY&d7b)U zt@53fz<=@iFFYR(<5WTqmPkV`+DP(^L|T>3n>5pZWZ(NQpx9M?@9#$-oJq2yTxB}B z79M;!!1d`b|HrTCd^67*8)tv#*MIGg{^D2H-$`<38<>0^Z6|4ZYZ zUVQz}Fa4iCfBw5VFltk6Y<%Z)e^IrBq0FYKbxKrY{}lBs>2u$)LV|0OG5I@R_O8><8$f2sr^ zIjRI8p_o~h0ESUxMo6MPpoqv77`PAuumt+VA+>JgwYFwksg@D|q~m$udgt6*@4reFO(g42OuY zCOEC~o@#qmCsALqRnmfQ=Z$35{CSu|sjOyfnuzJOVce|RN7R78YiR*A6Qr}TifEia z)cpObqpFK5`aYN$)#Nxuv;a6la>zLJouyDqBhwAFw0fweDPiWcliX(R-%PfQ^rs(< zszZCF8`PRvMD#PPg0=&h2~Kn5c`K4be=U=&ydG$sQ^d?gh)q1op#BX?gz8!4<0{^bfAs(QciHpeRsf{N~8} z#g@Gl6k)1$?fn6#4K(1?ydAw+vZ8Lucm#pWOTt22N?84!DvN6B3k4>JA*w*+#^Y&Z z30oNv#fX1ed*^;N%fYnXUM@(1&HmV=)j9jf1FQHp>OuS}`%j z_Ouak_o9XhikM4?ZS@^=Ja9c2PG5<7FAm?vGrbPq74$`Pf;pmNd%h$|lO6HUiWYr+ zF*b+DDx_1`ub|Ef>A_9LAxwh5IDuPZk`RcQg_4)TGNmnuW7G?$+~uFM4+&Q%L49<0 zk)$1hsX;hQ|Lk9j2xYZuD;*-}u+3+P@kWU&WYGQ9e?|s&0LJ99TmW|U1Vm&3m)rQk zBKv(>oNBSNg}lRfLMhIv*?N+uQTSC=0yD?{=y3IP0tr+kW;Pra1Une&I%ZU@7)c?Gjw)B!cOLB! zpZf5Buz<`W$EK!%#Z>=XIirL;yu7?dT7w;z<9`L*DO{;QYG~i9Vzoi24Szes$Knv%cPMnVFTZjGPls3&1*AH12PN1Nw8vUDY5R1@Lit`_aLsPw z;j9_+PY$tHJCJ?$vhf((j^it{xBB{=_9kJkjWW-68GA+h?d+8(!I9wRpy62Mz^=%S z?XWIq$HN152;g=}Fw#jl2J%t~lPDg(Px!`dYL8dhS~4mtgj?eoMRhKH4GN9TEHjgj zMnRvB1%3L^6~pfMYBuFcHL#gsc?cD8&0L-W-xZEM#g54FBL*kc{R80_4J z9wrfc=ouv9J)tBZm)?rZ2erJC{E)fFQ&JQhQ)zUq=AsZ%N4I5i4&s^0Bwu^CO7d0x zNxSkJ>NO=&8M+-+?)qcOIEr>#e!D#T9-4r{Bs?&$Ji#$(UKU z)OoPo<9BrOhwXQEddrJ*M_gxdrN@N!Vx~PiyV_Y>bNZbbei5hBUd-FQp1OnuJiIz* z*st~S-b0-n!Obo4 z>oRk*`P#|F!^;amUhG^Ip4J7ihQJ-}=oe=AeHbBkSFYvvmY4H|_UeNjSH9G!&T7sN z_jFc|^6NT|Yv-u2%|U;)&2P)NBkd&-(SFEg>zmNIk7ehG4RR$NjE%Fiphgu m&K*4p09G896rP!T?=J1Ka1lyjf4Xxo&~AmLQ0P)vmAzCVY73^RLTW0IYP9PbDK%*m z&9+Sp@rN4hnA9|BqD{0fMca%T6RTp9K<9!*i2=^~cSk}SzJXNBzY6ZmJv)Q1i~;ndQzKThZ4`0q>+ zuDv+<24`HGwDqyqEygL=$J9PpX-+aVIc6~*C!DouMexzvG~)YV967d+9F+wu5o40mmUHSH_?+sD~HVIDCOd6S&Z6pxvUxUXDW6VS)4}MkfjN$ zw^sa9(X^gx-F}L##M!f4Gc6#1Gn0!udi`$`FH!nW`X! zPgeyK3bzqu(_Wh)Xv3NDrZ|f;AjZrEr}Dwe1wpw~wG~(pHGzGQ?0BGhue(1+W;BN$ zRN10z4zM9NMvN?YR^5Mi@@{?ATkJZ$F+x_IbB1Zj<$z7wZA_#i!Mcle%9G+cV%6P) ztRVJTK}__8LVz)??3c|rT@Bno>)+FCI!rYUGBGXD5e|lP-H+s|EtA!1ax{Db zW^*z!O>4xB$O)L)(|ixD6&H8}lrh6~h{~U=O*qYe?fpRdaLqe_VX3AX0&8oJ(4Z;+ zkSCZc7{*DQy$2ThJyW-ihE=~LFDT|>`afo*|hBiPSkOrm%x}%^%oKkLE zF%@U(y2SIFW&uJX8thP2J($!$5}AWU8JHA!MuH&~s399fHROU~B8n_dMjL>?+fl_I z)};mTFH?nWa5G|i6|FqA`9oB3dp#b0s(u7;T&oA&%S6M+ph+=YStnec zQY{@&`lFVYU{iVPV590BibmaDG~}-VTTzUx>Jhe>!2JiU55ki*-})8}iv!y-Y8{c@ z_$=M0PI&YDdXYXo$1NOx!S-TX8u!LontR;};D4pSeFm~c@==w1>As$2%AAcl(Wz?b&+< z0Fo{gBU@5kF&#>dC*Ory8=Ql{;e^u%cEQ=N);%1MDMGTksc^3f!<$rv7fGJ)y2Rl0 zf$kAd!tL&B`1+jfi5J=G={31r=((xEJTB~n2T#oJ{2dZ!Dk{Y=-&6Nc29yOIhEbvhSYiZ1mMG~E9NCOn zlqEixnF;QDSYnn8HE|DnSnyjknJnR9%q^lU!9=oT{)sd4;KSnfXolz9+fsZv_ulXK z-ShjM-#Op+^e%jU7v_JG=aj(W&sz{UK6$EQWMu5*8-`4h4>YD1%mv960OvoHc6MXI_qJG1;qy*CK!} z4!KsL!EmxJoyFOdA@w{X=}eb0YzC89>$!P(BF-cWle7qLc89>=Llnhn_W(eI->Iaf zO*1;Qlelt5qm@e(#gcX$Ak2;3DTs)H-DyB1)bHVss@w5xIbVOFy3=8}z^OAW%wMd& z@IO4Yr(iRjx~xUSuB(Zb&rkV>-`#k)|6=@iHzi{x$CwGon;tSVwIrA*{Zz+ z-m{lM-slUUzCPc6^nzCfYxu?59wdKI>#H%dswwH7oP%jIZ=7w&n!(+EPqG9mWz1!9 z*;A5KUffVMLk##02u8?bg+wZr$Ym6%kVpk{&ISBPdo_S+_#HF;8K^fU6`0}VztwtL zEy_4MQ%W!kA68-1i2%`O))>T(I= zYjLvfZ!=aTl4xCH+Q;*WSA0cQ*fHC;fYk7B6aJu?6;+Lv^gcxAN_xfP#Ai@fBA#WI zDqHRYwuAYr`YE&^lEk4G7n5J&e2R_+FFghu+lzgdsU*2vl1wGhEM412&V9cx5ZE@S zxQ)!&-X^E(&58=R`BIw9|2n+^VKKKqLp{5FAd3>9=>%?it7!z`wI;8}Dy=+@C_+QZ zf13{BRk4*l%@+?o3w3AG9a7pka{8s-6%HxZSblYDD9AdU^(P$4R+2Y;c4JszU8;o(p5 eeQCHEBI3#LkR22BjUGl>^P{&RAcn@S$o~P*n^6q_ diff --git a/contracts/sysio.epoch/sysio.epoch.wasm b/contracts/sysio.epoch/sysio.epoch.wasm index a37d8a5a5004e9d28674044141dd130376234324..f0eb00907d77cc12062c13d361bcfc13af055e1f 100755 GIT binary patch delta 6724 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 delta 5956 zcmaJl3wRXO)pPI6?k1V+X7b!evdQd{kljEa0TPfOgb^tLM3k2iZNowq$tEN~7UU%f z4;2t-k^_V)*obMR@(K(}KtLi?KtP}h5(<7&svrG|ysh+y@9%%^%x)4E>o_o)^ z=brcMDG(N|6l(gL3{?y8;cENk8wDIzakbOqYhe(-*wCrAb!ApWQPI?yWxk43#-Wn&l<|H_gYHj(t$z=4CFR!~~RS)7@M? zeNcdeRFQ%*H=L2(t6!H$Vo&*tiVK5HnfDNGA)z&Uunn#VWdHuu7vWalXi=XM>6GyH>u3yjcfrW z0$&M|9ma?)2`!&6K6q545J1>01eCAA8$Fla{RI9H?IksED!QJ04KJDV5NyP!AX%I#iZfZ zoS5(EgTr8x3W*n{OT%%Z9*{;5vKgY|x{%FkpSTwYF0GEw>7K=IO+56b!EGFD=42up z^XXlKY?QpAl_j{Ma0R$#l%nBId=trp#)RJ>{pDeHgwDiah^gr9gjW-lHtglZOdLlh zWs)q&O^PRG7@TyQc-21EGkCNbWse|fn~&(~@z6$t%Ybt59@<27IgAq0IqXkpQXsyo zj^>$oIbrD#FLV%2lnn|$#7x9(Ev=QXbYIB-EakAfK zw%aswcC^s8n zQ*JWC6S=P;9NohuhdpIIbsLl_j0vU>5C#=@1Kj4skYKbL5#V6OIgZ@VMx z3L*V#o_ZP~UY_*HObpA{(O=EbzYqGT`dpvqnd*1+Ws~E5M@4IlAA{$HF#A1D^X$O6Dvut;N<2L}qgIy;1qrh0tnNNv9zRt<}S9YYInH=0>wPLa(m(=o9MHOCqYGJ&^MMW(eQM673W+N#^tsYT@ zPFOdxP+$G=FecHqRVW=fIW?5U*b2*H>!_Z_q<~UQF_qmIIVj7c1t{m`IXC|VH_FUO zj7a$@i9r@{`-{)8pkMNgKXl%a5l1w(1cLO4fVzK5_jx@A!vWS zyT?VDx&`cyv^{0W?;017QXy)t6D{kRJDQ|vTx;jLavt9O09B+s5CT#fJE*4ZAR&Cv z-{vmGo0raW;mvCoIpO_zvrvz&`MEtjEfJ6EV|LcaGXwl^MnPhjyc z*&VQHo)g}g&kEwL`Q1pCD!)=BXu>7I+J&;?Vd1VZ;jS^^;Pk>oyvuhBnJ^WLbYX(X zC+gKVpmy=&I6A#}&C_~VrRXgtDk`7Kc(9k5ilV6=CGV+LUEZs@yjMf=g1Tg!cEmA+ zU%t8~HQXI&2Vm5;9e@ESWvQ>fhEaF6{yvXavM%kZ8<_D98~FHw6VQ3thxBd%e7$US z$SO;h|J*7U*~Kr0t?~`#HO;ga+pN;Md}$|(%fy1>-7koy?DM+t=Nai~Y}u<>&aYjR z3LD?p)Vc9fEmOu)?!V~*rmb+GncrBkF{YhByBqXd*^GVe4=YPG56%C%sn{*A(SYQH z9jlnBj;vx1n*H0!sJrso`5@IWBaN)7LoG6=Na$cBTL&Y-e`>5qXsuEgB4xiJGgs2kqitRLoCvwogu&H4*)3A>rcRy&h1SDN(hi_hi?L(Laf zmI*9HthT-w?oI?Z)l)NNr9TiEH4~MGwR%h&#)E*n`tYLcU_vezs3T)ur8bhqt`Lq$Ul!QH6Lyie|m%q1@ z(YpRLnvk6^^ZjSDPafu^dV;s&l_uI=YB@=-zaAQNw^vxW^Zp==&fc44KGAJ}fnyS& zd~-`exS{KyQ8NtM(uG`viY->M30~jgZ0q{IB<&2%uxHDFaO!7y@@J{PER;J|Zw6e|iWO!S5h-rf-;Nzky%LDFHN>L6Aa zx^oZ7fEPZBRZXsMi1W{q_Bqm{HJ?1nF&h>(9z!YJ}j!_%zCXAW7ZI6*K zO?{LB7xpIO=wEx6Yx5Zhw|&(WMLid=t)iM;pFBSmJt2@`prF7m#IME+P4 z8FCNzU=>0@eXhTf2yY)YwoenF`we%V_w|ageVpX>#s2TveCQ?-=U}z|H}Hrr56h#un2qb zv%w4L7bj_`k$Mw|*aWx0QYV##d}_m9&8yzL*hxU@gv)pE1`&TN59_#%y?W_Np6)Dp zQ1C6bzUp(|o{7ND*n8be?;U|L*ZnxzdwmEK^<)N^ZzV#%8v_yd%8kwxZe zpVdZhBTxB WIRE off that reserve and credits the + // staker's claim ledger (pending_claims if AuthX-linked, else + // parked in unmapped_tokens by native address until they link). + // Both proto messages are flattened into primitive params per the + // no-proto-messages-in-actions rule. { 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; - // Split reward_amount (TokenAmount) into (kind, amount) on the - // inline action per the no-proto-messages-in-actions rule. const uint64_t reward_raw = static_cast(static_cast(sr.reward_amount.amount)); + // 1. Outpost-side reserve credit (aggregate, native-denominated). action( permission_level{self, "active"_n}, "sysio.reserv"_n, "onreward"_n, std::make_tuple(from_chain, sr.reward_amount.kind, reward_raw) ).send(); + // 2. Per-staker WIRE-side claim-ledger credit. staker_wire_account + // is passed as the raw string (empty => not yet AuthX-linked, + // so sysio.cap parks by native address); staker_native_address + // .address is a proto `bytes` field, directly compatible with + // the action's std::vector parameter. + action( + permission_level{self, "active"_n}, + "sysio.cap"_n, "onreward"_n, + std::make_tuple(sr.outpost_id, + sr.staker_wire_account.name, + from_chain, + sr.staker_native_address.address, + sr.reward_amount.kind, + reward_raw, + sr.reward_epoch_index, + sr.external_epoch_ref, + sr.share_bps) + ).send(); } break; diff --git a/contracts/sysio.msgch/sysio.msgch.wasm b/contracts/sysio.msgch/sysio.msgch.wasm index 3d0058efa26c20be6f7313e82dc8b1a20d2d5ff4..4b3335519a79076c7ff8c5bcf6ad392f95c23074 100755 GIT binary patch delta 25663 zcmdsf3w#vS_5Yrk-E7_&UU~A^U0zE9s4WIWREAbmL==3qROO-4N&rEN^r4by5mSp8 zoZ=0N8Wc4sU=VB~phiUrDiSSVK$IwmsbUQ+{^X}1zwfzsW_LC$B>YSJ`+fc&_+)nO z+_~r6bIv`ld*<%g<=^~;ziifmJ34#4#j|HGc<`6{8CeEDXY42{kWzw|ivnF{u=uO%_&67l)q$DCr@^!Ro^)A%W z`dGi%3VZLjR(Z3mP2NWa{83NpMIMt<+Wnorre-=o|Db=;d4JUI)yzL>^R)Z4`C6H_ zKzl$-Ui-d(pXT)#y{(6QZ%|KbLPC#>a7uzF;4ylsOa5vrp&Jzy5zQXv33|1m+R%V* z9`#(3=E1#|N-5ls<_S>HYu}jXUCj4pttX&QSy39$0{V)OckY~^7NCH4uIz_jx;eCH z#oVBl67U4HfqEh?-hsMj^(y}eox;}P^b#43Z{QBNbH{|3qZWPwCL!R>)qkyMS>x`-8TN5)M z!P`eOccp)c*06>)ppi03j83CGLo?CQ8_-N|QGkp_65~=SEY3?NKYgK_yP$MhP8D>CMNUch@3nHGOSg(JFbx(t!@|Rpx0R!e}}(IMs}nE z>!Oj}@O$#ek&)lqM}CQx*Dm=H2>8t<(?CE5Tcx|euF)Cn>f1&==SA0o%ZJgY)=igJ z;e~Ej455A2HCOb`z-yWrDC*(K08x#;*0>|RtrxFIN1wVYM$tP~)|C_5Z`Lp;rhJHa zN`x{0cxAOlYc2EV6N_J@5CA8lc`ZbSkw`R~Jw=6R0(p&fXf)PSZY&&6%56l&0gt)Y zQ)Fx;CK>=-rT}=%tsZc*P?NBjHF!42c{U{Wkrf=9*;x`3oeZZ_$7m<(^09;HZ`QoA zc@Toiv8e?E@)=Sr1 zg!lc|;{DaVuT3NRRNS0K-K4~2?4ev+63xUSV}(^TZol!ChFQ{J&jhTvfM)a&AHPSL z*6riFCE$tAD6p1{FH8DwvY%RmC-h19Z$ifcYx0DP={xI%31RIJiJNc7Ti;BWLtDg= zb<~gYMZtPHgZhZ+w^NSw&xu#XJSgx6bd2!ybKv}Ez zPkKNH35q9ABwRizT2BSsLpK|IMY6AMzN3C&5Z$azl2lD$`AGq0byJyuO;c}A^W!x$ z6il}6oZrzZ{yhYL^`hTT>4A&yv7tQn@QQx=nGjRmZ1xu!<8uRgKhGh58l`F}+*qYc zcqf2T_e3AcmE28X&Pp8gKuV#uHvzchTPaWSYU}yrcmTWmNodaktNE#}Xv%mx2Y`9x zu=Z!4>&0>(zi2-xbw8OWpIFyEorU*reY$(l>9yD0t4X~N?8{QEYmW3yt@T`*MxeEC zrV+LdJPqAw{h|_oMR+xxX&tIeZU4bBYtYJE_BefEy}0ZtAUxq2&=UeWGf#`IzjUU=m-vL0vtq@CW<>J&-gd1hu-DZ+uCGTDjj!elA#C76o(2>%cIZ zy+xs92b1LyWYMB6wP9A+Fu_-eL#cf&vD=%x<`7s!zuDw1GKUleQ}m(gCZO4KXZvG4 z0~{oRz=Zo0u{{9^bGbewJIZsg|!O5G}esE*|gNha;60m zFU8XFn-%_|U@ESPj=?nie9~7GOqUpW3bVE5gn&N`h(Il3 zYW#tWfHtL2gTVnk3t@$D_4mPbp#EEoc?f#XZ#E|s1yYUCX@C;enyuQZ;S-qVjA2L{ zp4Lg>5rJotk&w&da0ioM0xJ9g!0PL%@)>8RCgLY^ID(-)P+UC6w2tQt#?Xc57)A(q zCIs~qfYnWpaV9rZVs1=%>*c-K*F^A7nsRYkM}EBA7f282dHfSpODclyjm`p8btyZTX517D^k9NwEEAg< z+ng{^p8%kZ5-6kxj2)ZS0go_z&<98Xu)f@v%0Jy)3KD5_dz2dpxRK23YkH#YTvG8pIt`32Tkkdj~>2JJ~EyGOHL- z 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 diff --git a/contracts/sysio.msig/sysio.msig.wasm b/contracts/sysio.msig/sysio.msig.wasm index 00d06f8c96ea6ccf93aca72f0fb7d07e2e46f216..6fc9401028c13346fddbd359fb775965a5c58ca0 100755 GIT binary patch delta 1644 zcmZuxZ%kWN6u>3cFj^2c>1)8bKY$DZ0h8H%A*5R};!IqK zYgs}FF)rD5(O@(h`LL-GvgRez>0+F^xDU*!i_tip%nxAV7Pk+^IMI2|D|C(ep{MVj z^SkHwyZ3kATls;0a*ZC_S4uN9lcX7vlr!n{@k3E*nR&8rkI<_0VWMh8RW+?llN|~x zbGzNlUCun^s%T|3s;a1Em5QRc;ipt;Vw4$PRnZv05NJonm|GPuvtec73Oy_nty28S zF3<{bUJ7M=E+^qc^XC5it`w!|1gVu-oW;3h-X&e71lG(=U6?2(oYu-~3$uMEuFa1K zm-fw1bH=6lpgc8M%Q)rol#PQY$&9Dwr)rst6V93?1^(n_iMSj@W7Z$i^Ah!2lSRK# z>KEh1vwQtWB!}7KLDC8BWlmyj7f4*s8wGh@kRL6`?=Q$dQ;#&~@1F*Nq-qvLnonIoCkuj5CEmI9BdFP>ihC8VgDs3LFbG*Exh6 ztVw+v3SSJY{4f2_Hodr1rMIm!&*YhB|I0j`FTPMv6x;~-{4P|W7o5$o74#Z|pul>o zdKT=m68tB0tuELLgt*}%#T|@0%tdTJ*xu&vYYE0g^NVaSfTs%H2Q6;} zBNXm^V<-x#u22KS=R%txo(z3Z1SB#{n`rOcZe{D*DAlc-^^FwVsSc;8E^gLG#e3mp zX!H6ehRWvdp9M_Y1LFV{XClvvo=DO*#2B0koGc8C_7%?kSoT5b( zZhSRbgIJ>}4cJ$jM%J*(I@Vo)t$Oepx^w5DWx!s4SZ}sXD8>;f%1v@njD70hMfp)) zb*xp)2`~Vv1dF0EWP*V;w`Yf~;`g2QsD0Np0Dz`$1n=tJ3~}6kezChAH+Fvk@n{b={JLkA>f);>nn6KH z?@5T+-q+xxm+h|+2X=o5h=x9&H}{^H6Y_Ta)YvX*UTe4yg!{zlQJ*-{AEgymw*PZV zy<%eE60H*5gMH9vX_$+T2d_B2F8IJLi8qE?){=LIX2kT62jH&`ZO!LS!Fp~uw?0n2 z*3(bcN*ire$F)dHWUtEo7J^KfSJREVfB)0#nCZlZat#{KWIe*onzTaW+% delta 1414 zcmY*ZZ){Ul6o2>jb?aaI+OF;E+jG5ZJJ+#w?HInz;c#zbBO|1+4F(+x-IlnpjR*!* z#L3)O0ugeFE|E+>fuhF9GnlBs82m#5g3-hnL^A!b1+$nf@ehfJp7T1^`r(~--~Ij0 z@0@$ix$p9AvhWi*IpG$kjvk#7hR76|8X~6-r!0TcviHxAk~L30^{hRY8$P&q-=VQa z!DD%GQjolYBze7F(Q2a}m&-+6KI->Lyp~2hl1=giZ8n<=9$V1Mvs4X8HZKJl5=~1f zbxHgf9k5;gmQ0F*6yl5Y3liWLEOC$QED@L>1@kY(*e1SgA&N0q`UfEj|K2_WcE2MO zR-Ckri3zGkg-*A?L?&pIfuq7-FG3F$p@$JQZaD5ykhtc4oXFh%(7#l1^5M~tQR%T; zV5io10MIkO?WCM9`!v`|_!rG}h0$1+fK>QkXkIDD=#B&xq9R7OGEt*TU?KdVEU!0{ zvnJ@7k`-KkqhuU8kf`(kJh=*mYD}82pXqXgUl2M$*C&nOsWq;$wlW-%~0>EJ0X-|jm$vZjal{!mVY|hqvmtF zLePUUtA33XPR4(WRshW%Q!vX|3_Q2Q>IfwJd^`nPZ^i4u{t&MLORGKx_)^s|Lc&IO zb(%n}k0i!Pn0HsFcv;Onpnum;410gg3~*kpJwhzuN-IB=_VeoG2&leOmox4pHy5_M zQ`4}0HFXfYo=D3eu`3-Zz$Y5U0RF3C6yQB;5q@s%ZLDbjqw@f?YO+$OXlZ_~N;Ueq zm6bwg_iI?Rk9@0qjkz)$GkD8-B(j&9+U0WjdCAy^d`dFxSuM zRlt2s_h1{|ZpSwK*p6**bkqUR)Ugr0CpTUM+t|4Y>}%N#bC@L0~ZQ7t$OIxRiLC6~3H#1ZeWsZ@}K!icuQ2-Gg4& z_cTFTqdjNAx_f8gC*a-@;a7VvCC$oM5_DaZVk0b!YP6ZrmFuIwBzEJqH+v)njf;J7 zKAC^Hy^914_m1m?1o^H0uZf?Z-`NMM=`7u*mmrlH$M=0m{{seVJk9_B 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 90db355540..afe46235d0 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 @@ -541,19 +541,21 @@ DataStream& operator>>(DataStream& ds, NodeOwnerReg& t) { return ds >> t.owner_address >> t.token_id >> t.nft_address; } -// StakingReward — the single staker-reward feedback path. Routes to -// `sysio.reserv::onreward` (credits the outpost-side reserve only). The -// per-staker WIRE payout is a separate next-epoch action owned by the -// staking work stream (separate engineer). +// StakingReward — the single staker-reward feedback path. `sysio.msgch` +// routes it twice: the aggregate native amount to `sysio.reserv::onreward` +// (outpost-side reserve), and the per-staker body to `sysio.cap::onreward` +// (WIRE-side claim ledger). Field order mirrors the proto (1..7). template DataStream& operator<<(DataStream& ds, const StakingReward& t) { return ds << t.outpost_id << t.staker_wire_account << t.share_bps - << t.period_start_ms << t.period_end_ms << t.reward_amount; + << 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.outpost_id >> t.staker_wire_account >> t.share_bps - >> t.period_start_ms >> t.period_end_ms >> t.reward_amount; + >> t.reward_epoch_index >> t.external_epoch_ref + >> t.reward_amount >> t.staker_native_address; } // StakeResult diff --git a/contracts/sysio.opreg/sysio.opreg.wasm b/contracts/sysio.opreg/sysio.opreg.wasm index 80ffff0eee3383a72c5b07b71f5623cb3bdd02cd..341bacbda9e5f8f428adb060a624ccb153b32c74 100755 GIT binary patch delta 4662 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 PA>Pfr?RO4lKimHYI=3g7 delta 4479 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 diff --git a/contracts/sysio.reserv/include/sysio.reserv/sysio.reserv.hpp b/contracts/sysio.reserv/include/sysio.reserv/sysio.reserv.hpp index fe69b23f66..74e90d7fc8 100644 --- a/contracts/sysio.reserv/include/sysio.reserv/sysio.reserv.hpp +++ b/contracts/sysio.reserv/include/sysio.reserv/sysio.reserv.hpp @@ -8,6 +8,9 @@ #include #include +#include +#include + namespace sysio { /** @@ -244,6 +247,103 @@ namespace sysio { using reserves_t = sysio::kv::table<"reserves"_n, reserve_key, reserve_entry>; + // ----------------------------------------------------------------------- + // Shared pricing math + // + // `swapquote` (the read-only action) and cross-contract callers that + // cannot use it (sysio/sysio has no synchronous inter-contract call + // with a return value) both go through `quote()` so the constant-product + // pricing has exactly one implementation. `sysio.cap` reads this + // contract's published `reserves` table and calls `quote()` to price a + // native staking reward into WIRE. + // ----------------------------------------------------------------------- + + /// Constant-product single-hop output: + /// dst = (reserve_dst * src_amount) / (reserve_src + src_amount) + /// Computed in uint128 (max product 2^128, exactly representable). + /// Returns 0 if any input is 0; saturates at uint64 max for absurd + /// inputs (practically unreachable for sane reserves). + static uint64_t cp_output(uint64_t reserve_src, uint64_t reserve_dst, uint64_t src_amount) { + if (reserve_src == 0 || reserve_dst == 0 || src_amount == 0) return 0; + uint128_t numerator = static_cast(reserve_dst) * src_amount; + uint128_t denominator = static_cast(reserve_src) + src_amount; + uint128_t result = numerator / denominator; + if (result > static_cast(std::numeric_limits::max())) { + return std::numeric_limits::max(); + } + return static_cast(result); + } + + /// Unsigned magnitude of an on-wire `TokenAmount.amount`. Negative + /// values (invalid in reserve accounting) saturate at 0. + static uint64_t to_unsigned(int64_t amount) { + return amount < 0 ? 0 : static_cast(amount); + } + + /// Constant-product swap quote against `reserves`. Identical pricing to + /// the `swapquote` action but callable in-process by cross-contract + /// readers. `reserves` is caller-supplied so the table owner is explicit + /// (this contract passes its own handle; `sysio.cap` passes one scoped + /// to `sysio.reserv`). Returns 0 when any required reserve is missing — + /// callers treat 0 as "no quote available". + /// + /// @param reserves Reserve table handle (rows owned by sysio.reserv). + /// @param from_kind Source TokenKind. + /// @param from_amount Source amount. + /// @param to_chain Destination chain (also the src-reserve hint on a + /// full token -> WIRE -> token hop). + /// @param to_token Destination TokenKind. + /// @return Destination amount, or 0 if no quote is available. + static uint64_t quote(reserves_t& reserves, + opp::types::TokenKind from_kind, + uint64_t from_amount, + opp::types::ChainKind to_chain, + opp::types::TokenKind to_token) { + using TK = opp::types::TokenKind; + if (from_amount == 0) return 0; + + // Trivial: WIRE -> WIRE. + if (from_kind == TK::TOKEN_KIND_WIRE && to_token == TK::TOKEN_KIND_WIRE) { + return from_amount; + } + + // Half-hop: WIRE -> outpost token on the destination chain. + if (from_kind == TK::TOKEN_KIND_WIRE) { + auto pk = reserve_key{pack_chain_token(to_chain, to_token)}; + if (!reserves.contains(pk)) return 0; + auto r = reserves.get(pk); + return cp_output(to_unsigned(r.reserve_wire_amount.amount), + to_unsigned(r.reserve_outpost_amount.amount), + from_amount); + } + + // Half-hop: outpost token on the source chain -> WIRE. + if (to_token == TK::TOKEN_KIND_WIRE) { + auto pk = reserve_key{pack_chain_token(to_chain, from_kind)}; + if (!reserves.contains(pk)) return 0; + auto r = reserves.get(pk); + return cp_output(to_unsigned(r.reserve_outpost_amount.amount), + to_unsigned(r.reserve_wire_amount.amount), + from_amount); + } + + // Full hop: src token -> WIRE -> dst token. `to_chain` doubles as the + // src-reserve hint since every outpost token lives on exactly one + // outpost. + auto src_pk = reserve_key{pack_chain_token(to_chain, from_kind)}; + auto dst_pk = reserve_key{pack_chain_token(to_chain, to_token)}; + if (!reserves.contains(src_pk) || !reserves.contains(dst_pk)) return 0; + auto src_r = reserves.get(src_pk); + auto dst_r = reserves.get(dst_pk); + uint64_t wire_intermediate = cp_output(to_unsigned(src_r.reserve_outpost_amount.amount), + to_unsigned(src_r.reserve_wire_amount.amount), + from_amount); + if (wire_intermediate == 0) return 0; + return cp_output(to_unsigned(dst_r.reserve_wire_amount.amount), + to_unsigned(dst_r.reserve_outpost_amount.amount), + wire_intermediate); + } + private: using ChainKind = opp::types::ChainKind; using TokenKind = opp::types::TokenKind; diff --git a/contracts/sysio.reserv/src/sysio.reserv.cpp b/contracts/sysio.reserv/src/sysio.reserv.cpp index 38891c7f05..6cd622ef35 100644 --- a/contracts/sysio.reserv/src/sysio.reserv.cpp +++ b/contracts/sysio.reserv/src/sysio.reserv.cpp @@ -13,25 +13,6 @@ uint64_t current_time_ms() { return static_cast(current_time_point().sec_since_epoch()) * 1000; } -/// Constant-product output from a single hop: -/// dst_amount = (reserve_dst * src_amount) / (reserve_src + src_amount) -/// -/// Computed in uint128 to avoid overflow on uint64 reserves * uint64 amounts -/// (max product is 2^128, exactly representable). Returns 0 if either reserve -/// is empty or src_amount is zero. -uint64_t cp_output(uint64_t reserve_src, uint64_t reserve_dst, uint64_t src_amount) { - if (reserve_src == 0 || reserve_dst == 0 || src_amount == 0) return 0; - uint128_t numerator = static_cast(reserve_dst) * src_amount; - uint128_t denominator = static_cast(reserve_src) + src_amount; - uint128_t result = numerator / denominator; - // Saturate at uint64 max — practically unreachable for sane reserves but - // protects against absurd inputs. - if (result > static_cast(std::numeric_limits::max())) { - return std::numeric_limits::max(); - } - return static_cast(result); -} - /// Build a TokenAmount with `kind` and `amount`. amount is int64 on the /// wire; the upstream callers carry uint64 quantities so the cast is /// explicit. @@ -42,12 +23,6 @@ TokenAmount make_token_amount(TokenKind kind, uint64_t amount) { return ta; } -/// Pull the unsigned magnitude out of a TokenAmount. Negative on-wire -/// amounts (which are not valid in reserve accounting) saturate at 0. -uint64_t to_unsigned(int64_t amount) { - return amount < 0 ? 0 : static_cast(amount); -} - } // anonymous namespace // --------------------------------------------------------------------------- @@ -94,51 +69,12 @@ uint64_t reserve::swapquote(opp::types::TokenKind from_kind, uint64_t from_amount, opp::types::ChainKind to_chain, opp::types::TokenKind to_token) { - if (from_amount == 0) return 0; - + // Pricing lives in the shared header helper so `sysio.cap` (which cannot + // call this read-only action — sysio/sysio has no synchronous + // inter-contract call) prices native staking rewards with the exact same + // math against this contract's published `reserves` table. reserves_t reserves(get_self()); - - // Trivial case: src token already IS WIRE, dst token also WIRE. - if (from_kind == TokenKind::TOKEN_KIND_WIRE && - to_token == TokenKind::TOKEN_KIND_WIRE) { - return from_amount; - } - - // Half-hop: src is WIRE — quote WIRE -> outpost token on dst chain. - if (from_kind == TokenKind::TOKEN_KIND_WIRE) { - auto pk = reserve_key{pack_chain_token(to_chain, to_token)}; - if (!reserves.contains(pk)) return 0; - auto r = reserves.get(pk); - return cp_output(to_unsigned(r.reserve_wire_amount.amount), - to_unsigned(r.reserve_outpost_amount.amount), - from_amount); - } - - // Half-hop: dst is WIRE — quote outpost token on src chain -> WIRE. - if (to_token == TokenKind::TOKEN_KIND_WIRE) { - auto pk = reserve_key{pack_chain_token(to_chain, from_kind)}; - if (!reserves.contains(pk)) return 0; - auto r = reserves.get(pk); - return cp_output(to_unsigned(r.reserve_outpost_amount.amount), - to_unsigned(r.reserve_wire_amount.amount), - from_amount); - } - - // Full hop: src token -> WIRE -> dst token. Two reserves consulted. - // `to_chain` doubles as the hint for the src reserve lookup since every - // outpost-token uniquely lives on exactly one outpost. - auto src_pk = reserve_key{pack_chain_token(to_chain, from_kind)}; - auto dst_pk = reserve_key{pack_chain_token(to_chain, to_token)}; - if (!reserves.contains(src_pk) || !reserves.contains(dst_pk)) return 0; - auto src_r = reserves.get(src_pk); - auto dst_r = reserves.get(dst_pk); - uint64_t wire_intermediate = cp_output(to_unsigned(src_r.reserve_outpost_amount.amount), - to_unsigned(src_r.reserve_wire_amount.amount), - from_amount); - if (wire_intermediate == 0) return 0; - return cp_output(to_unsigned(dst_r.reserve_wire_amount.amount), - to_unsigned(dst_r.reserve_outpost_amount.amount), - wire_intermediate); + return quote(reserves, from_kind, from_amount, to_chain, to_token); } // --------------------------------------------------------------------------- diff --git a/contracts/sysio.reserv/sysio.reserv.wasm b/contracts/sysio.reserv/sysio.reserv.wasm index cf5e419fbb931aefd816bd13e62891114ec492c8..cbb5751351b9bd09dcdd26dd81ad0ce24f4ff662 100755 GIT binary patch delta 2896 zcmb7GeP~QnzT^~q`7uU0_}kZN^hs%_mqeZ;ZR_K#U5OBt+d*5ZivQc&%3nhva;}(7ogt|X~L|! zI*vgGbyrystg#Cdn=4m&Sg3o(a_Li}H5JC+C=IDQ8}O;s%H+7WxpLS>&g78YS3%!FAUTW}-GxT3iXX7psfOrIB4nF0d9h4W#oAsAh*xnZH7j1eUh z`j-Hk?O0JjVhio74UXDs?}`#ltY2}$YH`J}aKtQckO_oA#b{(nglbL*84w0|6Rr#! z3jTtNpW`tCqc$Sa79B%HtUwRduWsPB@~n1=Cgtz6Q}jhSp!d12s=KP(GF58cZ#P-$ z(Sw2HMfqL*YxJJ{K>u^n$O*H^)>xyKcv`+=Owhl|Zqv(}j%IrgUe;9pyl6!*)H~xL zSu@+|h7y7}7kwf4REC&&xx_BC}cxa1d25MD=wp$}yV7j-IE>RLE0=&g$2z@KV16X%D)eklaV$k4@4f{ zugq-JEu+`K{Ctyn=mz`Vy2-BmVUfM|0P>vIizhUHcWpk=VCug1z_o$=?lP;`{Molzs}#5)%Xzm z;VroaP-(GTa~L12;>>!hHF&@rmJ;klr~7LBk228ABP88GPz53Uz5 zyslPc0#gVj+v1o)E3&i`QiMAeW*#YNLvR4n2cmg>PoWotDMG^H1=ZjXDVON2N7bo5 zj8u?4!kFO^v`Jm0@YaMesN_7Q`d;@I^>Ik?JgAxoP}`7yPq))vnas4)K3U5A2qWYD z%q&gGiRLbL^(}d|d1QOKO?#?5semVd||Ma~{=DLv5-r*EKhHSZdFEC@tR0n9-F#}?Hi zRNq#ezDLzgo&AI~BCWF@g6)zwTRb`-|Jibb4$9h&(>)!CDAGfSM=vUnB1LA^vBQib zZX^IsLNFBwwr8skll0mvz4 zH=cKbM~$0~UVmNiMtgS{5?y3>bd&+(QI$=MdbQpUG}Ce=nv-ui&(hD!{qD}AyXy~5 zr;GAd-7yMmJ&p%zw`So*6zU%gT66op*uIYu(_Md-y5y^F$EPDBV|_6gb!Z94847D# zxY2r_6fjt}X%*y0?)7L)xkbSHYTjIH?xeGtN*>*^ig^nf} u?P!tz{#;59cARJ?-qpA9SM&V5`1*x8@#L4iW0S`Z9FebdJgcu22>A~%OHPmg delta 2461 zcma)7U2IfE6rP#8clYkz{h8hE{&csL*}Gfawp4yfcNeI(Q$#={lwXa2+X_`!5DK)7 zG0N76u_BnlF(5=E>JvfaK_!F)!(RyUgoz|Ph>;hK7!5`qcrd{;cehZ`2R7N6Ip54V zbMAM3=D^Z}-J=CEC+&HklmFQaErXlBsq2ji_0pbKN&33pH)W_4#LCc zR;ra*ryxEeNx+miX4&w3K_nltXvtzZlq%@$d6p(fNd`)?e~U$O?DjlVWRedEvOuZh znwIc9>|t{Yx8@;WACja4=i&KgjlBm+E#jk+r0lDA0M}xH8q}NYN7u3%!fvwGk$CK^iq&4l@|?G-q@<%+R77rX^mlK0%iksSS+5 z@)Df#U>p%nEjM;BS*BisdlrV>s5gwW6)(#N|7+zf-pYSEU-iUc=kA+uffwix zaEB{L62TdCCoBtAop9MsIOh0>a9|!y2wBtvouLE`!b%)X7z(w_Xi`j^CnX%)OT77> zgc&>Oo`h*UPRf*c3;Y;b?he^Jkx~Y(r=!pvcG15fS2qf#tP6c!ZZZVxRv)e6aNF0mjpTVz}Ut?>~3>MowTgHX#Yr%& zR?-{{)Q-|T=!u?Hie^s>!Ajhsi18qO6~b7S5zAwfB|JIi5S|LIwUD0bVdQ#U6~xzt zs!ptE$H!%d>c~+zlTa*NN%Ye*@KkaY4Z!EgcC(NWxQ#L6GvG!tgI^h_>!xkcQ}-hB zf39u|^8ed}dGN*r7p~r(u*Q}b)!K1Hy&B6d6r?T3;>cAL7T0&tneajVa{33{sNaea zT|IF!+S-ld{WT#&}H1$Kz%=rp({+NRD5nCL&{t<|})IzOi7u3_~%ee&wu z#j5JNDEFE0X}S)R2_$9{R-0RU+HMGUk%%z{?9FtH9c;A*X%`%}>Yi`HHwhoDW((6+ zEoLKql9vQ))Qr4_4=RfIj@`eHg*JX4tEB~xsYa1P?ok~(GDXKC#@#}1Pd7kc=6x?$ z7BPJ7rnwXDWYS+vZJ4XUXW209%*Nr>YzV%|E{|>+5HD@sDwaMgyNgTaEF|*Hi^S{) L(KzTB%@Fc0@xBNU diff --git a/contracts/sysio.roa/sysio.roa.wasm b/contracts/sysio.roa/sysio.roa.wasm index 37ef02e4b01805f213289ac90650e8cc2cd301ae..64a3e4c0064c3491f7f370840121a00be5883020 100755 GIT binary patch delta 1520 zcmY*ZYfKzf6rOYM?C@Au&_;jr@JR6m_my&?bx$@~s(fuiH11L&ID=>%u8MyU=? zAkJ9b<4p0c(fKgVTdPwbUYg;|q)zH{QQf;<=O57k==>*oC+^B6atl<&k;WI)k0F&$ z*fLf#yQVP3;j1dI0P`fL-DJml$0`R-dA=dDrl1guuDgx!oaUC8|L~8@YORbC-txDiIiFYka6s`JIgLILTR6}riM$ZHA^J({1 zH14|d0n$116hQ_*=s6L6yTuQl$0%foM|?>b6%}bkK>dCjcLdh)afW&PYxXU^XZ)8C z6*Agwh?>vpAdo2@eb{4_ckvS&ve5t91~0|s-Hmm!R{W;D55hgU>F7A08 z4W5?P9|di4dAjfelA10BRRTmYlrof&n7`RVObHS>6N7In_IeCEBLT8W#sTB`E5g0S zoiQ?OE^7Q{Rt3*1nL?#*mm~lfJW|obYqviKnS6fxIq-|N(mM~hHSO4k{rzYj;E6kH z@hd0ECAxQhg8K2T4KXT7QHK>Yry(RsN8{9y&d-#m;hq=E zbCAG7`P)c9L@N5^hFaA|Y<6>1jU?4kQ_H`s)=+QC=c-)%UiDDay9Xh&H3^bI_U;rf zHbnzuTChMJC~SXqcvjhJW>iwTl{R2GAY(5z!(ax*F^3Y*`QV%$E| zJ2wbj|!7q1((YK}B@=46&{NeRKVL`Ig>Nv|MjSq#-}gDJ7v$s>i`*pUwG3EwB0P6c)-4+>M55urHS$=!bgB@W z`Rvq)Y<|~J7YZ^v^rG zW+J|LEcn$cH?h+6I9B5G7b;@$<^_lY)$YDa+`(q?LqxS5J4X4|Vt Za@_S3@5tMtEdN>UiHgH3QeOWi;a~ctboc-O delta 1417 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 diff --git a/contracts/sysio.uwrit/sysio.uwrit.wasm b/contracts/sysio.uwrit/sysio.uwrit.wasm index 44ea235d704d22cccc6a256c611607f825d6a1e3..52c9a914876a16a946c428befe285930fb31c7b7 100755 GIT binary patch delta 4129 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 delta 3655 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 V;Q9Ln>5OPRef2B4n)eZe{{SNsZU6uP diff --git a/contracts/tests/sysio.cap_tests.cpp b/contracts/tests/sysio.cap_tests.cpp index 53149cb12a..395cc120f8 100644 --- a/contracts/tests/sysio.cap_tests.cpp +++ b/contracts/tests/sysio.cap_tests.cpp @@ -15,23 +15,25 @@ using namespace sysio::opp::types; using mvo = fc::mutable_variant_object; -/// Test fixture for sysio.cap. Creates the contract account, deploys WASM / -/// ABI, and loads an ABI serializer. Reusable across `sysio.cap_tests` cases -/// that need the contract live but don't yet exercise inbound handlers -/// (those land once Q1 / Q5 resolutions arrive and the StakeUpdate / -/// StakingReward inbound paths are implemented). +/// Test fixture for sysio.cap. Deploys sysio.cap plus sysio.reserv (so the +/// reward leg can price native -> WIRE off its published reserve table) and +/// creates sysio.msgch / sysio.authex as the authorized inbound callers. class sysio_cap_tester : public tester { public: static constexpr auto CAP_ACCOUNT = "sysio.cap"_n; + static constexpr auto RESERV_ACCOUNT = "sysio.reserv"_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; sysio_cap_tester() { produce_blocks(2); - + // sysio.authex is created by the tester bootstrap (account-linking + // system account); it is signed for directly to drive linkswept, never + // re-created here. create_accounts({ - CAP_ACCOUNT, TOKEN_ACCOUNT, - "alice"_n, "bob"_n, + CAP_ACCOUNT, RESERV_ACCOUNT, MSGCH_ACCOUNT, + TOKEN_ACCOUNT, "alice"_n, "bob"_n, }); produce_blocks(2); @@ -39,73 +41,277 @@ class sysio_cap_tester : public tester { set_abi(CAP_ACCOUNT, contracts::cap_abi().data()); set_privileged(CAP_ACCOUNT); + set_code(RESERV_ACCOUNT, contracts::reserve_wasm()); + set_abi(RESERV_ACCOUNT, contracts::reserve_abi().data()); + set_privileged(RESERV_ACCOUNT); produce_blocks(); - const auto* accnt = control->find_account_metadata(CAP_ACCOUNT); + cap_abi_ser.set_abi(load_abi(CAP_ACCOUNT), + abi_serializer::create_yield_function(abi_serializer_max_time)); + reserv_abi_ser.set_abi(load_abi(RESERV_ACCOUNT), + abi_serializer::create_yield_function(abi_serializer_max_time)); + } + + abi_def load_abi(name account) { + const auto* accnt = control->find_account_metadata(account); BOOST_REQUIRE(accnt != nullptr); abi_def abi; BOOST_REQUIRE_EQUAL(abi_serializer::to_abi(accnt->abi, abi), true); - cap_abi_ser.set_abi(std::move(abi), abi_serializer::create_yield_function(abi_serializer_max_time)); + return abi; } - action_result push_cap_action(name signer, name action_name, const variant_object& data) { + action_result push(name code, abi_serializer& ser, name signer, + name action_name, const variant_object& data) { try { - base_tester::push_action(CAP_ACCOUNT, action_name, signer, data); + string atype = ser.get_action_type(action_name); + action act; + act.account = code; + act.name = action_name; + act.data = ser.variant_to_binary( + atype, data, abi_serializer::create_yield_function(abi_serializer_max_time)); + act.authorization = vector{{signer, config::active_name}}; + signed_transaction trx; + trx.actions.emplace_back(std::move(act)); + set_transaction_headers(trx); + trx.sign(get_private_key(signer, "active"), control->get_chain_id()); + push_transaction(trx); + // Close a block per applied action: each OPP inbound / crank is its + // own transaction in production, and distinct TaPoS makes an + // intentional replay (e.g. the dedupe test) a new transaction that + // actually reaches the contract instead of being rejected chain-side + // as a duplicate. + produce_block(); return success(); } catch (const fc::exception& ex) { return error(ex.top_message()); } } + action_result push_cap(name signer, name action_name, const variant_object& data) { + return push(CAP_ACCOUNT, cap_abi_ser, signer, action_name, data); + } + + /// Provision a reserve so reserve::quote returns a non-zero WIRE amount. + /// Field shape mirrors sysio.reserv::setreserve's flat ABI (chain, + /// outpost_kind, outpost_amount:uint64, wire_amount:uint64, + /// connector_weight_bps:uint32) — not a packed TokenAmount. + action_result setreserve(ChainKind chain, TokenKind outpost_kind, + int64_t outpost_amount, int64_t wire_amount, + uint32_t weight = 5000) { + return push(RESERV_ACCOUNT, reserv_abi_ser, RESERV_ACCOUNT, "setreserve"_n, mvo() + ("chain", chain) + ("outpost_kind", outpost_kind) + ("outpost_amount", static_cast(outpost_amount)) + ("wire_amount", static_cast(wire_amount)) + ("connector_weight_bps", weight)); + } + + /// Dispatch a STAKING_REWARD per-staker body to cap::onreward. + action_result onreward(name signer, uint64_t outpost_id, + const std::string& wire_account, ChainKind chain, + const std::vector& native_addr, TokenKind kind, + uint64_t amount, uint32_t epoch_index, + uint64_t external_epoch_ref, uint32_t share_bps = 10000) { + return push_cap(signer, "onreward"_n, mvo() + ("outpost_id", outpost_id) + ("staker_wire_account", wire_account) + ("reward_chain", chain) + ("staker_native_addr", native_addr) + ("reward_kind", kind) + ("reward_amount", amount) + ("reward_epoch_index", epoch_index) + ("external_epoch_ref", external_epoch_ref) + ("share_bps", share_bps)); + } + + fc::variant get_kv(name table, const char* type, uint64_t id) { + auto data = get_row_by_id(CAP_ACCOUNT, CAP_ACCOUNT, table, id); + return data.empty() ? fc::variant() + : cap_abi_ser.binary_to_variant( + type, data, abi_serializer::create_yield_function(abi_serializer_max_time)); + } + 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 stage_row(uint64_t id) { return get_kv("nativestage"_n, "native_stage", id); } + fc::variant cursor_row(uint64_t id) { return get_kv("rwdcursors"_n, "reward_cursor", id); } + + std::vector addr20{std::vector(20, char(0xA1))}; + abi_serializer cap_abi_ser; + abi_serializer reserv_abi_ser; }; BOOST_AUTO_TEST_SUITE(sysio_cap_tests) +// ── existing config / import surface ── + BOOST_FIXTURE_TEST_CASE(setconfig_initializes_singleton, sysio_cap_tester) { try { - const auto result = push_cap_action(CAP_ACCOUNT, "setconfig"_n, mvo{}); - BOOST_REQUIRE_EQUAL(result, success()); + BOOST_REQUIRE_EQUAL(push_cap(CAP_ACCOUNT, "setconfig"_n, mvo{}), success()); } FC_LOG_AND_RETHROW() } BOOST_FIXTURE_TEST_CASE(claim_rejects_empty_ledger, sysio_cap_tester) { try { - const auto result = push_cap_action("alice"_n, "claim"_n, mvo()("wire_account", "alice")); - BOOST_REQUIRE_NE(result, success()); + BOOST_REQUIRE_NE(push_cap("alice"_n, "claim"_n, mvo()("wire_account", "alice")), success()); } FC_LOG_AND_RETHROW() } BOOST_FIXTURE_TEST_CASE(importseed_accepts_credit_batch, sysio_cap_tester) { try { - std::vector addr(20, char(0xAB)); - const auto result = push_cap_action(CAP_ACCOUNT, "importseed"_n, mvo + BOOST_REQUIRE_EQUAL(push_cap(CAP_ACCOUNT, "importseed"_n, mvo ("chain", ChainKind::CHAIN_KIND_ETHEREUM) - ("credits", fc::variants{ - mvo()("native_address", addr)("wire_atomic", 982953049502) - }) - ); - BOOST_REQUIRE_EQUAL(result, success()); + ("credits", fc::variants{ mvo()("native_address", addr20)("wire_atomic", 982953049502) })), + success()); + // Pre-launch import lands as an unmapped balance (unlinked by definition). + auto u = unmapped_row(1); + BOOST_REQUIRE(!u.is_null()); + BOOST_REQUIRE_EQUAL(u["balance"].as_string().substr(0, 3), std::string("982")); } FC_LOG_AND_RETHROW() } BOOST_FIXTURE_TEST_CASE(importseed_rejects_negative_atomic, sysio_cap_tester) { try { - std::vector addr(20, char(0xCD)); - const auto result = push_cap_action(CAP_ACCOUNT, "importseed"_n, mvo + BOOST_REQUIRE_NE(push_cap(CAP_ACCOUNT, "importseed"_n, mvo ("chain", ChainKind::CHAIN_KIND_ETHEREUM) - ("credits", fc::variants{ - mvo()("native_address", addr)("wire_atomic", -1) - }) - ); - BOOST_REQUIRE_NE(result, success()); + ("credits", fc::variants{ mvo()("native_address", addr20)("wire_atomic", -1) })), + success()); } FC_LOG_AND_RETHROW() } BOOST_FIXTURE_TEST_CASE(importdone_locks_subsequent_importseed, sysio_cap_tester) { try { + BOOST_REQUIRE_EQUAL(push_cap(CAP_ACCOUNT, "importdone"_n, mvo{}), success()); + BOOST_REQUIRE_NE(push_cap(CAP_ACCOUNT, "importseed"_n, mvo + ("chain", ChainKind::CHAIN_KIND_ETHEREUM) + ("credits", fc::variants{ mvo()("native_address", addr20)("wire_atomic", 1) })), + success()); +} FC_LOG_AND_RETHROW() } + +// ── setwindow ── + +BOOST_FIXTURE_TEST_CASE(setwindow_rejects_zero, sysio_cap_tester) { try { + BOOST_REQUIRE_NE(push_cap(CAP_ACCOUNT, "setwindow"_n, mvo()("window_sec", 0)), success()); + BOOST_REQUIRE_EQUAL(push_cap(CAP_ACCOUNT, "setwindow"_n, mvo()("window_sec", 3600)), success()); +} FC_LOG_AND_RETHROW() } + +BOOST_FIXTURE_TEST_CASE(setwindow_requires_self_auth, sysio_cap_tester) { try { + BOOST_REQUIRE_NE(push_cap("alice"_n, "setwindow"_n, mvo()("window_sec", 3600)), success()); +} FC_LOG_AND_RETHROW() } + +// ── onreward auth + routing ── + +BOOST_FIXTURE_TEST_CASE(onreward_requires_msgch_auth, sysio_cap_tester) { try { + BOOST_REQUIRE_NE( + onreward("alice"_n, 1, "alice", ChainKind::CHAIN_KIND_ETHEREUM, addr20, + TokenKind::TOKEN_KIND_ETH, 1000, 7, 100), + success()); +} FC_LOG_AND_RETHROW() } + +BOOST_FIXTURE_TEST_CASE(onreward_linked_credits_pending_claims, sysio_cap_tester) { try { BOOST_REQUIRE_EQUAL( - push_cap_action(CAP_ACCOUNT, "importdone"_n, mvo{}), + setreserve(ChainKind::CHAIN_KIND_ETHEREUM, TokenKind::TOKEN_KIND_ETH, 1000000, 1000000), success()); - std::vector addr(20, char(0xEF)); - const auto result = push_cap_action(CAP_ACCOUNT, "importseed"_n, mvo - ("chain", ChainKind::CHAIN_KIND_ETHEREUM) - ("credits", fc::variants{ - mvo()("native_address", addr)("wire_atomic", 1) - }) - ); - BOOST_REQUIRE_NE(result, success()); + BOOST_REQUIRE_EQUAL( + onreward(MSGCH_ACCOUNT, 1, "alice", ChainKind::CHAIN_KIND_ETHEREUM, addr20, + TokenKind::TOKEN_KIND_ETH, 1000, 7, 100), + success()); + auto p = pending_of("alice"_n); + BOOST_REQUIRE(!p.is_null()); + BOOST_REQUIRE_GT(p["balance"].as().get_amount(), 0); + // No reserve quote needed -> nothing staged. + BOOST_REQUIRE(stage_row(1).is_null()); +} FC_LOG_AND_RETHROW() } + +BOOST_FIXTURE_TEST_CASE(onreward_unlinked_parks_unmapped_then_linkswept, sysio_cap_tester) { try { + BOOST_REQUIRE_EQUAL( + setreserve(ChainKind::CHAIN_KIND_ETHEREUM, TokenKind::TOKEN_KIND_ETH, 1000000, 1000000), + success()); + // Empty wire account -> parked in unmapped by native address. + BOOST_REQUIRE_EQUAL( + onreward(MSGCH_ACCOUNT, 1, "", ChainKind::CHAIN_KIND_ETHEREUM, addr20, + TokenKind::TOKEN_KIND_ETH, 5000, 7, 100), + success()); + BOOST_REQUIRE(pending_of("bob"_n).is_null()); + auto u = unmapped_row(1); + BOOST_REQUIRE(!u.is_null()); + const int64_t parked = u["balance"].as().get_amount(); + BOOST_REQUIRE_GT(parked, 0); + + // AuthX link sweeps it into pending_claims for bob. + BOOST_REQUIRE_EQUAL( + push_cap(AUTHEX_ACCOUNT, "linkswept"_n, mvo() + ("wire_account", "bob") + ("chain", ChainKind::CHAIN_KIND_ETHEREUM) + ("native_pubkey", addr20)), + success()); + BOOST_REQUIRE(unmapped_row(1).is_null()); + BOOST_REQUIRE_EQUAL(pending_of("bob"_n)["balance"].as().get_amount(), parked); +} FC_LOG_AND_RETHROW() } + +// ── dedupe cursor ── + +BOOST_FIXTURE_TEST_CASE(onreward_dedupes_stale_external_ref, sysio_cap_tester) { try { + BOOST_REQUIRE_EQUAL( + setreserve(ChainKind::CHAIN_KIND_ETHEREUM, TokenKind::TOKEN_KIND_ETH, 1000000, 1000000), + success()); + BOOST_REQUIRE_EQUAL( + onreward(MSGCH_ACCOUNT, 1, "alice", ChainKind::CHAIN_KIND_ETHEREUM, addr20, + TokenKind::TOKEN_KIND_ETH, 1000, 7, 100), + success()); + const int64_t after_first = pending_of("alice"_n)["balance"].as().get_amount(); + + // Replay same external_epoch_ref -> admitted=false -> no extra credit. + BOOST_REQUIRE_EQUAL( + onreward(MSGCH_ACCOUNT, 1, "alice", ChainKind::CHAIN_KIND_ETHEREUM, addr20, + TokenKind::TOKEN_KIND_ETH, 1000, 7, 100), + success()); + BOOST_REQUIRE_EQUAL(pending_of("alice"_n)["balance"].as().get_amount(), after_first); + + // A newer external_epoch_ref is accepted and adds more. + BOOST_REQUIRE_EQUAL( + onreward(MSGCH_ACCOUNT, 1, "alice", ChainKind::CHAIN_KIND_ETHEREUM, addr20, + TokenKind::TOKEN_KIND_ETH, 1000, 8, 101), + success()); + BOOST_REQUIRE_GT(pending_of("alice"_n)["balance"].as().get_amount(), after_first); +} FC_LOG_AND_RETHROW() } + +// ── no-quote staging + retryconvert ── + +BOOST_FIXTURE_TEST_CASE(onreward_no_quote_stages_then_retryconvert_promotes, sysio_cap_tester) { try { + // No reserve provisioned yet -> quote 0 -> staged in native units. + BOOST_REQUIRE_EQUAL( + onreward(MSGCH_ACCOUNT, 1, "alice", ChainKind::CHAIN_KIND_ETHEREUM, addr20, + TokenKind::TOKEN_KIND_ETH, 1000, 7, 100), + success()); + auto s = stage_row(1); + BOOST_REQUIRE(!s.is_null()); + BOOST_REQUIRE_EQUAL(s["native_amount"].as_uint64(), 1000u); + BOOST_REQUIRE(pending_of("alice"_n).is_null()); + + // retryconvert with still no reserve: leaves the row staged. + BOOST_REQUIRE_EQUAL(push_cap("alice"_n, "retryconvert"_n, mvo()("max_rows", 10)), success()); + BOOST_REQUIRE(!stage_row(1).is_null()); + + // Provision the reserve, then retryconvert promotes it to pending_claims. + BOOST_REQUIRE_EQUAL( + setreserve(ChainKind::CHAIN_KIND_ETHEREUM, TokenKind::TOKEN_KIND_ETH, 1000000, 1000000), + success()); + BOOST_REQUIRE_EQUAL(push_cap("alice"_n, "retryconvert"_n, mvo()("max_rows", 10)), success()); + BOOST_REQUIRE(stage_row(1).is_null()); + BOOST_REQUIRE_GT(pending_of("alice"_n)["balance"].as().get_amount(), 0); +} FC_LOG_AND_RETHROW() } + +// ── claimable window expiry / reversion ── + +BOOST_FIXTURE_TEST_CASE(flushexpired_reverts_expired_pending, sysio_cap_tester) { try { + BOOST_REQUIRE_EQUAL(push_cap(CAP_ACCOUNT, "setwindow"_n, mvo()("window_sec", 1)), success()); + BOOST_REQUIRE_EQUAL( + setreserve(ChainKind::CHAIN_KIND_ETHEREUM, TokenKind::TOKEN_KIND_ETH, 1000000, 1000000), + success()); + BOOST_REQUIRE_EQUAL( + onreward(MSGCH_ACCOUNT, 1, "alice", ChainKind::CHAIN_KIND_ETHEREUM, addr20, + TokenKind::TOKEN_KIND_ETH, 1000, 7, 100), + success()); + BOOST_REQUIRE(!pending_of("alice"_n).is_null()); + + // Advance chain time well past the 1-second window. + produce_blocks(10); + produce_block(fc::seconds(5)); + + BOOST_REQUIRE_EQUAL(push_cap("alice"_n, "flushexpired"_n, mvo()("max_rows", 50)), success()); + BOOST_REQUIRE(pending_of("alice"_n).is_null()); // reverted to capital fund } FC_LOG_AND_RETHROW() } BOOST_AUTO_TEST_SUITE_END() diff --git a/libraries/opp/proto/sysio/opp/attestations/attestations.proto b/libraries/opp/proto/sysio/opp/attestations/attestations.proto index b6384da16d..2f03537032 100644 --- a/libraries/opp/proto/sysio/opp/attestations/attestations.proto +++ b/libraries/opp/proto/sysio/opp/attestations/attestations.proto @@ -425,23 +425,42 @@ message NodeOwnerReg { sysio.opp.types.ChainAddress nft_address = 3; } -// Staking reward distribution (Outpost -> Depot). +// 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 depot routes this to `sysio.reserve::credit_lp` (or a -// per-staker share contract) so the staker's WIRE-side accounting is updated. +// Emitted by an outpost when a staker's slice of a validator-reward +// distribution has been computed. `sysio.msgch` routes it two ways: the +// aggregate native amount credits the outpost-side reserve +// (`sysio.reserv::onreward`, keeps `swapquote` solvent), and the per-staker +// body is dispatched to `sysio.cap::onreward`, which prices native -> WIRE off +// the reserve and credits the staker's claim ledger (`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 { // Outpost id emitting the reward — identifies the (chain, validator) context. uint64 outpost_id = 1; - // The staker's WIRE account. + // 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; - // Reward period (milliseconds since epoch). - uint64 period_start_ms = 4; - uint64 period_end_ms = 5; - // Absolute reward amount + token kind being credited. + // 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 + token kind being credited (native-denominated). 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; } // `NativeYieldReward` removed — `StakingReward` is the single staker-reward From ba738bae253526d97be0729199bfdedb72ce615d Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Tue, 19 May 2026 12:35:46 -0500 Subject: [PATCH 11/12] test: create sysio.uwrit in the emissions fixture sysio.epoch's epoch-advance fires an unconditional inline sysio.uwrit::chklocks (underwriter lock-expiry sweep, added with the part3 underwriter work). The t5_emissions_tests fixture created OPREG/EPOCH/CHALG/MSGCH but not sysio.uwrit, so every advance_epoch_state() aborted with "inline action's code account sysio.uwrit does not exist". The chain only requires the inline target account to exist. Add sysio.uwrit to create_accounts (bare: no ROA policy, no code) so the inline call is a harmless no-op -- no underwriter locks are staged in these tests. A 500 SYS RAM policy like the other accounts get would overrun nodedaddy's ROA pool. --- contracts/tests/emissions_tests.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/contracts/tests/emissions_tests.cpp b/contracts/tests/emissions_tests.cpp index 87d62d5407..d533bc0f37 100644 --- a/contracts/tests/emissions_tests.cpp +++ b/contracts/tests/emissions_tests.cpp @@ -53,6 +53,7 @@ static constexpr account_name OPREG = "sysio.opreg"_n; static constexpr account_name EPOCH = "sysio.epoch"_n; static constexpr account_name CHALG = "sysio.chalg"_n; static constexpr account_name MSGCH = "sysio.msgch"_n; +static constexpr account_name UWRIT = "sysio.uwrit"_n; // Keep these in sync with contracts/sysio.system/src/emissions.cpp static constexpr uint32_t SECONDS_PER_MONTH = 30u * 24u * 60u * 60u; @@ -325,7 +326,14 @@ class sysio_emissions_tester : public tester { // // Under ROA (active via the base tester), accounts need explicit ROA // RAM policies before set_code can succeed for a large contract. - create_accounts({ OPREG, EPOCH, CHALG, MSGCH }); + // + // UWRIT is created bare (no ROA policy, no code) on purpose: epoch + // advance fires an unconditional inline sysio.uwrit::chklocks + // (underwriter lock-expiry sweep). The chain only requires the target + // account to exist; with no code the inline call is a harmless no-op, + // and no underwriter locks are staged in these tests. Giving it a + // 500 SYS policy like the others would overrun nodedaddy's ROA pool. + create_accounts({ OPREG, EPOCH, CHALG, MSGCH, UWRIT }); produce_blocks(1); for (auto acct : { OPREG, EPOCH, CHALG, MSGCH }) { From b64bb97cf699c816eea2067e5c3e3afc878df888 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Fri, 22 May 2026 16:39:58 -0500 Subject: [PATCH 12/12] sysio.dclaim: rename from sysio.cap and address PR #348 review PR #348 review feedback. Rename the sysio.cap contract to sysio.dclaim. `cap` was flagged as too generic; `dclaim` (depot claim) names what the contract is -- the depot-side WIRE distribution and claim ledger. This renames the account, the contracts/sysio.dclaim/ directory, the C++ class, and every sysio.cap reference in sysio.msgch, sysio.system emissions, the tests, and contracts.hpp. The internal table names (capcfg, capcounters, ...) are left as-is -- they are the contract's schema, not its renamed identity. Rename the setwindow action to setclmwindow: clearer about which window it sets, and still within the 12-character action-name limit. Drop the per-contract magic_enum -I cmake hack -- the CDT WASM toolchain already supplies that include path, matching how the other OPP contracts resolve magic_enum. --- contracts/CMakeLists.txt | 2 +- .../CMakeLists.txt | 7 +- .../include/sysio.dclaim/sysio.dclaim.hpp} | 16 ++-- .../src/sysio.dclaim.cpp} | 68 +++++++------- .../sysio.dclaim.abi} | 20 ++--- .../sysio.dclaim.wasm} | Bin 29904 -> 29907 bytes .../tools/convert_import.py | 6 +- contracts/sysio.msgch/src/sysio.msgch.cpp | 6 +- contracts/sysio.msgch/sysio.msgch.wasm | Bin 132879 -> 132879 bytes .../sysio.opp.common/opp_table_types.hpp | 2 +- contracts/sysio.system/src/emissions.cpp | 2 +- contracts/sysio.system/sysio.system.wasm | Bin 162789 -> 162789 bytes contracts/tests/contracts.hpp.in | 4 +- contracts/tests/emissions_tests.cpp | 10 +-- ...o.cap_tests.cpp => sysio.dclaim_tests.cpp} | 84 +++++++++--------- .../tests/sysio.epoch_flushwtdw_tests.cpp | 4 +- 16 files changed, 114 insertions(+), 117 deletions(-) rename contracts/{sysio.cap => sysio.dclaim}/CMakeLists.txt (88%) rename contracts/{sysio.cap/include/sysio.cap/sysio.cap.hpp => sysio.dclaim/include/sysio.dclaim/sysio.dclaim.hpp} (96%) rename contracts/{sysio.cap/src/sysio.cap.cpp => sysio.dclaim/src/sysio.dclaim.cpp} (85%) rename contracts/{sysio.cap/sysio.cap.abi => sysio.dclaim/sysio.dclaim.abi} (98%) rename contracts/{sysio.cap/sysio.cap.wasm => sysio.dclaim/sysio.dclaim.wasm} (81%) rename contracts/{sysio.cap => sysio.dclaim}/tools/convert_import.py (97%) rename contracts/tests/{sysio.cap_tests.cpp => sysio.dclaim_tests.cpp} (74%) diff --git a/contracts/CMakeLists.txt b/contracts/CMakeLists.txt index b061d7260e..9ca1153f2e 100644 --- a/contracts/CMakeLists.txt +++ b/contracts/CMakeLists.txt @@ -53,7 +53,7 @@ add_subdirectory(sysio.msgch) add_subdirectory(sysio.uwrit) add_subdirectory(sysio.chalg) add_subdirectory(sysio.reserv) -add_subdirectory(sysio.cap) +add_subdirectory(sysio.dclaim) add_subdirectory(sysio.token) add_subdirectory(sysio.wrap) diff --git a/contracts/sysio.cap/CMakeLists.txt b/contracts/sysio.dclaim/CMakeLists.txt similarity index 88% rename from contracts/sysio.cap/CMakeLists.txt rename to contracts/sysio.dclaim/CMakeLists.txt index 6a14948542..1b3aa88450 100644 --- a/contracts/sysio.cap/CMakeLists.txt +++ b/contracts/sysio.dclaim/CMakeLists.txt @@ -1,4 +1,4 @@ -set(contract_name sysio.cap) +set(contract_name sysio.dclaim) bootstrap_contract(${contract_name}) if(BUILD_SYSTEM_CONTRACTS) @@ -14,7 +14,7 @@ if(BUILD_SYSTEM_CONTRACTS) TARGET ${contract_name}_native SOURCES ${SOURCES} INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/include - CONTRACT_CLASS "sysio::cap" + CONTRACT_CLASS "sysio::dclaim" HEADERS ${HEADERS} ABI_FILE ${CMAKE_BINARY_DIR}/contracts/${contract_name}/${contract_name}.abi ) @@ -36,9 +36,6 @@ if(BUILD_SYSTEM_CONTRACTS) $ $ $ - # cdt-cpp ignores -isystem; add vcpkg root explicitly as -I so magic_enum resolves. - PRIVATE - ${CDT_CONTRACT_INCLUDE_PATH} ) target_link_libraries(${target} diff --git a/contracts/sysio.cap/include/sysio.cap/sysio.cap.hpp b/contracts/sysio.dclaim/include/sysio.dclaim/sysio.dclaim.hpp similarity index 96% rename from contracts/sysio.cap/include/sysio.cap/sysio.cap.hpp rename to contracts/sysio.dclaim/include/sysio.dclaim/sysio.dclaim.hpp index aac4ec0f8b..05570657c4 100644 --- a/contracts/sysio.cap/include/sysio.cap/sysio.cap.hpp +++ b/contracts/sysio.dclaim/include/sysio.dclaim/sysio.dclaim.hpp @@ -17,7 +17,7 @@ namespace sysio { /** - * @brief sysio.cap — depot-side WIRE distribution and claim ledger. + * @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 @@ -42,16 +42,16 @@ namespace sysio { * * Claimable lifespan: every credited / staged balance carries an * `expires_at_sec`. `flushexpired` prunes anything past it; the WIRE stays - * in the `sysio.cap` account balance — i.e. it reverts into the staking + * in the `sysio.dclaim` account balance — i.e. it reverts into the staking * capital fund for redistribution. The window is configurable - * (`setwindow`), defaulting to 180 days. + * (`setclmwindow`), defaulting to 180 days. * * No cooldown/withdrawal machinery for v1 (no withdrawal flow in this * wave). When withdrawals come online post-launch, the cooldown-queue + * maturation-flush pattern can be added back — see opreg's * `withdraw_queue` for reference. */ - class [[sysio::contract("sysio.cap")]] cap : public contract { + class [[sysio::contract("sysio.dclaim")]] dclaim : public contract { public: using contract::contract; @@ -65,7 +65,7 @@ namespace sysio { static constexpr symbol WIRE_SYM = symbol("WIRE", 9); // Default claimable-reward lifespan: 180 days, in seconds. Configurable - // per deployment via `setwindow`. + // per deployment via `setclmwindow`. static constexpr uint32_t DEFAULT_CLAIM_WINDOW_SEC = 180u * 24u * 60u * 60u; // ----------------------------------------------------------------------- @@ -80,10 +80,10 @@ namespace sysio { /// Set the claimable-reward window (seconds). Unclaimed balances older /// than this revert to the capital fund on `flushexpired`. Auth=self. [[sysio::action]] - void setwindow(uint32_t window_sec); + void setclmwindow(uint32_t window_sec); /// User-callable: drain the caller's `pending_claims` row via an inline - /// transfer of WIRE from `sysio.cap` to `wire_account`. Erases the row. + /// transfer of WIRE from `sysio.dclaim` to `wire_account`. Erases the row. /// Reverts if no row exists or the balance is zero. [[sysio::action]] void claim(name wire_account); @@ -130,7 +130,7 @@ namespace sysio { /// Permissionless crank: prune up to `max_rows` expired ledger rows /// (`pending_claims`, `unmapped_tokens`). Erasing a credited row leaves - /// its WIRE in the `sysio.cap` balance — it reverts into the staking + /// its WIRE in the `sysio.dclaim` balance — it reverts into the staking /// capital fund for redistribution. Bounded. [[sysio::action]] void flushexpired(uint32_t max_rows); diff --git a/contracts/sysio.cap/src/sysio.cap.cpp b/contracts/sysio.dclaim/src/sysio.dclaim.cpp similarity index 85% rename from contracts/sysio.cap/src/sysio.cap.cpp rename to contracts/sysio.dclaim/src/sysio.dclaim.cpp index 7ecba1d46e..7d9580b8e2 100644 --- a/contracts/sysio.cap/src/sysio.cap.cpp +++ b/contracts/sysio.dclaim/src/sysio.dclaim.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include @@ -33,16 +33,16 @@ auto scan_find(Index& idx, uint128_t key, KeyFn key_of, MatchFn matches) { /// Current claimable-reward window (seconds) from config, default if unset. uint32_t config_window(name self) { - cap::capcfg_t cfg(self); - return cfg.get_or_default(cap::cap_config{}).claim_window_sec; + dclaim::capcfg_t cfg(self); + return cfg.get_or_default(dclaim::cap_config{}).claim_window_sec; } /// Allocate the next id from one of the monotonic counters. `pick` returns a /// reference to the field to bump. template uint64_t next_id(name self, Pick pick) { - cap::capcounters_t cnt(self); - cap::cap_counters c = cnt.get_or_default(cap::cap_counters{}); + dclaim::capcounters_t cnt(self); + dclaim::cap_counters c = cnt.get_or_default(dclaim::cap_counters{}); uint64_t& field = pick(c); uint64_t id = field++; cnt.set(c, self); @@ -59,15 +59,15 @@ void credit_wire(name self, name wacct, ChainKind chain, const uint32_t exp = now_sec() + window; if (wacct.value != 0) { - cap::pclaims_t pclaims(self); - auto it = pclaims.find(cap::pclaim_key{wacct.value}); + dclaim::pclaims_t pclaims(self); + auto it = pclaims.find(dclaim::pclaim_key{wacct.value}); if (it == pclaims.end()) { - pclaims.emplace(self, cap::pclaim_key{wacct.value}, - cap::pending_claim{ .wire_account = wacct, + pclaims.emplace(self, dclaim::pclaim_key{wacct.value}, + dclaim::pending_claim{ .wire_account = wacct, .balance = amt, .expires_at_sec = exp }); } else { - pclaims.modify(same_payer, cap::pclaim_key{wacct.value}, [&](auto& r) { + pclaims.modify(same_payer, dclaim::pclaim_key{wacct.value}, [&](auto& r) { r.balance += amt; r.expires_at_sec = exp; }); @@ -75,26 +75,26 @@ void credit_wire(name self, name wacct, ChainKind chain, return; } - cap::unmapped_t unmapped(self); + dclaim::unmapped_t unmapped(self); auto idx = unmapped.template get_index<"bychainad"_n>(); - auto it = scan_find(idx, cap::chain_addr_key(chain, addr), + auto it = scan_find(idx, dclaim::chain_addr_key(chain, addr), [](const auto& r) { return r.by_chain_addr(); }, [&](const auto& r) { return r.chain_kind == chain && r.native_pubkey == addr; }); if (it == idx.end()) { - uint64_t id = next_id(self, [](cap::cap_counters& c) -> uint64_t& { + uint64_t id = next_id(self, [](dclaim::cap_counters& c) -> uint64_t& { return c.next_unmapped_id; }); - unmapped.emplace(self, cap::unmapped_key{id}, - cap::unmapped_token{ .id = id, + unmapped.emplace(self, dclaim::unmapped_key{id}, + dclaim::unmapped_token{ .id = id, .chain_kind = chain, .native_pubkey = addr, .balance = amt, .expires_at_sec = exp }); } else { uint64_t rid = it->id; - unmapped.modify(same_payer, cap::unmapped_key{rid}, [&](auto& r) { + unmapped.modify(same_payer, dclaim::unmapped_key{rid}, [&](auto& r) { r.balance += amt; r.expires_at_sec = exp; }); @@ -108,9 +108,9 @@ void credit_wire(name self, name wacct, ChainKind chain, /// still staged awaiting a quote. bool cursor_admit(name self, uint64_t outpost_id, ChainKind chain, const std::vector& addr, uint64_t ext_ref) { - cap::rwdcursors_t cur(self); + dclaim::rwdcursors_t cur(self); auto idx = cur.template get_index<"byoutaddr"_n>(); - auto it = scan_find(idx, cap::outpost_addr_key(outpost_id, chain, addr), + auto it = scan_find(idx, dclaim::outpost_addr_key(outpost_id, chain, addr), [](const auto& r) { return r.by_outpost_addr(); }, [&](const auto& r) { return r.outpost_id == outpost_id @@ -118,11 +118,11 @@ bool cursor_admit(name self, uint64_t outpost_id, ChainKind chain, && r.native_pubkey == addr; }); if (it == idx.end()) { - uint64_t id = next_id(self, [](cap::cap_counters& c) -> uint64_t& { + uint64_t id = next_id(self, [](dclaim::cap_counters& c) -> uint64_t& { return c.next_cursor_id; }); - cur.emplace(self, cap::rwdcur_key{id}, - cap::reward_cursor{ .id = id, + cur.emplace(self, dclaim::rwdcur_key{id}, + dclaim::reward_cursor{ .id = id, .outpost_id = outpost_id, .chain = chain, .native_pubkey = addr, @@ -131,7 +131,7 @@ bool cursor_admit(name self, uint64_t outpost_id, ChainKind chain, } if (ext_ref <= it->last_external_epoch_ref) return false; uint64_t rid = it->id; - cur.modify(same_payer, cap::rwdcur_key{rid}, [&](auto& r) { + cur.modify(same_payer, dclaim::rwdcur_key{rid}, [&](auto& r) { r.last_external_epoch_ref = ext_ref; }); return true; @@ -142,7 +142,7 @@ bool cursor_admit(name self, uint64_t outpost_id, ChainKind chain, // --------------------------------------------------------------------------- // setconfig // --------------------------------------------------------------------------- -void cap::setconfig() { +void dclaim::setconfig() { require_auth(get_self()); capcfg_t cfg(get_self()); if (!cfg.exists()) { @@ -151,9 +151,9 @@ void cap::setconfig() { } // --------------------------------------------------------------------------- -// setwindow +// setclmwindow // --------------------------------------------------------------------------- -void cap::setwindow(uint32_t window_sec) { +void dclaim::setclmwindow(uint32_t window_sec) { require_auth(get_self()); check(window_sec > 0, "window_sec must be positive"); capcfg_t cfg(get_self()); @@ -165,7 +165,7 @@ void cap::setwindow(uint32_t window_sec) { // --------------------------------------------------------------------------- // claim // --------------------------------------------------------------------------- -void cap::claim(name wire_account) { +void dclaim::claim(name wire_account) { require_auth(wire_account); pclaims_t pclaims(get_self()); @@ -179,14 +179,14 @@ void cap::claim(name wire_account) { permission_level{ get_self(), "active"_n }, TOKEN_ACCOUNT, "transfer"_n, - std::make_tuple(get_self(), wire_account, payout, std::string("sysio.cap claim")) + std::make_tuple(get_self(), wire_account, payout, std::string("sysio.dclaim claim")) ).send(); } // --------------------------------------------------------------------------- // linkswept — AuthX link completed: sweep unmapped -> pending. // --------------------------------------------------------------------------- -void cap::linkswept(name wire_account, ChainKind chain, std::vector native_pubkey) { +void dclaim::linkswept(name wire_account, ChainKind chain, std::vector native_pubkey) { require_auth(AUTHEX_ACCOUNT); const uint32_t window = config_window(get_self()); @@ -211,7 +211,7 @@ void cap::linkswept(name wire_account, ChainKind chain, std::vector native // --------------------------------------------------------------------------- // onreward — per-staker WIRE-side credit of a STAKING_REWARD // --------------------------------------------------------------------------- -void cap::onreward(uint64_t outpost_id, +void dclaim::onreward(uint64_t outpost_id, std::string staker_wire_account, opp::types::ChainKind reward_chain, std::vector staker_native_addr, @@ -222,7 +222,7 @@ void cap::onreward(uint64_t outpost_id, require_auth(MSGCH_ACCOUNT); // Tolerate degenerate input rather than aborting the inbound OPP envelope - // (the verifier role lives upstream in msgch::evalcons; cap trusts but + // (the verifier role lives upstream in msgch::evalcons; dclaim trusts but // must not break the message chain on a malformed row). if (reward_amount == 0 || staker_native_addr.empty()) return; @@ -247,9 +247,9 @@ void cap::onreward(uint64_t outpost_id, // --------------------------------------------------------------------------- // flushexpired — prune expired rows; credited WIRE reverts to the capital -// fund (it simply stays in the sysio.cap balance once the row is erased). +// fund (it simply stays in the sysio.dclaim balance once the row is erased). // --------------------------------------------------------------------------- -void cap::flushexpired(uint32_t max_rows) { +void dclaim::flushexpired(uint32_t max_rows) { const uint32_t cutoff = now_sec(); uint32_t budget = max_rows; @@ -277,7 +277,7 @@ void cap::flushexpired(uint32_t max_rows) { // --------------------------------------------------------------------------- // importseed — bootstrap pre-launch holders into unmapped_tokens // --------------------------------------------------------------------------- -void cap::importseed(ChainKind chain, std::vector credits) { +void dclaim::importseed(ChainKind chain, std::vector credits) { require_auth(get_self()); capcfg_t cfg(get_self()); @@ -304,7 +304,7 @@ void cap::importseed(ChainKind chain, std::vector credits) { // --------------------------------------------------------------------------- // importdone // --------------------------------------------------------------------------- -void cap::importdone() { +void dclaim::importdone() { require_auth(get_self()); capcfg_t cfg(get_self()); cap_config current = cfg.get_or_default(cap_config{}); diff --git a/contracts/sysio.cap/sysio.cap.abi b/contracts/sysio.dclaim/sysio.dclaim.abi similarity index 98% rename from contracts/sysio.cap/sysio.cap.abi rename to contracts/sysio.dclaim/sysio.dclaim.abi index 448aeedfa6..bf6de3de5a 100644 --- a/contracts/sysio.cap/sysio.cap.abi +++ b/contracts/sysio.dclaim/sysio.dclaim.abi @@ -205,12 +205,7 @@ ] }, { - "name": "setconfig", - "base": "", - "fields": [] - }, - { - "name": "setwindow", + "name": "setclmwindow", "base": "", "fields": [ { @@ -219,6 +214,11 @@ } ] }, + { + "name": "setconfig", + "base": "", + "fields": [] + }, { "name": "unmapped_key", "base": "", @@ -288,13 +288,13 @@ "ricardian_contract": "" }, { - "name": "setconfig", - "type": "setconfig", + "name": "setclmwindow", + "type": "setclmwindow", "ricardian_contract": "" }, { - "name": "setwindow", - "type": "setwindow", + "name": "setconfig", + "type": "setconfig", "ricardian_contract": "" } ], diff --git a/contracts/sysio.cap/sysio.cap.wasm b/contracts/sysio.dclaim/sysio.dclaim.wasm similarity index 81% rename from contracts/sysio.cap/sysio.cap.wasm rename to contracts/sysio.dclaim/sysio.dclaim.wasm index 0495eb57e40f6403d0eaf8ecb25b135183af8857..c8727667f666041bb1559bd1a9c7e1154bb81bc5 100755 GIT binary patch delta 724 zcmXX@Ur19?96rBuuWLE2TY_c=UGJQ(@*xv6P}{vLXS$JQB1+J-L~$vpm^$7xcfRv|-~FBQdmDwtC_H*1 znQtx-%Xwl1*$b)G5pb!*Q2I3RX|#w2Tm~_SoyN;GNhLmGv(M=2)<_W&R9C5?^s2<2 ziW0=&Lp?hpS8VxHjzmIa5C_%(n~-ziGJh>A6t446rdfb#7BuTH%_HVf3RBE)xdRJa zOSvk;5}#C1x6EE{9iW5rtR&3}Q{0<&PD0&>^fm}HJ>v$1`C>*oAho>hEK*IGPNc>& zyVog=6=!+za3sqEEBtF#RG8oFY=qCuu4+iR)g1BPF0CESAGKjLcUiX!^R=9Qq$HaI zse`tDK|RkM#BW*NFSPHrA42;X`%?*GEH{6Tb##s>Nh;YTk!)%auj2IoUo+&GcpKv} z@6GR%G4$tNj-g`(8z~0eFRaC&c;Vj-FvnI)s;PgT9WGUqAHE{lkOCGX%6dv;c=gt@ zLD9sI10%4&{Eh&uuq1sS;{Cc469)7G6U5o1t7ymYEVd$gknNPHvLXAhRwR0Oz}8$L zi1ScI5MYc~S1MRoq|U}KR5b`#U1?*I$1RYj24Xzu(ak7~)a8L`dNKO@*)6uIz94%h z0q-t3kEYV~tH$1kaa)@zsi1;#nl;s5gbx0_J|c!3Zdk%0k2kjBh9@7tEx{#r??f9; zSMjc3{F--2$cCD$5@H*VH`h>v5wnf=w3LVrLLJ4z{H^nglzhcRiQ%6imR0ty`=qJm RR7Z1rCok?9rm|`!K#Shq4no_)A%#aGig!j023aeya|nJZ!y7VT75s`!K<^ zxEnIe@?iya3+%=AHu`sxb&NT_?8SPxT7PwIgwK9iIPNPXFH8L66N4^jik zjT@B4a#H;GvoFO5i~LhcM3`Ugu7>x_rD{l(sA=NdpmrY3@3dYt2kkqB`DR)xQj)`s z)DcIkpq@F~@tvRk743UnN6~)C^;Ci-=FHeH_9 z%^58+hA!^s7_cS&V)F@j=~#32l0&1y$dH&)@DYnx>iD8Zr+X{Qr Sct$!~8{~(Yda10~3Hb*Ri~|G! diff --git a/contracts/sysio.cap/tools/convert_import.py b/contracts/sysio.dclaim/tools/convert_import.py similarity index 97% rename from contracts/sysio.cap/tools/convert_import.py rename to contracts/sysio.dclaim/tools/convert_import.py index 31f817530e..6e8ef910a1 100755 --- a/contracts/sysio.cap/tools/convert_import.py +++ b/contracts/sysio.dclaim/tools/convert_import.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Convert the indexer JSON dump into sysio.cap::importseed action batches. +"""Convert the indexer JSON dump into sysio.dclaim::importseed action batches. Live sources: ETH: curl -H 'x-api-key: ' https://index.wire.foundation/opp/balances @@ -36,7 +36,7 @@ Rows with wire_atomic == 0 are filtered. Output is a JSON array of importseed action arg objects, each batched up to --batch-size credits per call. `native_address` is emitted as the hex spelling of the raw -bytes (no 0x prefix), which the sysio.cap ABI consumes as `bytes`. +bytes (no 0x prefix), which the sysio.dclaim ABI consumes as `bytes`. Batching: default is 10,000 credits per trx, which fits well inside the 150ms execution / 500KB transaction-size envelope. At launch @@ -103,7 +103,7 @@ def eth_decode(addr: str) -> bytes: # --------------------------------------------------------------------------- def parse_args() -> argparse.Namespace: ap = argparse.ArgumentParser( - description="Emit sysio.cap::importseed batches from the indexer JSON.", + description="Emit sysio.dclaim::importseed batches from the indexer JSON.", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__, ) diff --git a/contracts/sysio.msgch/src/sysio.msgch.cpp b/contracts/sysio.msgch/src/sysio.msgch.cpp index 8ce21cbc79..f514e7b605 100644 --- a/contracts/sysio.msgch/src/sysio.msgch.cpp +++ b/contracts/sysio.msgch/src/sysio.msgch.cpp @@ -501,7 +501,7 @@ void dispatch_attestation(name self, uint64_t attestation_id, break; case AttestationType::ATTESTATION_TYPE_STAKING_REWARD: - // Per-staker staking reward -> sysio.cap claim ledger. The v6 + // Per-staker staking reward -> sysio.dclaim claim ledger. The v6 // staking-reward path does not deposit back to a reserve (the // external-pool credit and native -> WIRE conversion are // outpost-side), so the pre-v6 reserv::onreward leg is dropped and @@ -514,12 +514,12 @@ void dispatch_attestation(name self, uint64_t attestation_id, const uint64_t reward_raw = static_cast(static_cast(sr.reward_amount.amount)); // staker_wire_account.name is the raw account string (empty => - // staker not yet AuthX-linked, so sysio.cap parks by native + // staker not yet AuthX-linked, so sysio.dclaim parks by native // address). staker_native_address carries the chain (kind) and // the raw address bytes. action( permission_level{self, "active"_n}, - "sysio.cap"_n, "onreward"_n, + "sysio.dclaim"_n, "onreward"_n, std::make_tuple(sr.outpost_id, sr.staker_wire_account.name, sr.staker_native_address.kind, diff --git a/contracts/sysio.msgch/sysio.msgch.wasm b/contracts/sysio.msgch/sysio.msgch.wasm index 1a3d204d866b6663c5f20790c3315824cd9a3b30..fbfd9d3d53365419226dc4eb208fd077cd885588 100755 GIT binary patch delta 23 fcmeD0XxPG7%*(o9*Rms%wpZ{nPX7r2awrNY delta 23 fcmeD0XxPG7%*)!)uwd(i?G?O?(|-a0ZO93? 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 e5235f9c32..07486a6a5b 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 @@ -567,7 +567,7 @@ DataStream& operator>>(DataStream& ds, NodeOwnerReg& t) { // StakingReward — the single staker-reward feedback path. `sysio.msgch` // routes it twice: the aggregate native amount to `sysio.reserv::onreward` -// (outpost-side reserve), and the per-staker body to `sysio.cap::onreward` +// (outpost-side reserve), and the per-staker body to `sysio.dclaim::onreward` // (WIRE-side claim ledger). Field order mirrors the proto (1..7). template DataStream& operator<<(DataStream& ds, const StakingReward& t) { diff --git a/contracts/sysio.system/src/emissions.cpp b/contracts/sysio.system/src/emissions.cpp index 3e9f28ecc9..f5e749fd24 100644 --- a/contracts/sysio.system/src/emissions.cpp +++ b/contracts/sysio.system/src/emissions.cpp @@ -49,7 +49,7 @@ constexpr uint32_t ACTIVE_PRODUCER_WEIGHT = 15; // > any standby weight (1..cfg. // Basis-point denominator for all category / sub-split ratios. constexpr int64_t BPS_DENOMINATOR = 10000; -constexpr sysio::name CAPITAL_ACCOUNT = "sysio.cap"_n; +constexpr sysio::name CAPITAL_ACCOUNT = "sysio.dclaim"_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; diff --git a/contracts/sysio.system/sysio.system.wasm b/contracts/sysio.system/sysio.system.wasm index 89773653ef65a5df9dbd2ff383e9b2ea68c616bd..3c9d6e687db2a66297b4eb78deaa712d8351289d 100755 GIT binary patch delta 25 hcmaF*o%88;&W0AoEll5xSr_bDc4X4_-^NU@+ySq~4I%&l delta 25 hcmaF*o%88;&W0AoEll5xSsNM_Y@M+Ew=vTzcL1wq46y(J diff --git a/contracts/tests/contracts.hpp.in b/contracts/tests/contracts.hpp.in index 4c1db8e942..4a507fc4a8 100644 --- a/contracts/tests/contracts.hpp.in +++ b/contracts/tests/contracts.hpp.in @@ -30,8 +30,8 @@ struct contracts { static std::vector chalg_abi() { return read_abi("${CMAKE_BINARY_DIR}/contracts/sysio.chalg/sysio.chalg.abi"); } static std::vector reserve_wasm() { return read_wasm("${CMAKE_BINARY_DIR}/contracts/sysio.reserv/sysio.reserv.wasm"); } static std::vector reserve_abi() { return read_abi("${CMAKE_BINARY_DIR}/contracts/sysio.reserv/sysio.reserv.abi"); } - static std::vector cap_wasm() { return read_wasm("${CMAKE_BINARY_DIR}/contracts/sysio.cap/sysio.cap.wasm"); } - static std::vector cap_abi() { return read_abi("${CMAKE_BINARY_DIR}/contracts/sysio.cap/sysio.cap.abi"); } + static std::vector dclaim_wasm() { return read_wasm("${CMAKE_BINARY_DIR}/contracts/sysio.dclaim/sysio.dclaim.wasm"); } + static std::vector dclaim_abi() { return read_abi("${CMAKE_BINARY_DIR}/contracts/sysio.dclaim/sysio.dclaim.abi"); } static std::vector chains_wasm() { return read_wasm("${CMAKE_BINARY_DIR}/contracts/sysio.chains/sysio.chains.wasm"); } static std::vector chains_abi() { return read_abi("${CMAKE_BINARY_DIR}/contracts/sysio.chains/sysio.chains.abi"); } static std::vector tokens_wasm() { return read_wasm("${CMAKE_BINARY_DIR}/contracts/sysio.tokens/sysio.tokens.wasm"); } diff --git a/contracts/tests/emissions_tests.cpp b/contracts/tests/emissions_tests.cpp index b926e29ccf..91c0330155 100644 --- a/contracts/tests/emissions_tests.cpp +++ b/contracts/tests/emissions_tests.cpp @@ -408,7 +408,7 @@ class sysio_emissions_tester : public tester { // ----------------------------- void create_t5_holding_accounts() { vector accts = { - "sysio.cap"_n, "sysio.gov"_n, "sysio.batch"_n, "sysio.ops"_n + "sysio.dclaim"_n, "sysio.gov"_n, "sysio.batch"_n, "sysio.ops"_n }; for (auto a : accts) { if (!control->db().find(a)) { @@ -2486,7 +2486,7 @@ BOOST_FIXTURE_TEST_CASE( active_producers_get_equal_share, sysio_emissions_teste // --------------------------------------------------------------------------- BOOST_FIXTURE_TEST_CASE( holding_accounts_receive_correct_amounts, sysio_emissions_tester ) try { - // Category bucket recipients are fixed accounts (sysio.cap / sysio.gov / sysio.ops). + // Category bucket recipients are fixed accounts (sysio.dclaim / sysio.gov / sysio.ops). // The batch-op share is NOT sent to a holding account -- it is split across the // members of the current sysio.epoch batch_op_groups rotation slot. When no // operators are registered, the entire batch_pool stays in the treasury. @@ -2494,7 +2494,7 @@ BOOST_FIXTURE_TEST_CASE( holding_accounts_receive_correct_amounts, sysio_emissio const uint32_t start = head_secs() - ONE_EPOCH - 1; BOOST_REQUIRE_EQUAL( success(), initt5( config::system_account_name, tpsec(start) ) ); - asset cap_before = get_wire_balance("sysio.cap"_n); + asset dclaim_before = get_wire_balance("sysio.dclaim"_n); asset gov_before = get_wire_balance("sysio.gov"_n); asset batch_before = get_wire_balance("sysio.batch"_n); asset ops_before = get_wire_balance("sysio.ops"_n); @@ -2507,12 +2507,12 @@ BOOST_FIXTURE_TEST_CASE( holding_accounts_receive_correct_amounts, sysio_emissio int64_t gov = log["governance_amount"].as(); int64_t capex_base = test_split_bps(emission, CAPEX_BPS); - int64_t cap_received = get_wire_balance("sysio.cap"_n).get_amount() - cap_before.get_amount(); + 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(); - BOOST_REQUIRE_EQUAL( cap_received, capital ); + BOOST_REQUIRE_EQUAL( dclaim_received, capital ); BOOST_REQUIRE_EQUAL( gov_received, gov ); // sysio.batch is not an emissions recipient anymore -- batch pay goes to the // current rotation group, not a holding account. diff --git a/contracts/tests/sysio.cap_tests.cpp b/contracts/tests/sysio.dclaim_tests.cpp similarity index 74% rename from contracts/tests/sysio.cap_tests.cpp rename to contracts/tests/sysio.dclaim_tests.cpp index 192577c285..a9e56a80ca 100644 --- a/contracts/tests/sysio.cap_tests.cpp +++ b/contracts/tests/sysio.dclaim_tests.cpp @@ -15,33 +15,33 @@ using namespace sysio::opp::types; using mvo = fc::mutable_variant_object; -/// Test fixture for sysio.cap. Deploys sysio.cap and creates sysio.msgch / +/// Test fixture for sysio.dclaim. Deploys sysio.dclaim and creates sysio.msgch / /// sysio.authex as the authorized inbound callers. The v6 staking-reward path /// credits a WIRE-denominated amount directly (native -> WIRE conversion is /// outpost-side), so no sysio.reserv deployment is needed. -class sysio_cap_tester : public tester { +class sysio_dclaim_tester : public tester { public: - static constexpr auto CAP_ACCOUNT = "sysio.cap"_n; + 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; - sysio_cap_tester() { + sysio_dclaim_tester() { produce_blocks(2); // sysio.authex is created by the tester bootstrap (account-linking // system account); it is signed for directly to drive linkswept, never // re-created here. create_accounts({ - CAP_ACCOUNT, MSGCH_ACCOUNT, TOKEN_ACCOUNT, "alice"_n, "bob"_n, + DCLAIM_ACCOUNT, MSGCH_ACCOUNT, TOKEN_ACCOUNT, "alice"_n, "bob"_n, }); produce_blocks(2); - set_code(CAP_ACCOUNT, contracts::cap_wasm()); - set_abi(CAP_ACCOUNT, contracts::cap_abi().data()); - set_privileged(CAP_ACCOUNT); + set_code(DCLAIM_ACCOUNT, contracts::dclaim_wasm()); + set_abi(DCLAIM_ACCOUNT, contracts::dclaim_abi().data()); + set_privileged(DCLAIM_ACCOUNT); produce_blocks(); - cap_abi_ser.set_abi(load_abi(CAP_ACCOUNT), + dclaim_abi_ser.set_abi(load_abi(DCLAIM_ACCOUNT), abi_serializer::create_yield_function(abi_serializer_max_time)); } @@ -80,18 +80,18 @@ class sysio_cap_tester : public tester { } } - action_result push_cap(name signer, name action_name, const variant_object& data) { - return push(CAP_ACCOUNT, cap_abi_ser, signer, action_name, data); + action_result push_dclaim(name signer, name action_name, const variant_object& data) { + return push(DCLAIM_ACCOUNT, dclaim_abi_ser, signer, action_name, data); } - /// Dispatch a STAKING_REWARD per-staker body to cap::onreward. `amount` is + /// 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 outpost_id, 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_cap(signer, "onreward"_n, mvo() + return push_dclaim(signer, "onreward"_n, mvo() ("outpost_id", outpost_id) ("staker_wire_account", wire_account) ("reward_chain", chain) @@ -103,9 +103,9 @@ class sysio_cap_tester : public tester { } fc::variant get_kv(name table, const char* type, uint64_t id) { - auto data = get_row_by_id(CAP_ACCOUNT, CAP_ACCOUNT, table, id); + auto data = get_row_by_id(DCLAIM_ACCOUNT, DCLAIM_ACCOUNT, table, id); return data.empty() ? fc::variant() - : cap_abi_ser.binary_to_variant( + : dclaim_abi_ser.binary_to_variant( type, data, abi_serializer::create_yield_function(abi_serializer_max_time)); } fc::variant pending_of(name acct) { return get_kv("pclaims"_n, "pending_claim", acct.to_uint64_t()); } @@ -114,23 +114,23 @@ class sysio_cap_tester : public tester { std::vector addr20{std::vector(20, char(0xA1))}; - abi_serializer cap_abi_ser; + abi_serializer dclaim_abi_ser; }; -BOOST_AUTO_TEST_SUITE(sysio_cap_tests) +BOOST_AUTO_TEST_SUITE(sysio_dclaim_tests) // -- config / import surface -- -BOOST_FIXTURE_TEST_CASE(setconfig_initializes_singleton, sysio_cap_tester) { try { - BOOST_REQUIRE_EQUAL(push_cap(CAP_ACCOUNT, "setconfig"_n, mvo{}), success()); +BOOST_FIXTURE_TEST_CASE(setconfig_initializes_singleton, sysio_dclaim_tester) { try { + BOOST_REQUIRE_EQUAL(push_dclaim(DCLAIM_ACCOUNT, "setconfig"_n, mvo{}), success()); } FC_LOG_AND_RETHROW() } -BOOST_FIXTURE_TEST_CASE(claim_rejects_empty_ledger, sysio_cap_tester) { try { - BOOST_REQUIRE_NE(push_cap("alice"_n, "claim"_n, mvo()("wire_account", "alice")), success()); +BOOST_FIXTURE_TEST_CASE(claim_rejects_empty_ledger, sysio_dclaim_tester) { try { + BOOST_REQUIRE_NE(push_dclaim("alice"_n, "claim"_n, mvo()("wire_account", "alice")), success()); } FC_LOG_AND_RETHROW() } -BOOST_FIXTURE_TEST_CASE(importseed_accepts_credit_batch, sysio_cap_tester) { try { - BOOST_REQUIRE_EQUAL(push_cap(CAP_ACCOUNT, "importseed"_n, mvo +BOOST_FIXTURE_TEST_CASE(importseed_accepts_credit_batch, sysio_dclaim_tester) { try { + BOOST_REQUIRE_EQUAL(push_dclaim(DCLAIM_ACCOUNT, "importseed"_n, mvo ("chain", ChainKind::CHAIN_KIND_EVM) ("credits", fc::variants{ mvo()("native_address", addr20)("wire_atomic", 982953049502) })), success()); @@ -140,41 +140,41 @@ BOOST_FIXTURE_TEST_CASE(importseed_accepts_credit_batch, sysio_cap_tester) { try BOOST_REQUIRE_EQUAL(u["balance"].as_string().substr(0, 3), std::string("982")); } FC_LOG_AND_RETHROW() } -BOOST_FIXTURE_TEST_CASE(importseed_rejects_negative_atomic, sysio_cap_tester) { try { - BOOST_REQUIRE_NE(push_cap(CAP_ACCOUNT, "importseed"_n, mvo +BOOST_FIXTURE_TEST_CASE(importseed_rejects_negative_atomic, sysio_dclaim_tester) { try { + BOOST_REQUIRE_NE(push_dclaim(DCLAIM_ACCOUNT, "importseed"_n, mvo ("chain", ChainKind::CHAIN_KIND_EVM) ("credits", fc::variants{ mvo()("native_address", addr20)("wire_atomic", -1) })), success()); } FC_LOG_AND_RETHROW() } -BOOST_FIXTURE_TEST_CASE(importdone_locks_subsequent_importseed, sysio_cap_tester) { try { - BOOST_REQUIRE_EQUAL(push_cap(CAP_ACCOUNT, "importdone"_n, mvo{}), success()); - BOOST_REQUIRE_NE(push_cap(CAP_ACCOUNT, "importseed"_n, mvo +BOOST_FIXTURE_TEST_CASE(importdone_locks_subsequent_importseed, sysio_dclaim_tester) { try { + BOOST_REQUIRE_EQUAL(push_dclaim(DCLAIM_ACCOUNT, "importdone"_n, mvo{}), success()); + BOOST_REQUIRE_NE(push_dclaim(DCLAIM_ACCOUNT, "importseed"_n, mvo ("chain", ChainKind::CHAIN_KIND_EVM) ("credits", fc::variants{ mvo()("native_address", addr20)("wire_atomic", 1) })), success()); } FC_LOG_AND_RETHROW() } -// -- setwindow -- +// -- setclmwindow -- -BOOST_FIXTURE_TEST_CASE(setwindow_rejects_zero, sysio_cap_tester) { try { - BOOST_REQUIRE_NE(push_cap(CAP_ACCOUNT, "setwindow"_n, mvo()("window_sec", 0)), success()); - BOOST_REQUIRE_EQUAL(push_cap(CAP_ACCOUNT, "setwindow"_n, mvo()("window_sec", 3600)), success()); +BOOST_FIXTURE_TEST_CASE(setclmwindow_rejects_zero, sysio_dclaim_tester) { try { + BOOST_REQUIRE_NE(push_dclaim(DCLAIM_ACCOUNT, "setclmwindow"_n, mvo()("window_sec", 0)), success()); + BOOST_REQUIRE_EQUAL(push_dclaim(DCLAIM_ACCOUNT, "setclmwindow"_n, mvo()("window_sec", 3600)), success()); } FC_LOG_AND_RETHROW() } -BOOST_FIXTURE_TEST_CASE(setwindow_requires_self_auth, sysio_cap_tester) { try { - BOOST_REQUIRE_NE(push_cap("alice"_n, "setwindow"_n, mvo()("window_sec", 3600)), success()); +BOOST_FIXTURE_TEST_CASE(setclmwindow_requires_self_auth, sysio_dclaim_tester) { try { + BOOST_REQUIRE_NE(push_dclaim("alice"_n, "setclmwindow"_n, mvo()("window_sec", 3600)), success()); } FC_LOG_AND_RETHROW() } // -- onreward auth + routing -- -BOOST_FIXTURE_TEST_CASE(onreward_requires_msgch_auth, sysio_cap_tester) { try { +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_cap_tester) { try { +BOOST_FIXTURE_TEST_CASE(onreward_linked_credits_pending_claims, sysio_dclaim_tester) { try { BOOST_REQUIRE_EQUAL( onreward(MSGCH_ACCOUNT, 1, "alice", ChainKind::CHAIN_KIND_EVM, addr20, 1000, 7, 100), success()); @@ -184,7 +184,7 @@ BOOST_FIXTURE_TEST_CASE(onreward_linked_credits_pending_claims, sysio_cap_tester BOOST_REQUIRE_EQUAL(p["balance"].as().get_amount(), 1000); } FC_LOG_AND_RETHROW() } -BOOST_FIXTURE_TEST_CASE(onreward_unlinked_parks_unmapped_then_linkswept, sysio_cap_tester) { try { +BOOST_FIXTURE_TEST_CASE(onreward_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), @@ -196,7 +196,7 @@ BOOST_FIXTURE_TEST_CASE(onreward_unlinked_parks_unmapped_then_linkswept, sysio_c // AuthX link sweeps it into pending_claims for bob. BOOST_REQUIRE_EQUAL( - push_cap(AUTHEX_ACCOUNT, "linkswept"_n, mvo() + push_dclaim(AUTHEX_ACCOUNT, "linkswept"_n, mvo() ("wire_account", "bob") ("chain", ChainKind::CHAIN_KIND_EVM) ("native_pubkey", addr20)), @@ -207,7 +207,7 @@ BOOST_FIXTURE_TEST_CASE(onreward_unlinked_parks_unmapped_then_linkswept, sysio_c // -- dedupe cursor -- -BOOST_FIXTURE_TEST_CASE(onreward_dedupes_stale_external_ref, sysio_cap_tester) { try { +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()); @@ -228,8 +228,8 @@ BOOST_FIXTURE_TEST_CASE(onreward_dedupes_stale_external_ref, sysio_cap_tester) { // -- claimable window expiry / reversion -- -BOOST_FIXTURE_TEST_CASE(flushexpired_reverts_expired_pending, sysio_cap_tester) { try { - BOOST_REQUIRE_EQUAL(push_cap(CAP_ACCOUNT, "setwindow"_n, mvo()("window_sec", 1)), success()); +BOOST_FIXTURE_TEST_CASE(flushexpired_reverts_expired_pending, sysio_dclaim_tester) { try { + BOOST_REQUIRE_EQUAL(push_dclaim(DCLAIM_ACCOUNT, "setclmwindow"_n, mvo()("window_sec", 1)), success()); BOOST_REQUIRE_EQUAL( onreward(MSGCH_ACCOUNT, 1, "alice", ChainKind::CHAIN_KIND_EVM, addr20, 1000, 7, 100), success()); @@ -239,7 +239,7 @@ BOOST_FIXTURE_TEST_CASE(flushexpired_reverts_expired_pending, sysio_cap_tester) produce_blocks(10); produce_block(fc::seconds(5)); - BOOST_REQUIRE_EQUAL(push_cap("alice"_n, "flushexpired"_n, mvo()("max_rows", 50)), success()); + BOOST_REQUIRE_EQUAL(push_dclaim("alice"_n, "flushexpired"_n, mvo()("max_rows", 50)), success()); BOOST_REQUIRE(pending_of("alice"_n).is_null()); // reverted to capital fund } FC_LOG_AND_RETHROW() } diff --git a/contracts/tests/sysio.epoch_flushwtdw_tests.cpp b/contracts/tests/sysio.epoch_flushwtdw_tests.cpp index 70ab8eb367..9b4705edfa 100644 --- a/contracts/tests/sysio.epoch_flushwtdw_tests.cpp +++ b/contracts/tests/sysio.epoch_flushwtdw_tests.cpp @@ -52,7 +52,7 @@ class sysio_epoch_flushwtdw_tester : public tester { // RAM to new accounts — too small for the sysio.token deploy // (~100KB). Doing creation first sidesteps that. // - // sysio.cap / sysio.gov / sysio.ops are payepoch destinations for + // sysio.dclaim / sysio.gov / sysio.ops are payepoch destinations for // the capital / governance / capex emission buckets; without them // the first pay epoch's WIRE transfer fails with "to account does // not exist". sysio.authex is auto-created by base_tester (see @@ -61,7 +61,7 @@ class sysio_epoch_flushwtdw_tester : public tester { create_accounts({ TOKEN_ACCOUNT, EPOCH_ACCOUNT, OPREG_ACCOUNT, MSGCH_ACCOUNT, CHALG_ACCOUNT, CHAINS_ACCOUNT, UWRIT_ACCOUNT, BATCHOP, UWRIT_OP, - "sysio.cap"_n, "sysio.gov"_n, "sysio.ops"_n + "sysio.dclaim"_n, "sysio.gov"_n, "sysio.ops"_n }); produce_blocks(2);