Skip to content
Open
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
Binary file modified contracts/sysio.authex/sysio.authex.wasm
Binary file not shown.
6 changes: 6 additions & 0 deletions contracts/sysio.chalg/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ if(BUILD_SYSTEM_CONTRACTS)
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../sysio.epoch/include>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../sysio.system/include>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../sysio.roa>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../sysio.uwrit/include>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../sysio.reserv/include>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../sysio.opreg/include>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../sysio.chains/include>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../sysio.authex/include>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../sysio.msgch/include>
)

target_link_libraries(${target}
Expand Down
181 changes: 181 additions & 0 deletions contracts/sysio.chalg/include/sysio.chalg/sysio.chalg.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,116 @@ namespace sysio {
[[sysio::action]]
void chkdispute(uint64_t dispute_id);

// -----------------------------------------------------------------------
// Underwriter-fault challenge (WIRE-297)
// -----------------------------------------------------------------------
//
// The OPP envelope dispute's sibling. Same adjudication machinery — Tier-1 electorate
// snapshotted at open, fixed quorum floor(N/2)+1, record-only votes, permissionless tally
// crank, resolution through the `slashop` -> `opreg::slash` chokepoint — with the
// differences the problem forces:
//
// * A human FILES it (the depot can observe envelope divergence itself; a source-chain
// fault it cannot — someone must allege it), so filing is permissionless and priced
// with a challenger bond.
// * The ballot is a VERDICT (uphold / reject), not a choice among candidate versions.
// * It never pauses the epoch: an unresolved challenge is survivable — the challenged
// collateral lock simply lapses back to a normal release — so the chain keeps advancing.
// * The vote deadline is the commitment's own collateral-lock expiry, and there is NO
// after-deadline relaxed tally: a challenge "must be voted on before the window
// expires" (Jonathan, 2026-07-27); past it the challenge LAPSES with a full bond
// refund. Envelope disputes relax after their deadline only because a paused chain
// MUST eventually resolve.

/// The fault a challenger alleges against the winning underwriter's commit. The council
/// adjudicates the allegation against source-chain state (not visible to the depot) and
/// votes; the enum classifies the case for the audit row and indexers.
enum class underwrite_fault_reason : uint8_t {
SOURCE_DEPOSIT_MISSING = 0, ///< the committed source deposit does not exist on the source chain
SOURCE_DEPOSIT_MISMATCH = 1, ///< the deposit exists but its parameters do not match the commit
NON_CANONICAL_ENVELOPE = 2, ///< the commit rides an envelope later adjudicated non-canonical
};

/// A Tier-1 voter's ballot in an underwriter challenge. The two reject flavours let the
/// council separate an honest mistake (bond refunded to the challenger) from a frivolous or
/// malicious challenge (bond forfeited to the wrongly-challenged underwriter) — forfeiture
/// only ever happens by explicit council judgment, never by default.
enum class uwchal_ballot : uint8_t {
UPHOLD = 0, ///< fault proven — slash the underwriter; bond returns to the challenger
REJECT_REFUND = 1, ///< no fault; honest mistake — bond returns to the challenger
REJECT_FORFEIT = 2, ///< no fault; frivolous or malicious — bond goes to the underwriter
};

/// Terminal verdict of a challenge (NONE while it is OPEN).
enum class uwchal_verdict : uint8_t {
NONE = 0, ///< challenge still open
UPHELD = 1, ///< underwriter slashed; bond returned to the challenger
REJECTED_REFUND = 2, ///< council rejected — honest mistake; bond returned to the challenger
REJECTED_FORFEIT = 3, ///< council rejected — frivolous; bond forfeited to the underwriter
LAPSED = 4, ///< lock window expired with no quorum — bond returned; nobody punished
};

/// Open a challenge against the WINNING underwriter of a CONFIRMED underwrite request,
/// alleging its commit was faulty. Permissionless — anyone may file — but time-gated to the
/// commitment's collateral-lock window and priced: the challenger escrows the WIRE value of
/// the challenged locks as a bond (`uwchalbond` quotes it; both run one shared formula, so
/// the quote can never drift from the charge — the `split_wire_fee` principle).
///
/// Effects, in order: validates the commitment (uwreq CONFIRMED, `underwriter` is its
/// winner, no prior challenge for this commitment — a verdict is final per commitment;
/// other commitments by the same underwriter are independently challengeable), snapshots
/// the Tier-1 electorate + quorum exactly as `opendispute` does, escrows the bond (inline
/// `sysio.token::transfer` under the challenger's own authority), records the OPEN row, and
/// marks the locks via `sysio.uwrit::holdlocks` — a held lock is NOT released at expiry
/// while its challenge is open. uwrit re-validates lock liveness authoritatively, so a
/// stale filing aborts whole, bond escrow included.
///
/// `reason` is the numeric `underwrite_fault_reason` value (checked through
/// `magic_enum::enum_cast` at this trust boundary); `detail` carries the challenger's
/// free-text evidence context for the council (source tx ids, observed amounts, notes).
[[sysio::action]]
void openuwchal(name challenger,
uint64_t uwreq_id,
name underwriter,
uint8_t reason,
std::string detail);

/// Cast a Tier-1 ballot in an open underwriter challenge. `ballot` is the numeric
/// `uwchal_ballot` value. Same eligibility rules as `votedispute`: the voter must belong to
/// the challenge's snapshotted electorate, one vote per owner. Record-only — the tally
/// lives in `chkuwchal`, mirroring the votedispute/chkdispute split.
[[sysio::action]]
void voteuwchal(name owner, uint64_t chal_id, uint8_t ballot);

/// Permissionless tally crank — `chkdispute`'s sibling. Nobody has to run it on a cadence:
/// `sysio.uwrit::chklocks` (inlined from every `sysio.epoch::advance`) pokes it for any
/// expired-but-challenged lock, which works precisely because the chain is NOT paused
/// during an underwriter challenge. A manual call after the deciding vote resolves sooner.
///
/// Resolution rules (N/Q = the snapshotted electorate size / quorum, fixed at open):
/// * UPHOLD ballots reach Q, any time — UPHELD: `slashop` the underwriter, then
/// `sysio.uwrit::sweeplocks` releases the now-deferred-slash locks (the slash lands
/// first — inline actions run depth-first in send order — so `opreg::releaselock`
/// takes its SLASHED branch, debiting the locked collateral and emitting the outbound
/// SLASH attestations); the bond returns to the challenger.
/// * REJECT ballots (both flavours combined) reach Q, any time — REJECTED: disposition by
/// majority among the rejectors, a tie favouring refund; `sysio.uwrit::freelocks`
/// clears the hold and the locks live out their natural window.
/// * `deadline_ms` (the locks' own expiry) passes with neither threshold met — LAPSED:
/// bond returned to the challenger, holds cleared, the locks release on the next
/// normal sweep. No verdict, nobody punished.
[[sysio::action]]
void chkuwchal(uint64_t chal_id);

/// Read-only: the WIRE bond `openuwchal` would require RIGHT NOW to challenge this
/// commitment — the winning underwriter's live lock amounts, each valued through its own
/// reserve's current books (`opp::amm::token_to_wire`), summed. Returns 0 when the
/// commitment is not currently challengeable (no CONFIRMED uwreq, `underwriter` is not its
/// winner, a challenge already exists, or no live unheld locks) or when a leg cannot be
/// priced. Shares its formula helper with `openuwchal`.
[[sysio::action, sysio::read_only]]
uint64_t uwchalbond(uint64_t uwreq_id, name underwriter);

// -----------------------------------------------------------------------
// Tables
// -----------------------------------------------------------------------
Expand Down Expand Up @@ -153,12 +263,83 @@ namespace sysio {

using chalgstate_t = sysio::kv::global<"chalgstate"_n, chalg_state>;

/// Challenge primary key (auto-incrementing id).
struct uwchal_key {
uint64_t id;
uint64_t primary_key() const { return id; }
SYSLIB_SERIALIZE(uwchal_key, (id))
};

/// An underwriter-fault challenge. Opened permissionlessly against one CONFIRMED
/// commitment; resolved by a Tier-1 vote or lapsed at the lock window's end. The row is
/// retained after resolution as the audit record AND as the guard that a commitment is
/// challenged at most once, ever (mirrors one-dispute-per-(outpost, epoch)).
struct [[sysio::table("uwchals")]] uwchal_entry {
uint64_t id;
uint64_t uwreq_id; ///< the challenged commitment's uwreq
name underwriter; ///< the CONFIRMED winner under challenge
name challenger; ///< who filed and posted the bond
underwrite_fault_reason reason; ///< alleged fault class
std::string detail; ///< challenger's free-text evidence context
opp::types::DisputeStatus status; ///< OPEN / RESOLVED — same lifecycle enum as `disputes`
uwchal_verdict verdict; ///< NONE while OPEN; the terminal outcome after
uint64_t bond_amount = 0; ///< WIRE the challenger escrowed (9-decimal units)
/// The challenged locks' `expires_at_ms` — the vote deadline. Past it an unresolved
/// challenge LAPSES (bond refunded); there is no after-deadline relaxed tally.
uint64_t deadline_ms = 0;
time_point opened_at{};
uint8_t network_gen = 0; ///< roa network generation the electorate was drawn from
/// The Tier-1 electorate + quorum, snapshotted at open — same discipline as
/// `dispute_entry`: voter eligibility (`voteuwchal`) and the tally denominator
/// (`chkuwchal`) come from one list frozen when the challenge opened, so registrations
/// after open can neither join nor dilute it.
std::vector<name> electorate;
uint32_t quorum = 0;

/// Uniqueness: one challenge EVER per (uwreq, underwriter) commitment.
uint128_t by_uwreq_underwriter() const {
return (static_cast<uint128_t>(uwreq_id) << 64) | underwriter.value;
}

SYSLIB_SERIALIZE(uwchal_entry,
(id)(uwreq_id)(underwriter)(challenger)(reason)(detail)(status)(verdict)
(bond_amount)(deadline_ms)(opened_at)(network_gen)(electorate)(quorum))
};

using uwchals_t = sysio::kv::table<"uwchals"_n, uwchal_key, uwchal_entry,
sysio::kv::index<"byuwrequw"_n,
sysio::const_mem_fun<uwchal_entry, uint128_t, &uwchal_entry::by_uwreq_underwriter>>
>;

/// Challenge-vote primary key (the voting Tier-1 owner). The vote table is scoped by
/// `chal_id`, so the owner alone is unique within a challenge.
struct uwchal_vote_key {
uint64_t owner;
uint64_t primary_key() const { return owner; }
SYSLIB_SERIALIZE(uwchal_vote_key, (owner))
};

/// One Tier-1 ballot in an underwriter challenge. Scoped by `chal_id`.
struct [[sysio::table("uwchalvote")]] uwchal_vote {
name owner;
uwchal_ballot ballot;
time_point voted_at{};

SYSLIB_SERIALIZE(uwchal_vote, (owner)(ballot)(voted_at))
};

using uwchalvotes_t =
sysio::kv::scoped_table<"uwchalvote"_n, uwchal_vote_key, uwchal_vote>;

private:
// Well-known accounts
static constexpr name EPOCH_ACCOUNT = "sysio.epoch"_n;
static constexpr name MSGCH_ACCOUNT = "sysio.msgch"_n;
static constexpr name OPREG_ACCOUNT = "sysio.opreg"_n;
static constexpr name ROA_ACCOUNT = "sysio.roa"_n;
static constexpr name UWRIT_ACCOUNT = "sysio.uwrit"_n;
static constexpr name RESERV_ACCOUNT = "sysio.reserv"_n;
static constexpr name TOKEN_ACCOUNT = "sysio.token"_n;

using DisputeStatus = opp::types::DisputeStatus;
using NodeOwnerTier = opp::types::NodeOwnerTier;
Expand Down
Loading
Loading