Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions OVERVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Envelope (one per outpost per epoch, per direction)
- **Epochs**: Time-bounded intervals managed by `sysio.epoch` — one global epoch clock for every chain. Each epoch has one group of batch operators on duty as relayers.
- **Envelopes**: Epoch-level containers. Each envelope carries the epoch index and timestamp, its route endpoints, and `previous_envelope_hash` — the digest of the previous envelope on the same (depot, outpost) stream — so each direction forms a hash chain. An envelope is capped at 32,768 bytes on every chain.
- **Messages**: Individual cross-chain payloads containing a header (message ID, sequence, checksum, encoding flags) and a payload (an array of attestation entries).
- **Attestations**: Typed data units that represent cross-chain actions — operator rosters, liquidity syndication and desyndication, yield reports, node-owner registrations, staking rewards.
- **Attestations**: Typed data units that represent cross-chain actions — operator rosters, liquidity syndication and desyndication, yield reports and node-owner registrations.

### Chain Addressing

Expand Down Expand Up @@ -71,7 +71,6 @@ This allows a single unified message format across all supported chains.
| **Liquidity Syndication** | `SYNDICATE_LIQ`, `LIQ_YIELD` | outpost → depot | A user syndicating liq tokens into outpost custody; realized custody yield |
| **Liquidity Desyndication** | `DESYNDICATE_LIQ` | depot → outpost | Return syndicated liq tokens to a holder on the outpost |
| **Node Owners** | `NODE_OWNER_REG` | outpost → depot | Node-owner NFT committed on Ethereum, registering the owner on the depot |
| **Staking** | `STAKING_REWARD` | outpost → depot | Per-staker reward credited to the `sysio.dclaim` claim ledger (validator-staking lifecycle types are reserved but deferred) |
Comment thread
huangminghuang marked this conversation as resolved.
Resolved

Cross-chain swaps routed through outposts, outpost reserves and outpost-side operator collateral have been removed. Swaps are a depot-local AMM (`sysio.swap`) with no outpost participation; operator collateral is held on the depot by `sysio.opreg`.

Expand Down Expand Up @@ -112,7 +111,7 @@ Holders of a node-owner NFT who commit it on the Ethereum outpost (`NODE_OWNER_R
| **sysio.bond** | Generic bonded underwriting of provable statements |
| **sysio.liq** | Shadow liq tokens (`LIQETH`, `LIQSOL`) minted against outpost custody, with a WIRE yield index |
| **sysio.swap** | Depot-local constant-product AMM; every pair's second leg is WIRE |
| **sysio.dclaim** | WIRE claim ledger (launch import, staking rewards) |
| **sysio.dclaim** | Funded WIRE launch import, account linking and claims |
| **sysio.andon** | Emergency stop: one cord freezing depot fund egress |
| **sysio.roa** | Resources and node-owner registry |
| **sysio.authex** | Links a depot account to its external-chain keys |
Expand Down
97 changes: 4 additions & 93 deletions contracts/sysio.dclaim/include/sysio.dclaim/sysio.dclaim.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,15 @@ namespace sysio {
/**
* @brief sysio.dclaim — depot-side WIRE distribution and claim ledger.
*
* Holds the pending-WIRE balances owed to LIQ-token stakers and pre-launch
* pretoken purchasers, and the per-staker WIRE-side leg of the
* `STAKING_REWARD` flow.
*
* Inbound `STAKING_REWARD` attestations route through `sysio.msgch`, which
* dispatches the per-staker body here via `onreward`. The reward arrives
* already WIRE-denominated — native -> WIRE conversion and source-chain
* precision scaling happen outpost-side — so `onreward` simply credits the
* staker's claim ledger.
* Holds funded pre-launch WIRE distribution credits and preserves existing claims.
* External staking rewards no longer create credits or draw from T5.
*
* Ledgers:
* - `pending_claims` — per-Wire-account WIRE owed. `claim` drains a row via
* 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`.
* - `reward_cursors` — per-(chain_code, chain, native_pubkey) high-water
* mark of the last processed source-chain epoch reference. Replays /
* duplicates (`external_epoch_ref <= last`) are rejected at ingest, so no
* per-reward history is retained (roll-up + data-leak safe).
*
* Credited and imported balances never expire, including balances waiting
* for AuthX linking. They remain owed until claimed; there is no expiry
Expand All @@ -56,10 +45,7 @@ namespace sysio {

// Well-known accounts.
static constexpr name AUTHEX_ACCOUNT = "sysio.authex"_n;
static constexpr name MSGCH_ACCOUNT = "sysio.msgch"_n;
static constexpr name TOKEN_ACCOUNT = "sysio.token"_n;
static constexpr name RESERV_ACCOUNT = "sysio.reserv"_n;
static constexpr name SYSTEM_ACCOUNT = "sysio"_n;

/// WIRE token symbol with the system-wide nine-decimal precision.
static constexpr symbol WIRE_SYM = opp::wire::asset_symbol;
Expand Down Expand Up @@ -88,37 +74,6 @@ namespace sysio {
opp::types::ChainKind chain,
std::vector<char> native_pubkey);

/// Per-staker WIRE-side credit of a `STAKING_REWARD`. Dispatched inline
/// by `sysio.msgch` (the proto body flattened to primitives). Dedupes
/// on the source-chain epoch reference and credits `pending_claims` (if
/// linked) or `unmapped_tokens` (if not). The reward arrives already
/// WIRE-denominated; native -> WIRE conversion is outpost-side.
/// Auth=sysio.msgch.
///
/// @param chain_code Emitting outpost's chain code (dedupe scope).
/// @param staker_wire_account Staker's Wire account name, or "" when not
/// yet AuthX-linked (then parked by native
/// address until the link sweep).
/// @param reward_chain Source chain (parking + dedupe key).
/// @param staker_native_addr Staker's raw native address (dedupe +
/// parking key; always populated).
/// @param reward_amount Absolute WIRE reward amount in atomic
/// units (already the staker's prorated
/// portion).
/// @param reward_epoch_index WIRE epoch index (informational / audit).
/// @param external_epoch_ref Source-chain epoch reference; monotonic
/// per (chain, staker) — dedupe key.
/// @param share_bps Staker share in bps (informational only).
[[sysio::action]]
void onreward(uint64_t chain_code,
std::string staker_wire_account,
opp::types::ChainKind reward_chain,
std::vector<char> staker_native_addr,
uint64_t reward_amount,
uint32_t reward_epoch_index,
uint64_t external_epoch_ref,
uint32_t share_bps);

/// One row of an import batch: a pre-launch holder's WIRE credit on
/// `chain`. `native_address` is the raw on-chain key (20 B for ETH,
/// 32 B for Solana). `wire_atomic` is denominated in WIRE's 9-decimal
Expand Down Expand Up @@ -194,37 +149,7 @@ namespace sysio {
sysio::const_mem_fun<unmapped_token, uint128_t, &unmapped_token::by_chain_addr>>
>;

/// Per-(chain_code, chain, native_pubkey) dedupe cursor: the highest
/// source-chain epoch reference processed. Anything `<=` is a replay.
struct rwdcur_key {
uint64_t id;
uint64_t primary_key() const { return id; }
SYSLIB_SERIALIZE(rwdcur_key, (id))
};

struct [[sysio::table("rwdcursors")]] reward_cursor {
uint64_t id = 0;
uint64_t chain_code = 0;
opp::types::ChainKind chain = opp::types::ChainKind::CHAIN_KIND_UNKNOWN;
std::vector<char> native_pubkey;
uint64_t last_external_epoch_ref = 0;

uint64_t primary_key() const { return id; }

uint128_t by_chaincode_addr() const {
return chaincode_addr_key(chain_code, chain, native_pubkey);
}

SYSLIB_SERIALIZE(reward_cursor,
(id)(chain_code)(chain)(native_pubkey)(last_external_epoch_ref))
};

using rwdcursors_t = sysio::kv::table<"rwdcursors"_n, rwdcur_key, reward_cursor,
sysio::kv::index<"bychaincode"_n,
sysio::const_mem_fun<reward_cursor, uint128_t, &reward_cursor::by_chaincode_addr>>
>;

/// Cap-staking config singleton.
/// Bootstrap import config singleton.
struct [[sysio::table("capcfg")]] cap_config {
/// One-way flag protecting the bootstrap `importseed` action.
bool imported_complete = false;
Expand All @@ -236,9 +161,8 @@ namespace sysio {
/// Monotonic id counters.
struct [[sysio::table("capcounters")]] cap_counters {
uint64_t next_unmapped_id = 1;
uint64_t next_cursor_id = 1;

SYSLIB_SERIALIZE(cap_counters, (next_unmapped_id)(next_cursor_id))
SYSLIB_SERIALIZE(cap_counters, (next_unmapped_id))
};

using capcounters_t = sysio::kv::global<"capcounters"_n, cap_counters>;
Expand All @@ -263,19 +187,6 @@ namespace sysio {
return (static_cast<uint128_t>(chain) << 64) | prefix;
}

/// (chain_code, chain, native address) -> uint128 narrowing key. High 64
/// bits = chain_code; low 64 = chain (high 32) xored with the first 4
/// address bytes.
static uint128_t chaincode_addr_key(uint64_t chain_code,
opp::types::ChainKind chain,
const std::vector<char>& 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<uint64_t>(chain) << 32) ^ static_cast<uint64_t>(prefix);
return (static_cast<uint128_t>(chain_code) << 64) | lo;
}

private:
using ChainKind = opp::types::ChainKind;
using TokenKind = opp::types::TokenKind;
Expand Down
123 changes: 8 additions & 115 deletions contracts/sysio.dclaim/src/sysio.dclaim.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#include <sysio.dclaim/sysio.dclaim.hpp>
#include <sysio.opp.common/safe_ops.hpp>

#include <cstdint>
#include <string>
Expand All @@ -18,8 +17,7 @@ constexpr name ram_payer = "sysio"_n;
/// Exact-match scan over a uint128 secondary index: `lower_bound` then walk
/// while the narrowing key still matches, returning the first row the
/// predicate accepts (or `idx.end()`). The uint128 key only narrows; the
/// predicate resolves prefix collisions deterministically. One implementation
/// shared by the unmapped + cursor lookups (no duplicated scan loops).
/// predicate resolves prefix collisions deterministically for unmapped-credit lookups.
template<class Index, class KeyFn, class MatchFn>
auto scan_find(Index& idx, uint128_t key, KeyFn key_of, MatchFn matches) {
auto it = idx.lower_bound(key);
Expand All @@ -42,14 +40,9 @@ uint64_t next_id(name self, Pick pick) {
return id;
}

/// Non-throwing validation of a string destined for `name(std::string_view)`. Shared with every
/// other OPP inbound handler via `sysio.opp.common/safe_ops.hpp` so the never-throw name domain is
/// defined and audited in exactly one place.

/// Saturating WIRE credit. `asset::operator+=` aborts on overflow past `asset::max_amount`
/// (2^62-1); credit_wire runs inside the never-throw OPP inbound path (via onreward), so cap at
/// the asset maximum rather than abort. A single staker balance approaching 2^62 atomic WIRE units
/// is not reachable in practice — the cap exists purely to preserve the never-throw contract.
/// (2^62-1). AuthX linkswept is reachable through OPP node-owner registration and must not
/// abort that delivery. Both imported and swept balances preserve saturation at the asset maximum.
inline void add_wire_capped(asset& balance, const asset& amt) {
const int64_t room = asset::max_amount - balance.amount; // balance.amount in [0, max_amount]
balance.amount += (amt.amount <= room ? amt.amount : room);
Expand All @@ -70,16 +63,9 @@ void credit_pending(name self, name wacct, const asset& amt) {
}
}

/// Credit `amt` WIRE to the staker. Linked (`wacct` set) -> `pending_claims`;
/// otherwise parked in `unmapped_tokens` keyed by (chain, addr). Both ledgers
/// retain unclaimed balances indefinitely.
void credit_wire(name self, name wacct, ChainKind chain,
const std::vector<char>& addr, const asset& amt) {
if (wacct.value != 0) {
credit_pending(self, wacct, amt);
return;
}

/// Park an imported WIRE credit by native identity until AuthX links it to a claim account.
void credit_unmapped(name self, ChainKind chain,
const std::vector<char>& addr, const asset& amt) {
dclaim::unmapped_t unmapped(self);
auto idx = unmapped.template get_index<"bychainad"_n>();
auto it = scan_find(idx, dclaim::chain_addr_key(chain, addr),
Expand All @@ -104,42 +90,6 @@ void credit_wire(name self, name wacct, ChainKind chain,
}
}

/// Dedupe at ingest. Returns true if `(chain_code, chain, addr)` has not seen
/// `ext_ref` (or any >= it) yet — and advances the cursor. Returns false for a
/// replay / out-of-order duplicate (`ext_ref <= last`). Advancing here (not at
/// conversion time) means a replay is rejected even while an earlier reward is
/// still staged awaiting a quote.
bool cursor_admit(name self, uint64_t chain_code, ChainKind chain,
const std::vector<char>& addr, uint64_t ext_ref) {
dclaim::rwdcursors_t cur(self);
auto idx = cur.template get_index<"bychaincode"_n>();
auto it = scan_find(idx, dclaim::chaincode_addr_key(chain_code, chain, addr),
[](const auto& r) { return r.by_chaincode_addr(); },
[&](const auto& r) {
return r.chain_code == chain_code
&& r.chain == chain
&& r.native_pubkey == addr;
});
if (it == idx.end()) {
uint64_t id = next_id(self, [](dclaim::cap_counters& c) -> uint64_t& {
return c.next_cursor_id;
});
cur.emplace(ram_payer, dclaim::rwdcur_key{id},
dclaim::reward_cursor{ .id = id,
.chain_code = chain_code,
.chain = chain,
.native_pubkey = addr,
.last_external_epoch_ref = ext_ref });
return true;
}
if (ext_ref <= it->last_external_epoch_ref) return false;
uint64_t rid = it->id;
cur.modify(same_payer, dclaim::rwdcur_key{rid}, [&](auto& r) {
r.last_external_epoch_ref = ext_ref;
});
return true;
}

} // anonymous namespace

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -196,61 +146,6 @@ void dclaim::linkswept(name wire_account, ChainKind chain, std::vector<char> nat
}
}

// ---------------------------------------------------------------------------
// onreward — per-staker WIRE-side credit of a STAKING_REWARD
// ---------------------------------------------------------------------------
void dclaim::onreward(uint64_t chain_code,
std::string staker_wire_account,
opp::types::ChainKind reward_chain,
std::vector<char> staker_native_addr,
uint64_t reward_amount,
uint32_t reward_epoch_index,
uint64_t external_epoch_ref,
uint32_t share_bps) {
require_auth(MSGCH_ACCOUNT);

// Tolerate degenerate input rather than aborting the inbound OPP envelope
// (the verifier role lives upstream in msgch::evalcons; dclaim trusts but
// must not break the message chain on a malformed row). reward_amount is cross-chain-supplied;
// bound it to the asset range here so an oversized value soft-drops instead of aborting later
// when the WIRE asset is constructed/credited (asset()/operator+= range-check abort).
if (reward_amount == 0 || reward_amount > static_cast<uint64_t>(asset::max_amount) ||
staker_native_addr.empty()) return;

// Dedupe at ingest so a replay / out-of-order duplicate is rejected.
if (!cursor_admit(get_self(), chain_code, reward_chain,
staker_native_addr, external_epoch_ref)) {
return;
}

name wacct; // value 0 == not yet AuthX-linked
// Validate the cross-chain-supplied account string before constructing name(): an invalid
// string is treated as unlinked (credit parked by native address) rather than aborting the
// inbound dispatch via name()'s internal check(). `is_valid_literal` is name's OWN predicate --
// the one its constructor uses -- so it cannot drift from what the constructor accepts.
if (!staker_wire_account.empty() && sysio::name::is_valid_literal(staker_wire_account)) {
wacct = name(staker_wire_account);
}

// reward_amount arrives already WIRE-denominated -- native -> WIRE
// conversion and source-chain precision scaling are outpost-side -- so the
// claim ledger is credited directly.
credit_wire(get_self(), wacct, reward_chain, staker_native_addr,
asset{ static_cast<int64_t>(reward_amount), WIRE_SYM });

// Pull funding from sysio.system's drainable pool so the dclaim balance
// covers this credit immediately -- a staker can claim in the next block
// rather than waiting for a pay-epoch. sysio.system::fundclaim caps to
// the remaining pool and never throws, preserving the never-throw
// contract for OPP inbound dispatch.
action(
permission_level{ get_self(), "active"_n },
SYSTEM_ACCOUNT,
"fundclaim"_n,
std::make_tuple(get_self(), static_cast<int64_t>(reward_amount))
).send();
}

// ---------------------------------------------------------------------------
// importseed — bootstrap pre-launch holders into unmapped_tokens
// ---------------------------------------------------------------------------
Expand All @@ -268,10 +163,8 @@ void dclaim::importseed(ChainKind chain, std::vector<import_credit> credits) {
check(!credit.native_address.empty(), "empty native_address");
if (credit.wire_atomic == 0) continue;

// Pre-launch holders are unlinked by definition -> name{} routes the
// credit to unmapped_tokens, with the same non-expiring upsert path as
// staking rewards (one implementation in credit_wire).
credit_wire(get_self(), name{}, chain, credit.native_address,
// Pre-launch holders remain unmapped until an AuthX link sweeps their credit.
credit_unmapped(get_self(), chain, credit.native_address,
asset{ credit.wire_atomic, WIRE_SYM });
}
}
Expand Down
Loading
Loading